我真的很喜欢@Hassek 的建议,并想强调一下他对明显缺乏标准实践的观点是多么的好,这适用于 Django 的许多方面,而不仅仅是测试,因为我们所有人都在使用框架考虑到不同的关注点,此外还增加了我们在设计应用程序时所具有的高度灵活性,我们经常会得到适用于同一问题的截然不同的解决方案。
话虽如此,但我们大多数人在测试我们的应用程序时仍然为许多相同的目标而努力,主要是:
- 让我们的测试模块井井有条
- 创建可重用的断言和辅助方法、辅助函数来减少测试方法的 LOC,以使它们更加紧凑和可读
- 表明有一种明显的系统方法来测试应用程序组件
像@Hassek 一样,这些是我的偏好,可能与您可能正在应用的做法直接冲突,但我觉得很高兴分享我们已经证明有效的东西,如果只是在我们的案例中。
没有测试用例夹具
应用程序夹具工作得很好,如果您有某些恒定的模型数据,您希望保证出现在数据库中,例如一组城镇及其名称和邮局号码。
但是,我认为这是提供测试用例数据的一种不灵活的解决方案。测试夹具非常冗长,模型突变迫使您要么经历一个漫长的过程来重现夹具数据,要么执行繁琐的手动更改,并且很难手动执行保持参照完整性。
此外,您很可能会在测试中使用多种固定装置,而不仅仅是模型:您希望存储来自 API 请求的响应主体、创建以 NoSQL 数据库后端为目标的固定装置、编写固定装置用于填充表单数据等。
最后,使用 API 来创建数据简洁易读,并且更容易发现关系,因此我们大多数人都求助于使用工厂来动态创建夹具。
大量使用工厂
工厂函数和方法比删除测试数据更可取。您可以创建您可能想要重用的辅助工厂模块级函数或测试用例方法
跨应用程序测试或整个项目。特别是,@Hassek 提到的factory_boy 为您提供了继承/扩展夹具数据并进行自动排序的能力,如果您手动执行,这可能看起来有点笨拙。
利用工厂的最终目标是减少代码重复并简化创建测试数据的方式。我不能给你确切的指标,但我敢肯定,如果你以敏锐的眼光审视你的测试方法,你会注意到你的大部分测试代码主要是准备驱动测试所需的数据。
如果做得不正确,阅读和维护测试会变得很累人。当数据突变导致全面的测试失败不那么明显时,这种情况往往会升级,此时您将无法应用系统的重构工作。
我个人解决此问题的方法是从 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 自己的方法来解决相关问题、使用框架组件的教科书等等——但没有人真正努力弄清楚如何将测试代码变成有效、有用的交流工具,如果只需要为测试代码制定明确的指导方针,那就太可惜了。