Хватит терять клиентов! Или как разработчику тестировать сайт, на примере PVS-Studio. Часть 1

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Сайт с багами – горе для бизнеса. Всего одна 404-я или 500-я ошибка может лишить вашу компанию солидной суммы денег и хорошей репутации. Но есть способ избежать этого: тестировать сайт. О том, как это сделать, расскажет данная статья. Прочитав её, вы узнаете, как тестировать код на Django, создать своего пользователя-тестировщика и много чего ещё. Добро пожаловать под кат.

Что вы делаете, когда пишете тесты?

Как бы вы ответили на этот вопрос? Я бы сказал – кайфую. У каждого разработчика есть своё мнение на счёт тестов. Лично мне этот процесс безумно нравится. Благодаря ему я могу не только писать более надёжный код, но и начинаю лучше разбираться в своих и чужих программах. А вишенкой на торте является чувство, испытываемое, когда все тесты становятся зелёными. В этот момент моя шкала перфекционизма достигает своего апогея.

Иногда при тестировании меня затягивает так, будто я прохожу Half-Life. Я начинаю проводить за этим процессом всё рабочее и свободное время. Конечно, со временем тесты надоедают, и тогда приходится делать перерыв. После чего опять можно стать ноулайфером на несколько недель, будто Valve выпустила новую часть. Если у вас также, то вы понимаете, о чём я. Но хватит разговоров, перейдём к делу!

Тестирование бэкенда

Наш сайт написан на Django, поэтому примеры кода указаны для этого фреймворка.

Перед началом советую ознакомиться со списком рекомендаций, которые структурируют и делают более комфортным процесс написания тестов. Я сформировал их за счёт советов других программистов и личного опыта.

  • Файлы с тестами хранятся в папке tests внутри приложения;

  • тесты моделей, вьюх и форм лежат в файлах test_models.pytest_views.py и test_forms.py соответственно;

  • название тестового метода начинается с префикса test_ (например, test_get_sum или test_status_code);

  • название класса, содержащего тесты, имеет вид: ТестируемаяСущностьTests (например, TrialTests или FeedbackFileTests).

Тестирование моделей

Создадим приложение my_app и заполним файл models.py следующим кодом:

from django.db import models

class Trial(models.Model):
    """Простая модель пользовательского триала"""

    email = models.EmailField(
        verbose_name='Электронная почта',
        max_length=256,
        unique=False,
    )

    def __str__(self):
        return str(self.email)

    class Meta:
        verbose_name = 'Триал'
        verbose_name_plural = 'Триалы'

Эта модель – упрощённая версия нашей модели Trial. Вот, что мы можем у неё проверить:

  1. У поля email параметр verbose_name – "Электронная почта".

  2. У поля email параметр max_length – 256.

  3. У поля email параметр unique – False.

  4. Метод str возвращает значение параметра email.

  5. Параметр verbose_name модели – "Триал".

  6. Параметр verbose_name_plural модели – "Триалы".

От некоторых программистов мне приходилось слышать мнение, что тестирование моделей – бесполезная трата времени. Но мой опыт подсказывает, что это мнение ошибочное. Приведу простой пример. Для поля email мы указали максимальную длину – 256 (в соответствии с документом RFC 2821). Случайно удалить последнюю цифру – не большая проблема. Если такая оплошность вдруг произойдёт, то пользователь с почтой my_super_long_email@gmail.com (29 символов) получит ошибку и не сможет отправить запрос на триал. А значит, компания потеряет потенциального клиента. Конечно, можно написать дополнительную валидацию, но лучше быть уверенным, что и без неё программа успешно работает.

Перейдём к тестам и сначала решим, где они будут находиться. Вы можете писать все тесты в одном файле tests.py (Django добавляет его при создании приложения) либо последовать рекомендациям выше и разделить их.

Если второй вариант вам импонирует больше, то удалите tests.py. Затем создайте папку tests с пустым файлом __init__.py. При запуске тестов он скажет Python, где их искать. Сюда же добавляем еще 3 файла: test_forms.pytest_models.py и test_views.py. Содержимое директории приложения будет примерно таким:

Открываем файл test_models.py и добавляем в него следующий код:

from django.test import TestCase

from my_app.models import Trial

class TrialTests(TestCase):
    """Тесты для модели Trial"""

    def test_verbose_name(self):
        pass

    def test_max_length(self):
        pass

    def test_unique(self):
        pass

    def test_str_method(self):
        pass

    def test_model_verbose_name(self):
        pass

    def test_model_verbose_name_plural(self):
        pass

У Django для тестирования есть специальный модуль django.test. Один из самых важных его классов — TestCase. Именно он и позволяет писать тесты. Чтобы это сделать, нам нужно просто наследовать наш класс от TestCase.

Все наши тесты – это методы класса TrialTests. Пока они ничего не делают, но это ненадолго. Каждый из них будет тестировать по одному условию из списка выше.

Разберёмся с запуском тестов. Чтобы разом выполнить все тесты вашего сайта, введите эту команду в консоли:

python manage.py test

Для запуска тестов конкретного класса, например TrialTests, пишем:

python manage.py test my_app.tests.test_models.TrialTests

Любая из этих команд запустит наши 6 тестов. Выбираем одну из них, вводим в консоль, жмём Enter и получаем примерно такой вывод:

Из него видно, что за 0.001 секунду проверено 6 тестов. "OK" в конце вывода говорит об их успешном выполнении.

Теперь напишем настоящие тесты. Чтобы это сделать, нам придётся обращаться к параметрам объекта модели Trial. Значит, нужно создать этот объект. И тут важно знать, что для тестов Django использует отдельную чистую базу. Перед прогоном тестов она создаётся, после — удаляется. На скриншоте выше об этом говорят первая и последняя строчки. Если вдруг по какой-то причине база не смогла удалиться, Django скажет об этом и придётся снести её вручную.

Чтобы работать с этой базой, можно использовать 3 метода:

  1. setUp – выполняется перед запуском каждого теста;

  2. tearDown – выполняется после завершения каждого теста;

  3. setUpTestData – выполняется перед запуском всех тестов конкретного класса.

Воспользуемся последним. Поскольку он является методом класса, то добавляем соответствующий декоратор. Внутри создаём объект класса Trial и получаем от него поле email, которое мы будем использовать в самих тестах.

class TrialTests(TestCase):
    """Тесты для модели Trial"""

    @classmethod
    def setUpTestData(cls):
        """Заносит данные в БД перед запуском тестов класса"""

        cls.trial = Trial.objects.create(
            email='test@gmail.com'
        )
        cls.email_field = cls.trial._meta.get_field('email')

Теперь при запуске тестов класса TrialTests в новой базе будет создаваться объект trial. После прогона он будет удаляться.

Напишем тест параметра verbose_name.

def test_verbose_name(self):
    """Тест параметра verbose_name"""

    real_verbose_name = getattr(self.email_field, 'verbose_name')
    expected_verbose_name = 'Электронная почта'

    self.assertEqual(real_verbose_name, expected_verbose_name)

Из поля email_field мы извлекаем значение параметра verbose_name. Затем применяем метод assertEqual из класса TestCase. Он сравнивает два параметра – реальное и ожидаемое значения verbose_name. Если они равны, тест отработает удачно. В противном случае – упадёт.

Напишем такие же тесты для параметров max_length и unique.

def test_max_length(self):
    """Тест параметра max_length"""

    real_max_length = getattr(self.email_field, 'max_length')

    self.assertEqual(real_max_length, 256)

def test_unique(self):
    """Тест параметра unique"""

    real_unique = getattr(self.email_field, 'unique')

    self.assertEqual(real_unique, False)

Тут всё так же, как и с verbose_name.

Кстати, в тесте параметра unique мы проверяем, что значение равно False. Можно сделать это проще с помощью команды assertFalse. Перепишем код этого теста.

def test_unique(self):
    """Тест параметра unique"""

    real_unique = getattr(self.email_field, 'unique')

    self.assertFalse(real_unique)

Код немного уменьшился и стал более читабельным. Кстати, у Django много таких полезных assert-ов.

Теперь проверим строковое отображение объекта.

def test_string_representation(self):
    """Тест строкового отображения"""

    self.assertEqual(str(self.trial), str(self.trial.email))

Тут всё просто. Проверяем, что строковое отображение объекта равно его электронной почте.

И последнее – это тесты полей модели:

def test_model_verbose_name(self):
    """Тест поля verbose_name модели Trial"""

    self.assertEqual(Trial._meta.verbose_name, 'Триал')

def test_model_verbose_name_plural(self):
    """Тест поля verbose_name_plural модели Trial"""

    self.assertEqual(Trial._meta.verbose_name_plural, 'Триалы')

Через _meta обращаемся к полям модели Trial и сравниваем их значение с ожидаемым.

Если сейчас запустить тесты, то они успешно отработают, как и раньше. Но ведь так неинтересно! Давайте что-нибудь сломаем. Пусть нашей жертвой станет параметр verbose_name модели Trial. Откроем её код и поменяем значение этого поля с "Триал" на "Что-то другое". Прогоним тесты.

Как видим, один из тестов упал. Django говорит об этом и о том, что реальное значение поля ("Что-то другое") не равно ожидаемому ("Триал").

Миксины – полезные ребята

Тесты моделей – однотипны. Поэтому, когда у вас много сущностей, тестировать их не самая приятная рутина. Я попробовал несколько упростить данный процесс с помощью миксинов. Мой способ неидеален, и я не настаиваю на его использовании, но, возможно, он вам будет полезен.

Думаю, вы заметили, что при тестировании полей verbose_name, max_length и unique прослеживается некоторое дублирование кода. Мы получаем значение поля объекта и сравниваем его с ожидаемым. И так во всех трёх тестах. А значит, можно написать общую функцию, выполняющую эту работу.

def run_field_parameter_test(
        model, self_,
        field_and_parameter_value: dict,
        parameter_name: str) -> None:
    """Тестирует значение параметра для всех объектов модели"""

    for instance in model.objects.all():
        # Пример 1: field = "email"; expected_value = 256.
        # Пример 2: field = "email"; expected_value = "Электронная почта".
        for field, expected_value in field_and_parameter_value.items():
            parameter_real_value = getattr(
                instance._meta.get_field(field), parameter_name
            )

            self_.assertEqual(parameter_real_value, expected_value)

Разберёмся с параметрами. С model, думаю, всё понятно. self_ нужен только для вызова метода assertEqual. Поскольку self — это ключевое слово в Python, то мы добавляем к нему _, чтобы не произошло недопонимания. field_and_parameter_value — это словарь с полем и значением его параметра. Например, если мы проверяем параметр max_length, то можно в эту переменную передать email и 256. Если проверяем verbose_name, то передаём email и "Электронная почта". parameter_name — это тестируемый параметр: max_lengthverbose_name и т.д.

Теперь обратимся к коду. Вначале мы получаем все объекты модели и проходимся по ним. Дальше обходим словарь с полями и ожидаемыми значениями параметров. После получаем реальные значения параметров, обращаясь к объекту. А затем сравниваем их с ожидаемыми. Код очень похож на ранее написанный в тестах. Только теперь это всё в одной функции. Кстати, если бы её имя начиналось с префикса test, Django принял бы её за реальный тест и пытался бы выполнить вместе с остальными.

Напишем миксины. Для каждого поля должен быть свой миксин. Для примера возьмём поля verbose_name и max_length.

class TestVerboseNameMixin:
    """Миксин для проверки verbose_name"""

    def run_verbose_name_test(self, model):
        """Метод, тестирующий verbose_name"""

        run_field_parameter_test(
            model, self, self.field_and_verbose_name, 'verbose_name'
        )

class TestMaxLengthMixin:
    """Миксин для проверки max_length"""

    def run_max_length_test(self, model):
        """Метод, тестирующий max_length"""

        run_field_parameter_test(
            model, self, self.field_and_max_length, 'max_length'
        )

Мы создаём нужный метод и в нём вызываем нашу общую функцию с соответствующими параметрами. self.field_and_verbose_name и self.field_and_max_length берутся из класса, который наследуется от миксина. А именно – из метода setUpTestData класса TrialTests.

@classmethod 
def setUpTestData(cls): 
    # ...
    cls.field_and_verbose_name = {
        'email': 'Электронная почта',
    }

    cls.field_and_max_length = {
        'email': 256,
    }

Наследуем класс TrialTests от наших миксинов.

class TrialTests(TestCase, TestVerboseNameMixin, TestMaxLengthMixin):
    # ...

Если у вас будет много миксинов – их можно объединить, например, в кортеж и при наследовании распаковывать его.

MIXINS_SET = (
    TestVerboseNameMixin, TestMaxLengthMixin,
)

class TrialTests(TestCase, *MIXINS_SET):
    # ...

Теперь можно переписать наши тесты:

def test_verbose_name(self):
    """Тест параметра verbose_name"""

    super().run_verbose_name_test(Trial)

def test_max_length(self):
    """Тест параметра max_length"""

    super().run_max_length_test(Trial)

Когда у вас много тестов для разных моделей, этот способ оказывается весьма полезным.

Тестирование логики

Перейдём к тестам кода из views.py. Возьмём для примера функцию получения домена из имейла.

def get_domain(email: str) -> str:
    """Возвращает домен имейла"""

    try:
        _, domain = email.split('@')
    except ValueError:
        domain = ''

    return domain

Таким может быть её тест:

from django.test import TestCase

from my_app.views import get_domain

EMAIL_AND_DOMAIN = {
    'test1@gmail.com': 'gmail.com',
    'test2@wrong_email': 'wrong_email',
    'test3@mail.ru': 'mail.ru',
    'test4@@wrong_email.com': '',
}

class FunctionsTests(TestCase):
    """Класс с тестами функций"""

    def test_get_domain(self):
        """Тест функции get_domain"""

        for email, expected_domain in EMAIL_AND_DOMAIN.items():
            real_domain = get_domain(email)

            self.assertEqual(real_domain, expected_domain)

В константе хранятся имейлы и их реальные домены. В тесте мы по ним проходимся, получаем домен с помощью тестируемой функции и сравниваем с ожидаемым.

Теперь немного поговорим про одну полезную конструкцию. Давайте как-нибудь изменим наши имейлы. Например, исправим test1@gmail.com на test1@habr.com, а test2@wrong_email на test2@habr. Запустим тесты.

Они ожидаемо упали. Но почему мы видим, что некорректен только один имейл, хотя мы меняли два? Дело в том, что по умолчанию Django не будет продолжать тестирование, если произошёл фейл. Он просто прекратит прогон тестов, будто команда break, вызванная внутри цикла. Этот факт вряд ли может порадовать, особенно если у вас тесты такие же долгие, как вечерняя поездка по МКАД. Но, к счастью, есть решение. Нам поможет конструкция with self.subTest(). Она указывается после объявления цикла.

# ...
for email, expected_doamin in EMAIL_AND_DOMAIN.items():
    with self.subTest(f'{email=}'):
        real_domain = get_domain(email)

        self.assertEqual(real_domain, expected_doamin)

В скобках метода subTest мы указываем строку, которую хотим вывести, когда тест упадёт. В данной ситуации это проверяемый имейл.

Теперь при любом фейле Django сохранит отчёт о нём и пойдёт дальше. А после завершения прогона выведет информацию по всему, что упало.

Разберём тест ещё одной функции. При получении от пользователя промокода мы преобразовываем его в более удобный вид – убираем символы "#" и пробелы. Для этого у нас есть функция get_correct_promo:

def get_correct_promo(promo: str) -> str:
    """Возвращает промокод без # и пробелов"""

    return promo.replace('#', '').replace(' ', '')

Так может выглядеть тест для неё:

from django.test import TestCase

from my_app.views import get_correct_promo

PROMO_CODES = {
    '#sast': 'sast',
    '#beauty#': 'beauty',
    '#test test2': 'testtest2',
    'test1 test2 test3': 'test1test2test3',
}

class FunctionsTests(TestCase):
    """Класс с тестами функций"""

    def test_get_correct_promo(self):
        """Тест функции get_correct_promo"""

        for incorrect_promo, correct_promo in PROMO_CODES.items():
            real_promo = get_correct_promo(incorrect_promo)

            self.assertEqual(real_promo, correct_promo)

В константе хранятся некорректные и корректные промокоды. В тесте мы по ним проходимся, и сравниваем промокод, полученный с помощью функции get_correct_promo и действительно корректный промокод.

Пожалуй, тестирование views — самое простое из этой триады тестов. В нём мы просто вызываем нужную функцию и проверяем, что значение, которое она возвращает, совпадает с ожидаемым. Кстати, создавая константы с данными для проверки, желательно придумывать как можно больше разных значений. Так вы повысите шансы на то, что ваш тест станет более эффективным.

Тесты форм

Тесты форм похожи на тесты моделей. В них мы также можем проверять поля и методы.

Создадим форму модели Trial:

from django import forms

from my_app.models import Trial

class TrialForm(forms.ModelForm):
    """Форма модели Trial"""

    class Meta:
        model = Trial
        exclude = ()

Одним из примеров тестов для неё может быть этот код:

from django.test import TestCase

from my_app.forms import TrialForm

class TrialFormTests(TestCase):
    """Тесты формы TrialForm"""

    def test_field_labels(self):
        """Тест лейблов полей"""

        form = TrialForm()
        email_label = form.fields['email'].label

        self.assertEqual(email_label, 'Электронная почта')

В нём мы создаём объект нашей формы и сравниваем label поля с ожидаемым. Примерно так можно писать тесты для форм. Но мы их почти не используем. Для этого есть более эффективный способ, о котором я расскажу в следующей части.

Свой пользователь-тестировщик

Итак, backend-часть вашего сайта протестирована. Но вдруг на одной из его страниц вы заметили 404-ю ошибку. Написанные тесты не нашли её. Они также не помогут, например, при поиске битых ссылок на страницах. Эти тесты просто не рассчитаны на баги такого рода. Но как тогда их отлавливать? Для этого нужны тесты, имитирующие действия пользователя. Можно использовать django.test.Client, но он позволяет запускать тесты только на самом сервере сайта, а это не всегда удобно. Поэтому обратимся к Python библиотеке requests.

Данные тесты обычно получаются объёмными, поэтому их лучше вынести в отдельный файл (или файлы), например test_requests.py.

Проверка статус-кодов

Для проверки статус-кода страницы нужно:

  1. Зайти на страницу;

  2. Получить её статус-код;

  3. Проверить, что статус-код равен 200.

У библиотеки requests много полезных методов. В выполнении 1-го и 2-го пунктов нам поможет head. С его помощью мы будем отправлять HEAD-запрос на страницы сайта.

from requests import head

Нам достаточно передать в него лишь URL-адрес, чтобы получить ответ со всей нужной информацией о странице. А из этой информации можно извлечь статус-код:

response = head('<URL-адрес страницы>')
print(response.status_code)

Теперь перейдём к написанию теста. Создадим константу с тестируемыми страницами. Для простоты возьмём домен только русской версии.

DOMAIN = 'https://pvs-studio.com/ru/'

PAGES = (
    '',
    'address/',
    'pvs-studio/',
    'pvs-studio/download/',
    # ...
)

PAGES = (DOMAIN + page for page in PAGES)

Конечно, в идеале относительные пути страниц лучше брать из базы. Но если такой возможности нет – можно обойтись обычным кортежем.

Добавим класс PagesTests вместе с тестом test_status_code:

from django.test import TestCase

class PagesTests(TestCase):
    """Класс с тестами страниц"""

    def test_status_code(self):
        """Тест статус-кода"""

        for page in PAGES:
            with self.subTest(f'{page=}'):
                response = head(page) # (1)

                self.assertEqual(response.status_code, 200) # (2) и (3)

В тесте на каждую страницу мы отправляем HEAD-запрос и сохраняем ответ в переменной response. После этого проверяем, равен ли статус-код страницы 200.

Проверка ссылок на страницах

Проверить ссылки можно так:

  1. Отправить на страницу GET-запрос и получить её контент;

  2. С помощью регулярного выражения получить из контента все ссылки;

  3. Отправить по каждой ссылке HEAD-запрос и проверить, что статус-код ответа – 200.

Для поиска ссылок будем использовать метод findall батарейки re. Для отправки GET-запроса – метод get всё той же библиотеки requests. И не забудем о методе head.

from re import findall

from requests import get, head

Далее переменные. Для этого теста нам понадобится константа PAGES, объявленная ранее, и переменная с регулярным выражением для ссылки.

LINK_REGULAR_EXPRESSION = r'<a[^>]* href="([^"]*)"'

И, наконец, напишем сам тест.

def test_links(self):
    """Тест ссылок страниц"""

    valid_links = set()

    for page in PAGES:
        page_content = get(page).content # (1)
        page_links = set( # (2)
            findall(LINK_REGULAR_EXPRESSION, str(page_content))
        )

        for link in page_links:
            if link in valid_links:
                continue

            with self.subTest(f'{link=} | {page=}'):
                response = head(link, allow_redirects=True)

                if response.status_code == 200:
                    valid_links.add(link)

                self.assertEqual(response.status_code, 200) # (3)

На каждую страницу отправляем GET-запрос и из полученного ответа извлекаем контент. Далее с помощью регулярного выражения и метода findall получаем все ссылки, находящиеся на странице. Их заносим в set, чтобы убрать дубли. Последний этап – уже знакомая нам ситуация: обходим все ссылки, отправляем на них HEAD-запрос и проверяем статус-код. Если переменная link – редирект, то параметр allow_redirects укажет, что мы можем его выполнить. По умолчанию его значение – False. Также добавляем валидные ссылки в set, чтобы в дальнейшем не отправлять на них запрос.

Кстати, иногда на странице можно встретить относительные ссылки. Например, "/ru/pvs-studio/faq/". Сайт к ним подставляет URL-адрес, но тест этого не делает и, как следствие, не может выполнить запрос.

Чтобы этого избежать, создадим такую функцию:

SITE_URL = 'https://pvs-studio.com'

def get_full_link(link: str) -> str:
    """Возвращает полную ссылку (с URL-адресом сайта)"""

    if not link.startswith('http'):
        link = SITE_URL + link

    return link

Если полученная ссылка – относительная, функция добавляет к ней URL-адрес сайта. Теперь в тесте при получении ссылки мы будем использовать эту функцию:

# ...
for link in page_links:
    link = get_full_link(link)
# ...

Бывают ситуации, когда тест выдаёт не настоящий статус-код страницы. Обычно он либо 403, либо 404. Например, для этой страницы, используемый нами метод head, вернёт статус-код 404. Так происходит, потому что некоторые сайты не хотят выдавать роботам данные о странице. Чтобы этого избежать, нужно использовать метод get, а также для большей уверенности в тесте, добавить заголовок User-Agent.

from requests import get

head_response = head(link)
print(head_response.status_code) # 404

get_response = get(link, headers={'User-Agent': 'Mozilla/5.0'})
print(get_response.status_code) # 200

Тесты редиректов

Ещё один вариант тестов с помощью requests – тесты редиректов. Чтобы их проверить, нам нужно:

  1. Перейти по ссылке и получить ответ;

  2. Сравнить URL-адрес ответа с ожидаемым.

Следовательно, нам пригодятся два URL-адреса. Первый – ссылка-редирект, на которую жмёт пользователь. Второй – URL-адрес страницы, на которую посетитель в итоге перешёл. Как и в примере со статус-кодами, лучше получать эти URL-ы из базы. Если такой возможности нет, то рекомендую использовать словарь.

REDIRECT_URLS = {
    '/ru/m/0008/': '/ru/docs/',
    '/en/articles/': '/en/blog/posts/',
    '/ru/d/full/': '/ru/docs/manual/full/',
}

Не забудем про переменную SITE_URL, созданную ранее.

SITE_URL = 'https://pvs-studio.com'

И напишем сам тест.

def test_redirects(self):
    """Проверяет правильность редиректа"""

    for link, page_url in REDIRECT_URLS.items():
        with self.subTest(f'{link=} | {page_url=}'):
            page_response = head(
                SITE_URL + link, allow_redirects=True
            ) # (1)

            expected_page_url = SITE_URL + page_url

            self.assertEqual(page_response.url, expected_page_url) # (2)

Первым делом по ссылке мы отправляем HEAD-запрос. При этом разрешаем использовать редиректы. Из полученного ответа берём URL-адрес страницы и сравниваем его с ожидаемым.

Библиотека requests позволяет выполнять много разных проверок сайта. Основные методы для тестов, как вы могли заметить – head и get. Но есть и другие. Они тоже могут пригодиться. Всё зависит от ваших задач.

Заключение

Итак, теперь вы знаете, как писать тесты для бэкенда и как создать своего пользователя-тестировщика. О проверке форм, JS, переводах и прочем мы поговорим во второй части. Для фидбека или критики пишите в комментарии либо в мой инстаграм. Спасибо, что уделили время, и до скорых встреч!)

Источник: https://habr.com/ru/company/pvs-studio/blog/648717/


Интересные статьи

Интересные статьи

Продолжаем серию материалов про создание системы заметок. В этой части мы спроектируем и разработаем RESTful API Service на Golang cо Swagger и авторизацией. Будет много кода, ещё больше рефакторинга ...
Если у вас есть проект с интенсивной обработкой данных глубокими моделями (или еще нет, но вы собираетесь его создать), то вам будет полезно познакомиться с приемами по п...
В данной части статьи по разработке простейшей нейронной сети мы научимся обучать нейронные сети. Читать далее
Есть статьи о недостатках Битрикса, которые написаны программистами. Недостатки, описанные в них рядовому пользователю безразличны, ведь он не собирается ничего программировать.
Всем привет, Хабр сообщество! Хочу сегодня рассказать Вам о workflow 3D-художников, как в это вникнуть и остаться со стабильной нервной системой. Статья нацелена на новичков в данной области, опы...