Мне очень нравятся предложения от @Hassek, и я хочу подчеркнуть, какое замечательное замечание он делает по поводу очевидного отсутствия стандартных практик, что верно для многих аспектов Django, а не только для тестирования, поскольку все мы подходим к фреймворку с разными проблемами. Имея в виду, а также добавляя к этому большую степень гибкости, которую мы имеем при разработке наших приложений, мы часто получаем совершенно разные решения, применимые к одной и той же проблеме.
Тем не менее, большинство из нас по-прежнему стремятся к одним и тем же целям при тестировании наших приложений, в основном:
- Аккуратная организация наших тестовых модулей
- Создание повторно используемых утверждений и вспомогательных методов, вспомогательных функций, которые уменьшают LOC для тестовых методов, чтобы сделать их более компактными и читабельными.
- Демонстрация очевидного систематического подхода к тестированию компонентов приложения.
Как и @Hassek, это мои предпочтения, которые могут напрямую противоречить практикам, которые вы можете применять, но я чувствую, что приятно поделиться тем, что мы доказали, что они работают, хотя бы только в нашем случае.
Нет приспособлений для тестовых случаев
Приспособления приложения отлично работают, если у вас есть определенные постоянные данные модели, которые вы хотели бы гарантировать присутствие в базе данных, скажем, набор городов с их названиями и номерами почтовых отделений.
Однако я считаю это негибким решением для предоставления данных тестового примера. Тестовые фикстуры очень многословны, мутации модели заставляют вас либо пройти длительный процесс воспроизведения данных фикстуры, либо выполнять утомительные ручные изменения, а поддержание ссылочной целостности сложно выполнить вручную.
Кроме того, вы, скорее всего, будете использовать множество видов фикстур в своих тестах, а не только для моделей: вы хотели бы хранить тело ответа от запросов API, создавать фикстуры, предназначенные для серверных частей базы данных NoSQL, писать имеющиеся фикстуры, которые используются для заполнения данных формы и т. д.
В конце концов, использование API для создания данных является кратким, удобочитаемым и значительно упрощает определение отношений, поэтому большинство из нас прибегают к использованию фабрик для динамического создания фикстур.
Широко использовать фабрики
Фабричные функции и методы предпочтительнее, чем вытаптывание ваших тестовых данных. Вы можете создавать вспомогательные фабричные функции уровня модуля или методы тестового примера, которые вы можете повторно использовать в тестах приложения или во всем проекте. В частности, factory_boy
, о котором упоминает @Hassek, предоставляет вам возможность наследовать/расширять данные приборов и выполнять автоматическую последовательность, которая может выглядеть немного неуклюже, если бы вы сделали это вручную в противном случае.
Конечная цель использования фабрик — сокращение дублирования кода и оптимизация создания тестовых данных. Я не могу дать вам точные показатели, но я уверен, что если вы внимательно изучите свои методы тестирования, вы заметите, что большая часть вашего тестового кода в основном подготавливает данные, которые вам потребуются для запуска ваших тестов.
Когда это делается неправильно, чтение и сопровождение тестов становится утомительной деятельностью. Это имеет тенденцию обостряться, когда мутации данных приводят к не столь очевидным сбоям тестов по всем направлениям, и в этот момент вы не сможете применять систематические усилия по рефакторингу.
Мой личный подход к этой проблеме заключается в том, чтобы начать с модуля myproject.factory
, который создает легкодоступные ссылки на методы QuerySet.create
для моих моделей, а также для любых объектов, которые я мог бы регулярно использовать в большинстве своих тестов приложений:
from django.contrib.auth.models import User, AnonymousUser
from django.test import RequestFactory
from myproject.cars.models import Manufacturer, Car
from myproject.stores.models import Store
create_user = User.objects.create_user
create_manufacturer = Manufacturer.objects.create
create_car = Car.objects.create
create_store = Store.objects.create
_factory = RequestFactory()
def get(path='/', data={}, user=AnonymousUser(), **extra):
request = _factory.get(path, data, **extra)
request.user = user
return request
def post(path='/', data={}, user=AnonymousUser(), **extra):
request = _factory.post(path, data, **extra)
request.user = user
return request
Это, в свою очередь, позволяет мне сделать что-то вроде этого:
from myproject import factory as f # Terse alias
# A verbose, albeit readable approach to creating instances
manufacturer = f.create_manufacturer(name='Foomobiles')
car1 = f.create_car(manufacturer=manufacturer, name='Foo')
car2 = f.create_car(manufacturer=manufacturer, name='Bar')
# Reduce the crud for creating some common objects
manufacturer = f.create_manufacturer(name='Foomobiles')
data = {name: 'Foo', manufacturer: manufacturer.id)
request = f.post(data=data)
view = CarCreateView()
response = view.post(request)
Большинство людей серьезно относятся к сокращению дублирования кода, но я на самом деле намеренно добавляю некоторые из них всякий раз, когда чувствую, что это способствует полноте тестирования. Опять же, цель любого подхода к фабрикам состоит в том, чтобы свести к минимуму количество мозгов, которые вы вносите в заголовок каждого метода тестирования.
Используйте насмешки, но используйте их с умом
Я поклонник mock
, так как мне понравилось решение автора того, во что я верю. была проблема, которую он хотел решить. Инструменты, предоставляемые пакетом, позволяют формировать тестовые утверждения путем внедрения ожидаемых результатов.
# Creating mocks to simplify tests
factory = RequestFactory()
request = factory.get()
request.user = Mock(is_authenticated=lamda: True) # A mock of an authenticated user
view = DispatchForAuthenticatedOnlyView().as_view()
response = view(request)
# Patching objects to return expected data
@patch.object(CurrencyApi, 'get_currency_list', return_value="{'foo': 1.00, 'bar': 15.00}")
def test_converts_between_two_currencies(self, currency_list_mock):
converter = Converter() # Uses CurrencyApi under the hood
result = converter.convert(from='bar', to='foo', ammount=45)
self.assertEqual(4, result)
Как видите, макеты действительно полезны, но у них есть неприятный побочный эффект: ваши макеты ясно показывают ваши предположения о том, как ведет себя ваше приложение, что приводит к связанности. Если Converter
подвергся рефакторингу для использования чего-то другого, кроме CurrencyApi
, кто-то может не понять, почему метод тестирования внезапно дает сбой.
Таким образом, с большой силой приходит большая ответственность — если вы собираетесь быть умником и использовать макеты, чтобы избежать глубоко укоренившихся тестовых препятствий, вы можете полностью запутать истинную природу своих тестовых неудач.
Прежде всего, будьте последовательны. Очень очень последовательно
Это самый важный момент, который нужно сделать. Будьте последовательны абсолютно во всем:
- как вы организуете код в каждом из ваших тестовых модулей
- как вы вводите тестовые примеры для компонентов вашего приложения
- как вы вводите методы тестирования для подтверждения поведения этих компонентов
- как вы структурируете методы тестирования
- как вы подходите к тестированию общих компонентов (представлений на основе классов, моделей, форм и т. д.)
- как вы применяете повторное использование
В большинстве проектов часто упускается из виду то, как вы будете совместно подходить к тестированию. В то время как сам код приложения выглядит идеально — соблюдение руководств по стилю, использование идиом Python, повторное применение собственного подхода Django к решению связанных проблем, использование компонентов фреймворка по учебнику и т. д. — никто не прилагает усилий, чтобы понять, как это сделать. превратить тестовый код в действенный, полезный инструмент коммуникации, и будет обидно, если, возможно, все, что нужно, это иметь четкие рекомендации по тестовому коду.
person
Filip Dupanović
schedule
07.08.2012