Django тестирование

Django тестирование

Картинка к публикации: Django тестирование

Введение

Тестирование является неотъемлемой частью процесса разработки программного обеспечения. Особенно это актуально для веб-разработки, где приложения часто подвергаются изменениям и должны поддерживать высокий уровень доступности и безопасности. В экосистеме Django, которая ценится за свою "батарейку включена" философию, наличие встроенного инструмента для тестирования приложений является большим преимуществом. Тестирование в Django позволяет разработчикам создавать надежные и устойчивые веб-приложения, обеспечивая качество и предотвращая регрессии функционала при внесении изменений в код.

Значение тестирования в разработке на Django

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

Обзор Django Unittest Framework

Django использует стандартный модуль Python unittest в качестве основы для своей тестовой системы. unittest — это богатая библиотека, которая поддерживает автоматизацию тестов, агрегацию тестов в коллекции и независимый поиск тестов. В Django этот модуль расширен дополнительными возможностями для работы с веб-специфичными особенностями, такими как запросы, ответы, хранение данных и отображение контента.

В Django Unittest Framework включены следующие компоненты:

  • TestCase: Класс, расширяющий unittest.TestCase, предоставляет набор инструментов для создания тестов для Django-проектов, включая клиент для тестирования запросов и ответов.
  • TransactionTestCase: Этот класс предназначен для ситуаций, когда тесты требуют контроля транзакций базы данных и требуют полного отката транзакций после выполнения каждого теста.
  • LiveServerTestCase: Класс для проведения интеграционных тестов с использованием живого сервера, что позволяет имитировать взаимодействие пользователя с приложением.

Django также предоставляет множество специфических утверждений (assertions) для тестирования веб-специфичных вещей, таких как формы, представления, перенаправления и многое другое.

Через все эти инструменты Django Unittest Framework предоставляет разработчикам мощный и гибкий набор возможностей для обеспечения качества и надежности веб-приложений. В следующих разделах мы подробно рассмотрим использование классов, методов и декораторов, предоставляемых этой фреймворком, а также приведем практические примеры тестов для моделей, представлений и форм в Django.

Основы работы с Unittest в Django

Прежде чем погрузиться в написание тестов для вашего Django-приложения, важно понимать, как настроить тестовое окружение и каковы основы структуры тестового файла.

Настройка тестового окружения в Django — это простой, но важный процесс. Основные шаги включают:

Использование отдельной базы данных для тестирования: Django автоматически создает отдельную базу данных для тестов, чтобы ваши тесты не влияли на данные в вашей рабочей базе данных. Вы можете настроить это поведение через settings.py, задав параметр DATABASES.

Установка конфигурации:

  • В settings.py убедитесь, что DEBUG установлен в False для более реалистичного тестирования.
  • В случае использования кэширования установите CACHES так, чтобы в тестах использовался локальный кэш, например, LocMemCache.

Выбор правильного TestCase:

  • Если ваши тесты не требуют базы данных, используйте SimpleTestCase.
  • Для тестирования большинства функций используйте TestCase, который поддерживает транзакции базы данных.
  • TransactionTestCase следует использовать, если вам необходимо тестировать код, который зависит от транзакций базы данных.

Настройка MEDIA и STATIC директорий:

  • Для имитации правильной работы с медиа- и статическими файлами в тестах, убедитесь, что MEDIA_ROOT и STATIC_ROOT настроены на тестовые пути.

Изоляция тестов:

  • Используйте setUp и tearDown методы для создания и очистки данных перед и после каждого теста.

Стандартный тестовый файл в Django обычно содержит следующую структуру:

# tests.py

from django.test import TestCase
from .models import MyModel

class MyModelTest(TestCase):

    def setUp(self):
        # Здесь создаем инстансы объектов, которые будут использоваться в тестах.
        self.object = MyModel.objects.create(name='Test')

    def test_str_representation(self):
        # Тест, проверяющий строковое представление модели.
        self.assertEqual(str(self.object), 'Test')

    def tearDown(self):
        # Здесь удаляем объекты после теста, если это необходимо.
        # Обычно Django сам управляет базой данных тестов.
        pass

В этом примере мы импортировали TestCase из модуля django.test, определили класс MyModelTest, который наследует от TestCase, и написали методы setUp для инициализации тестовых данных и tearDown для их очистки после тестов. Методы с именем, начинающимся на test_, автоматически распознаются как тестовые методы, которые будут выполнены тестовым раннером Django.

При запуске тестов Django будет искать модули с именем tests.py в каждом приложении проекта или подмодули в пакете tests, если таковые есть. Тесты можно запустить с помощью команды python manage.py test.

Настроив тестовое окружение и поняв структуру тестового файла, вы готовы начать писать тесты, которые помогут обеспечить надежность и качество вашего Django-приложения.

Классы в Django Unittest

Тестирование в Django основано на классах, каждый из которых предоставляет различные возможности и предназначен для определенных сценариев использования. Ниже рассмотрим основные классы фреймворка unittest, расширенные в Django для специфических нужд веб-разработки.

TestCase является самым часто используемым классом при тестировании Django-приложений. Он предоставляет широкий набор инструментов для тестирования и работает с транзакционной базой данных, что означает, что каждый тест исполняется в своей транзакции, которая откатывается после завершения теста.

Пример использования TestCase:

from django.test import TestCase
from myapp.models import Animal

class AnimalTestCase(TestCase):
    def setUp(self):
        Animal.objects.create(name="lion", sound="roar")
        Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        lion = Animal.objects.get(name="lion")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

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

Пример использования SimpleTestCase:

from django.test import SimpleTestCase
from django.urls import reverse

class SimpleTests(SimpleTestCase):
    def test_home_page_status_code(self):
        response = self.client.get(reverse('home'))
        self.assertEqual(response.status_code, 200)

TransactionTestCase полезен, когда нужно тестировать код, который взаимодействует с транзакциями базы данных. В отличие от TestCase, TransactionTestCase не использует транзакционную базу данных, что означает, что изменения, внесенные в базу данных во время теста, не откатываются автоматически.

Пример использования TransactionTestCase:

from django.test import TransactionTestCase
from myapp.models import Widget

class WidgetTransactionTestCase(TransactionTestCase):
    def test_widget_creation(self):
        Widget.objects.create(name='sprocket')
        self.assertEqual(Widget.objects.count(), 1)

LiveServerTestCase используется для интеграционных тестов, когда нужно запустить тесты, имитирующие реальные HTTP запросы к живому серверу. Это особенно полезно для тестов с использованием инструментов для автоматизации браузера, таких как Selenium.

Пример использования LiveServerTestCase:

from django.test import LiveServerTestCase
from selenium.webdriver.firefox.webdriver import WebDriver

class MySeleniumTests(LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = WebDriver()

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

    def test_login(self):
        self.selenium.get(f'{self.live_server_url}/login/')
        username_input = self.selenium.find_element_by_name("username")
        username_input.send_keys('myuser')
        password_input = self.selenium.find_element_by_name("password")
        password_input.send_keys('secret')
        self.selenium.find_element_by_id('submit').click()
        # ... more assertions ...

Каждый из этих классов имеет свое назначение и обеспечивает эффективное тестирование в различных

Расширенное использование TestCase

Класс TestCase в Django предлагает разнообразные методы для подготовки тестового окружения и проведения всестороннего тестирования компонентов вашего приложения. Далее мы рассмотрим методы setUp, setUpClass, tearDown, и tearDownClass, а также применение TestCase для тестирования моделей, представлений (views) и форм.

setUp(self)

Этот метод вызывается перед каждым тестовым методом. Используется для настройки объектов, которые будут использоваться во многих тестах (хотя каждый тест получит свежий набор объектов). Это обеспечивает изоляцию между тестами, так как изменения, сделанные в одном тесте, не повлияют на другие.

tearDown(self)

Этот метод вызывается после каждого тестового метода. Он отвечает за очистку после теста. Любые ресурсы, выделенные в setUp, должны быть освобождены здесь, чтобы избежать утечек ресурсов.

Пример использования setUp и tearDown:

from django.test import TestCase
from myapp.models import MyModel

class MyModelTestCase(TestCase):
    def setUp(self):
        self.my_model = MyModel.objects.create(name='Test Name')

    def test_model_str(self):
        self.assertEqual(str(self.my_model), 'Test Name')

    def tearDown(self):
        # Так как Django по умолчанию откатывает транзакции после каждого теста,
        # явное удаление объектов часто не требуется.
        pass

setUpClass(cls)

Этот метод вызывается перед запуском всех тестов в тестовом классе. Обычно используется для выполнения дорогостоящих операций, которые необходимо сделать один раз для всех тестов в классе. Он должен быть декорирован как @classmethod.

tearDownClass(cls)

Этот метод вызывается после выполнения всех тестов в классе и служит для очистки ресурсов, выделенных в setUpClass. Это может включать закрытие соединений с базой данных или удаление файлов, созданных для тестов. Как и setUpClass, этот метод должен быть декорирован как @classmethod.

Пример использования setUpClass и tearDownClass:

from django.test import TestCase

class MyTestCase(TestCase):
    @classmethod
    def setUpClass(cls):
        super(MyTestCase, cls).setUpClass()
        # Здесь можно настроить объекты, общие для всех тестов.

    @classmethod
    def tearDownClass(cls):
        # Очистка после выполнения всех тестов в классе.
        super(MyTestCase, cls).tearDownClass()

    def test_one(self):
        # Первый тестовый метод.
        pass

    def test_two(self):
        # Второй тестовый метод.
        pass

Последовательность вызовов

Когда вы запускаете тесты, последовательность вызова методов будет следующей:

  1. setUpClass (один раз для класса)
  2. setUp (перед каждым тестовым методом)
  3. Тестовый метод (например, test_something)
  4. tearDown (после каждого тестового метода)
  5. Повторение шагов 2-4 для каждого тестового метода в классе
  6. tearDownClass (один раз после выполнения всех тестов в классе)

Такая структура гарантирует, что каждый тест изолирован и имеет одинаковые условия при старте, в то же время позволяя избежать лишних настроек и очисток, что может значительно сократить общее время выполнения тестов при большом их количестве.

Примеры тестирования

Тестирование моделей в Django обычно включает проверку методов модели, валидаторов и интеграцию с базой данных.

Пример теста для модели

from django.test import TestCase
from myapp.models import MyModel

class MyModelTest(TestCase):
    def test_default_values(self):
        my_model_instance = MyModel()
        self.assertEqual(my_model_instance.my_field, 'Default Value')

    def test_custom_method(self):
        my_model_instance = MyModel(my_field='Value')
        self.assertEqual(my_model_instance.get_my_field_display(), 'Display Value')

Тестирование представлений (views) часто включает проверку кода состояния ответа, шаблонов, используемых в ответе, и контекста, переданного в шаблон.

Пример теста для представления

from django.urls import reverse
from django.test import TestCase

class ViewTest(TestCase):
    def test_view_status_code(self):
        url = reverse('my_view')
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

    def test_view_template_used(self):
        url = reverse('my_view')
        response = self.client.get(url)
        self.assertTemplateUsed(response, 'my_template.html')

При тестировании форм важно проверить корректность их валидации и поведение при передаче валидных и невалидных данных.

Пример теста для формы

from django.test import TestCase
from myapp.forms import MyForm

class FormTest(TestCase):
    def test_form_valid(self):
        form_data = {'field_one': 'value1', 'field_two': 'value2'}
        form = MyForm(data=form_data)
        self.assertTrue(form.is_valid())

    def test_form_invalid(self):
        form_data = {'field_one': '', 'field_two': 'value2'}
        form = MyForm(data=form_data)
        self.assertFalse(form.is_valid())

Декораторы в Unittest Django

Декораторы в библиотеке unittest Django предоставляют удобные инструменты для контроля над выполнением тестов. Они позволяют легко пропускать тесты, отмечать тесты как ожидаемо падающие или выполнение тестов в зависимости от определенных условий. Ниже представлены основные декораторы, используемые для этих целей.

@skip

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

from unittest import skip
from django.test import TestCase

class MyTests(TestCase):
    @skip('demonstrating skipping')
    def test_nothing(self):
        self.fail('should not happen')

    def test_something(self):
        pass

@skipIf

позволяет пропустить тест, если условие истинно. Это может быть полезно для пропуска тестов, которые зависят от внешнего окружения или конфигурации, которая может не быть доступна в определенное время.

from unittest import skipIf
from django.test import TestCase
import sys

class MyTests(TestCase):
    @skipIf(sys.version_info < (3, 8), "requires python3.8 or higher")
    def test_python_version(self):
        pass

@skipUnless

обратный @skipIf, @skipUnless декоратор пропускает тест, если условие ложно. Он используется для проверки наличия необходимых условий перед выполнением теста.

from unittest import skipUnless
from django.test import TestCase

class MyTests(TestCase):
    @skipUnless(condition=lambda: True, reason="requires condition to be True")
    def test_condition(self):
        pass

@expectedFailure

применяется к тестам, которые известны как падающие. Тесты, помеченные как ожидаемо неудачные, не считаются неудачными в тестовом прогоне, если они действительно падают. Однако, если тест проходит успешно, то он помечается как неудачный, потому что ожидалась неудача.

from unittest import expectedFailure
from django.test import TestCase

class MyTests(TestCase):
    @expectedFailure
    def test_fail(self):
        self.assertEqual(1, 0, "broken")

@unittest.mock.patch(target, new)

позволяет заменить целевой объект в указанном модуле или классе на мок-объект во время выполнения декорированного теста.

import unittest
from unittest.mock import patch
import my_module

class TestExternalAPICall(unittest.TestCase):
    @patch('my_module.external_api_call')
    def test_external_api_call(self, mock_api_call):
        # Configure the mock to return a specific response
        mock_api_call.return_value = "mock response"
        
        # Call the function that we're testing, which will use the mock
        response = my_module.external_api_call()
        
        # Assert that the mock was called as expected
        mock_api_call.assert_called_once()
        
        # Assert that the response was as expected
        self.assertEqual(response, "mock response")

В этом примере мы мокируем функцию external_api_call в модуле my_module. Все вызовы этой функции внутри test_external_api_call теперь будут использовать мок, который мы настроили так, чтобы он возвращал "mock response".

@unittest.mock.patch.object(target, attribute, new)

заменяет указанный атрибут целевого объекта на мок во время теста.

import unittest
from unittest.mock import patch
import my_module

class TestSomeClassMethod(unittest.TestCase):
    @patch.object(my_module.SomeClass, 'method')
    def test_method(self, mock_method):
        # Configure the mock to return a specific response
        mock_method.return_value = "mocked method"
        
        # Instantiate the object we're testing
        some_object = my_module.SomeClass()
        
        # Call the method that we're testing, which will use the mock
        response = some_object.method()
        
        # Assert that the mock was called as expected
        mock_method.assert_called_once()
        
        # Assert that the response was as expected
        self.assertEqual(response, "mocked method")

В этом примере мы мокируем метод method класса SomeClass. Мы создаём экземпляр класса SomeClass и вызываем его метод method, который теперь будет использовать настроенный мок, возвращающий "mocked method".

@data(*values) и @unpack (из модуля ddt — Data-Driven Tests)

позволяют параметризировать тесты, передавая различные наборы данных в один и тот же тест.

pip install ddt
import unittest
from ddt import ddt, data, unpack

@ddt
class MyTestCase(unittest.TestCase):
    @data((3, 2), (4, 3), (5, 3))
    @unpack
    def test_addition(self, first, second):
        result = first + second
        self.assertEqual(result, first + second)

В приведенном выше примере @data предоставляет несколько наборов данных для теста, а @unpack разбивает каждый набор данных на отдельные аргументы, которые передаются в метод теста.

@override_settings(target=value)

предназначен для временной замены или добавления значения в настройки Django во время выполнения тестов. Когда вы используете @override_settings, изменения настроек применяются непосредственно перед выполнением теста и отменяются сразу после его завершения. Это означает, что изменения не повлияют на другие тесты в вашем тестовом наборе, что позволяет избежать побочных эффектов. Это полезно, когда вам нужно изменить настройки только для одного теста или группы тестов, без изменения глобальных настроек проекта.

from django.test import TestCase
from django.conf import settings
from django.test.utils import override_settings

class MyTestCase(TestCase):    
    # Использование декоратора на уровне метода
    @override_settings(PAYMENT_GATEWAY='Dummy')
    def test_payment_gateway(self):
        # PAYMENT_GATEWAY будет 'Dummy' только в этом тесте
        self.assertEqual(settings.PAYMENT_GATEWAY, 'Dummy')

    # Использование декоратора на уровне класса, применится ко всем тестам в классе
    @override_settings(DEBUG=True)
    def test_something_else(self):
        # DEBUG будет True только в этом тесте
        self.assertTrue(settings.DEBUG)


# Использование как контекстного менеджера внутри метода
class AnotherTestCase(TestCase):    
    def test_something_important(self):
        with override_settings(DEBUG=False):
            # DEBUG будет False только в этом блоке
            self.assertFalse(settings.DEBUG)


# Можно также переопределить несколько настроек одновременно
@override_settings(DEBUG=True, EMAIL_BACKEND='django.core.mail.backends.console.EmailBackend')
class MyOtherTestCase(TestCase):    
    def test_debug_mode(self):
        # DEBUG будет True и EMAIL_BACKEND будет переопределен только для тестов в этом классе
        self.assertTrue(settings.DEBUG)
        self.assertEqual(settings.EMAIL_BACKEND, 'django.core.mail.backends.console.EmailBackend')

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

Ассерты в Django Unittest

Ассерты являются основным инструментом для проверки предполагаемых условий в тестах. Django Unittest расширяет стандартные ассерты, предоставляемые unittest модулем Python, добавляя специфичные для фреймворка проверки. Ниже описаны наиболее распространенные ассерты и те, что характерны для Django.

Стандартные ассерты

  • assertEqual(a, b): Проверяет равенство a и b.
  • assertTrue(x): Проверяет, что x истинно.
  • assertFalse(x): Проверяет, что x ложно.
  • assertIs(a, b): Проверяет, что a и b это один и тот же объект.
  • assertIsNone(x): Проверяет, что x это None.
  • assertIn(a, b): Проверяет, что a находится в b.
  • assertRaises(Exception): Проверяет, что следующий блок кода вызывает исключение Exception.

Специфичные для Django ассерты

  • assertTemplateUsed(response, template_name): Проверяет, был ли использован указанный шаблон при формировании ответа.
  • assertRedirects(response, expected_url, status_code=302, target_status_code=200): Проверяет, что ответ содержит перенаправление на указанный URL и что последовательность перенаправления проходит как ожидалось, с соответствующими кодами состояния.
  • assertFormError(response, form, field, errors): Проверяет наличие определенных ошибок формы в ответе.
  • assertContains(response, text, count=None, status_code=200): Проверяет, содержит ли ответ text указанное количество раз (count), и проверяет код ответа.
  • assertNotContains(response, text, status_code=200): Проверяет, что текст не содержится в ответе и проверяет код ответа.
  • assertQuerysetEqual(qs, values): Проверяет, что QuerySet (qs) равен списку значений (values).
from django.test import TestCase
from django.urls import reverse
from .models import MyModel

class MyModelTestCase(TestCase):
    
    def test_model_str(self):
        my_model_instance = MyModel.objects.create(name='Test')
        self.assertEqual(str(my_model_instance), 'Test')

    def test_homepage(self):
        response = self.client.get(reverse('home'))
        self.assertTemplateUsed(response, 'home.html')
        self.assertContains(response, 'Welcome to the homepage', status_code=200)

    def test_redirect(self):
        response = self.client.get('/redirect_url/')
        self.assertRedirects(response, '/expected_url/', status_code=302, target_status_code=200)
    
    def test_form_errors(self):
        response = self.client.post('/my_form/', {})
        self.assertFormError(response, 'form', 'my_field', 'This field is required.')

Дополнительные ассерты Django

  • assertNumQueries(num, func, *args, **kwargs): Проверяет, что при вызове функции func выполнено ровно num SQL-запросов.
  • assertQuerysetEqual(qs, values, transform=repr, ordered=True): Проверяет, что список объектов, полученных из QuerySet qs, после применения transform-функции, соответствует списку values. Параметр ordered определяет, должен ли порядок объектов совпадать.
  • assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value=''): Проверяет поведение поля формы для различных входных данных. valid и invalid — словари, ключами которых являются тестовые данные, а значениями — ожидаемые результаты.
  • assertJSONEqual(raw, expected_data): Проверяет, что строка raw является JSON-строкой, которая десериализуется в объект (чаще всего словарь), эквивалентный expected_data.
  • assertHTMLEqual(html1, html2): Сравнивает две HTML-строки и проверяет их эквивалентность с точки зрения HTML-содержимого.
  • assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs): Проверяет, что сообщение об исключении, вызванное callable, соответствует ожидаемому сообщению.
  • assertWarns(expected_warning, callable, *args, **kwargs): Проверяет, что предупреждение типа expected_warning вызывается при вызове callable.
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.forms import CharField
from django.db import connection

class AdvancedAssertsTestCase(TestCase):

    def test_assert_num_queries(self):
        with self.assertNumQueries(3):
            MyModel.objects.create(name='Test1')
            MyModel.objects.create(name='Test2')
            MyModel.objects.all().delete()

    def test_assert_field_output(self):
        self.assertFieldOutput(CharField, {'a': 'a'}, {'': ValidationError('This field cannot be blank.')})

    def test_assert_json_equal(self):
        response = self.client.get('/my_json_view/')
        self.assertJSONEqual(response.content, {'key': 'value'})

    def test_assert_raises_message(self):
        with self.assertRaisesMessage(ValueError, "Invalid value"):
            raise ValueError("Invalid value")

    def test_assert_warns(self):
        with self.assertWarns(DeprecationWarning):
            # Function that triggers a deprecation warning
            deprecated_function()

Использование ассертов является критически важным для подтверждения того, что ваше приложение работает должным образом. Они помогают обеспечить, что код соответствует ожидаемым критериям и что изменения в коде не нарушают существующую функциональность.

Интеграция с Continuous Integration

Continuous Integration (CI) является важной частью современной разработки программного обеспечения. Она позволяет автоматически выполнять тесты и другие важные скрипты каждый раз, когда происходят изменения в кодовой базе проекта. Ниже приведен пример того, как вы можете настроить запуск Django тестов с использованием GitHub Actions.

Шаг 1: Создайте файл workflow в вашем репозитории

В корне вашего репозитория GitHub создайте каталог .github/workflows/. В этом каталоге создайте файл, например django.yml. Это будет конфигурационный файл для GitHub Actions.

Шаг 2: Напишите конфигурацию для GitHub Actions

Вот пример содержимого для django.yml, который установит Python, установит зависимости и запустит тесты Django.

name: Django CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:12
        env:
          POSTGRES_DB: mydb
          POSTGRES_USER: user
          POSTGRES_PASSWORD: password
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
    - uses: actions/checkout@v2

    - name: Set up Python 3.8
      uses: actions/setup-python@v2
      with:
        python-version: 3.8

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 pytest
        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

    - name: Run migrations
      run: python manage.py migrate

    - name: Run tests
      run: python manage.py test

    - name: Flake8
      run: flake8 .
      continue-on-error: true

Объяснение конфигурации:

  • on: - Определяет, когда CI должен запускаться. В данном случае при push и pull_request в ветку main.
  • jobs: - Здесь мы определяем задачи, которые должны выполняться.
  • runs-on: - Здесь мы указываем, на какой системе должен запускаться наш CI (в данном случае это последняя версия Ubuntu).
  • services: - Если ваш Django проект использует базу данных (как PostgreSQL в примере), вы можете настроить службу контейнера.
  • steps: - Это различные шаги, которые GitHub будет выполнять. Это включает в себя такие действия, как проверка вашего репозитория, установка Python, установка зависимостей, применение миграций и запуск тестов.

Шаг 3: Добавьте файлы в репозиторий и запустите CI

Добавьте файл .github/workflows/django.yml в ваш репозиторий и сделайте commit и push в ветку main. GitHub Actions автоматически определит новую конфигурацию и начнет процесс CI.

Шаг 4: Мониторинг выполнения тестов

Вы можете следить за выполнением тестов в реальном времени, перейдя в раздел "Actions" вашего репозитория на GitHub.

Теперь каждый раз, когда вы будете делать push в ваш репозиторий или создавать pull request, GitHub Actions будет автоматически запускать ваш конфигурационный файл CI и выполнять определенные в нем задачи.

Полезные практики по тестированию

Тестирование — это критически важный аспект разработки программного обеспечения, который помогает убедиться, что ваш код работает как ожидается и предотвращает регрессии при изменении кода. Вот несколько полезных практик и советов по тестированию в Django, включая использование mocking и Factory Boy, а также стратегии отладки тестов.

Mocking в тестах

Mocking позволяет имитировать поведение классов или функций в ваших тестах, так что вы можете тестировать одну часть системы изолированно от других. Это особенно полезно для:

  • Тестирования взаимодействия с внешними сервисами (например, API)
  • Изоляции тестов от побочных эффектов и состояния
  • Ускорения тестов за счет избегания доступа к медленным ресурсам (например, базе данных)

В Python для mocking можно использовать модуль unittest.mock. Вот пример использования mocking в тестах Django:

from django.test import TestCase
from unittest.mock import patch
from myapp.views import external_api_view

class MyTestCase(TestCase):
    @patch('myapp.views.requests.get')  # Предположим, что requests.get используется в представлении для обращения к внешнему API
    def test_external_api(self, mock_get):
        # Настраиваем mock объект
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {'key': 'value'}

        response = external_api_view()
        
        # Проверяем, был ли вызван requests.get с определенным URL
        mock_get.assert_called_with('http://external.api/some/endpoint')

        # Проверяем ответ представления
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {'key': 'value'})

Использование фабрик объектов с Factory Boy

Factory Boy — это библиотека для Python, которая предоставляет фабрики для создания тестовых данных. Она может быть особенно полезна в тестах Django для создания экземпляров моделей:

import factory
from myapp.models import MyModel

class MyModelFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = MyModel

    field1 = factory.Sequence(lambda n: 'field%d' % n)
    field2 = factory.Faker('name')

# Использование в тесте
class MyModelTest(TestCase):
    def test_something_with_model(self):
        # Создаем объект с помощью фабрики
        my_model_instance = MyModelFactory()

        # Теперь можно использовать my_model_instance в тестах
        ...

Отладка тестов

Отладка тестов может быть сложной, особенно если они взаимодействуют с базой данных или другими компонентами системы. Вот несколько советов для отладки:

  • Используйте print или логгирование, чтобы получить информацию о состоянии теста в момент его выполнения.
  • В случае сложных тестов, запускайте один тест за раз используя python manage.py test myapp.tests.MyTestCase.test_my_test.
  • Если используете PyCharm или другие IDE, используйте встроенные отладчики для пошагового выполнения кода.
  • Используйте pdb или ipdb для Python, чтобы вставить точки останова в код и вручную исследовать состояние приложения:
  • При необходимости очистите тестовую базу данных или используйте --keepdb флаг для manage.py test, чтобы сохранить тестовую базу данных между запусками.

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

import pdb; pdb.set_trace()

Часто задаваемые вопросы

Вот некоторые часто задаваемые вопросы (FAQ) по использованию модуля unittest в Django, включая обработку распространенных ошибок и исключений:

Как мне создать тестовую базу данных для тестов?

Django автоматически создает отдельную тестовую базу данных при запуске тестов. Вы можете управлять этим поведением через параметры DATABASES в settings.py.

Почему мои тесты медленные и как их ускорить?

Тесты могут замедляться из-за многих факторов, включая сложные запросы к базе данных и создание большого количества объектов. Для ускорения тестов:

  • Используйте setUpTestData вместо setUp, чтобы данные создавались один раз для всего класса TestCase, а не перед каждым тестом.
  • Используйте mocking для изоляции тестов от медленных операций ввода-вывода.
  • Рассмотрите возможность использования SimpleTestCase или TransactionTestCase, если это возможно.

Что делать, если мои тесты проходят локально, но не проходят в CI?

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

Как мне имитировать/заменить методы модели в моих тестах?

Вы можете использовать unittest.mock.patch() для имитации методов:

from unittest.mock import patch

class MyTestCase(TestCase):
    @patch('myapp.models.MyModel.my_method')
    def test_my_method(self, mock_my_method):
        # Устанавливаем возвращаемое значение
        mock_my_method.return_value = 'fake_value'
        # Теперь вызов my_method вернет 'fake_value'
        ...

Как протестировать код, который обрабатывает исключения?

Вы можете использовать assertRaises для проверки, что определенное исключение было вызвано:

with self.assertRaises(ValueError):
    function_that_raises_value_error()

Что делать, если тесты зависят от данных, которые постоянно меняются?

Используйте фикстуры для загрузки стабильных тестовых данных или фабрики (например, Factory Boy) для генерации детерминированных тестовых данных.

Как мне проверить, что Django view возвращает правильный статус код?

Используйте клиент тестов Django для имитации запросов и проверки ответов:

response = self.client.get('/my-url/')
self.assertEqual(response.status_code, 200)

Почему Django не видит мои тестовые файлы?

Убедитесь, что ваши тестовые классы наследуются от django.test.TestCase и что их имена начинаются с test.

Как мне отключить логирование во время тестов?

Вы можете настроить логгирование в вашем settings.py, чтобы отключить его во время тестирования, или использовать unittest.mock.patch для имитации логгера.

Как протестировать функцию, которая вызывает django.core.mail.send_mail?

Используйте unittest.mock.patch для имитации send_mail и проверьте, что она была вызвана с правильными аргументами.

Важно помнить:

  • Тестируйте не только позитивные, но и негативные сценарии.
  • Следите за тем, чтобы тесты были независимыми друг от друга.
  • Используйте setUp и tearDown методы для подготовки и очистки после тестов.
  • Практикуйте Test-Driven Development (TDD), чтобы сначала написать тесты, а затем код.
  • Обеспечьте достаточное покрытие кода тестами.

Список ресурсов:

  • Официальная документация Django по тестированию: Основной источник информации о тестировании в Django, включая примеры.
  • Factory Boy: Документация по использованию Factory Boy для создания тестовых данных.
  • Coverage.py: Инструмент для измерения покрытия кода тестами.
  • Mock: Документация по модулю unittest.mock в стандартной библиотеке Python.

Дополнительные материалы и ссылки на документацию Django:

  • Django REST framework testing: Если вы работаете с Django REST framework, эти тесты будут особенно полезны.
  • Advanced testing topics: Раздел документации Django с продвинутыми темами тестирования.
  • Continuous Integration with Django: Советы по настройке непрерывной интеграции для Django проектов.

Использование этих ресурсов и следование лучшим практикам поможет вам писать эффективные и надежные тесты для ваших проектов на Django.


Читайте также:

ChatGPT
Eva
💫 Eva assistant