我真的很喜欢@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)
正如您所看到的,mock 确实很有帮助,但它们有一个令人讨厌的副作用:您的 mock 清楚地显示了您对应用程序行为方式的假设,这引入了耦合。如果Converter
被重构为使用CurrencyApi
.
因此,强大的力量伴随着巨大的责任——如果您要成为一个聪明人并使用模拟来避免根深蒂固的测试障碍,您可能会完全混淆测试失败的真实性质。
最重要的是,保持一致。非常非常一致
这是最重要的一点。与所有内容保持一致:
- 您如何在每个测试模块中组织代码
- 如何为应用程序组件引入测试用例
- 你如何引入测试方法来断言这些组件的行为
- 你如何构建测试方法
- 您如何测试通用组件(基于类的视图、模型、表单等)
- 你如何应用重用
对于大多数项目来说,关于如何以协作方式进行测试的问题经常被忽视。虽然应用程序代码本身看起来很完美——遵守风格指南、使用 Python 习惯用法、重新应用 Django 自己的方法来解决相关问题、使用框架组件的教科书等——但没有人真正努力弄清楚如何将测试代码变成有效的、有用的通信工具,如果只需要为测试代码制定明确的指导方针,那就太可惜了。