在Django中testing“不同层次”的最佳实践是什么?

不是新来的testing,但是对于在Django中testing不同层次的build议混乱感到困惑。

一些build议(他们是对的),以避免在模型中的Doctests ,因为他们是不可维护的…

其他人说不要使用灯具 ,因为它比辅助function更不灵活,例如..

还有两个人为了使用Mock对象而战斗。 第一组相信使用模拟和隔离系统的其余部分,而另一组喜欢停止嘲笑,并开始testing ..

我上面提到的所有,主要是关于testing模型。 functiontesting是另一回事(使用test.Client()VS webTest VS等)

是否有任何可维护的,可扩展的和适当的方式来testing不同层?

UPDATE

我知道Carl Meyer在PyCon 2012上的演讲

更新08-07-2012

我可以告诉你我的unit testing的做法,为我自己的目的工作得很好,我会给你我的理由:

1.-仅使用Fixtures来获得testing所需的信息,但不会改变,例如,你需要一个用户来进行每一个testing,所以使用一个基础设备来创build用户。

2.-使用工厂创build你的对象,我个人喜欢FactoryBoy (这是来自FactoryGirl这是一个ruby库)。 我为每个应用程序保存所有这些对象创build一个名为factories.py的单独文件。 通过这种方式,我将testing文件保留在所有需要的对象上,这使得它更易读易维护。 关于这种方法的一个很酷的事情就是你创build了一个可以被修改的基础对象,如果你想testing一些基于某个工厂对象的东西。 另外它不依赖于Django,所以当我开始使用MongoDB并且需要testing它们时,当我移植这些对象时,一切都很顺利。 现在读完工厂之后,常常会说“为什么我要使用夹具呢”。 由于这些设备不应该改变从工厂所有额外的好东西都是没用的,Django支持夹具非常好的开箱。

3.-我模拟了外部服务的调用,因为这些调用使得我的testing非常慢,而且他们依赖于与我的代码无关的事情。 例如,如果我在我的testing中发微博,我会testing它是否正确鸣叫,复制响应并模拟该对象,以便每次都返回确切的响应,而无需进行实际的调用。 当事情出错了,有时候也是很好的testing,嘲讽对于这个很好。

4.-我使用集成服务器( jenkins是我的build议),每次我推送到我的登台服务器时运行testing,如果它们失败,它会发送给我一个电子邮件。 这真是太好了,因为在我最后的变化中,我发生了一些其他的事情,并且我忘了运行testing。 它也给你其他的东西,如覆盖率报告, pylint / jslint / pep8validation,并且有很多插件可以设置不同的统计信息。

关于你testing前端的问题,django带有一些辅助函数来处理这个基本的方法。

这是我个人使用,你可以获取,发布,login用户等,这对我来说就够了。 我不倾向于使用像selenium这样的完整的前端testing引擎,因为我觉得testing除了业务层之外的任何东西都是一种矫枉过正的行为。 我相信有些人会有所不同,这总是取决于你在做什么。

除了我的意见,django 1.4带有一个非常方便的浏览器框架集成 。

我会设置一个示例应用程序,我可以应用这种做法,这样更容易理解。 我们来创build一个非常基本的博客应用:

结构体

blogger/ __init__.py models.py fixtures/base.json factories.py tests.py 

models.py

  from django.db import models class Blog(models.Model): user = models.ForeignKey(User) text = models.TextField() created_on = models.DateTimeField(default=datetime.now()) 

灯具/ base.json

 [ { "pk": 1, "model": "auth.user", "fields": { "username": "fragilistic_test", "first_name": "demo", "last_name": "user", "is_active": true, "is_superuser": true, "is_staff": true, "last_login": "2011-08-16 15:59:56", "groups": [], "user_permissions": [], "password": "IAmCrypted!", "email": "test@email.com", "date_joined": "1923-08-16 13:26:03" } } ] 

factories.py

 import factory from blog.models import User, Blog class BlogFactory(factory.Factory): FACTORY_FOR = Blog user__id = 1 text = "My test text blog of fun" 

tests.py

 class BlogTest(TestCase): fixtures = ['base'] # loads fixture def setUp(self): self.blog = BlogFactory() self.blog2 = BlogFactory(text="Another test based on the last one") def test_blog_text(self): self.assertEqual(Blog.objects.filter(user__id=1).count(), 2) def test_post_blog(self): # Lets suppose we did some views self.client.login(username='user', password='IAmCrypted!') response = self.client.post('/blogs', {'text': "test text", user='1'}) self.assertEqual(response.status, 200) self.assertEqual(Blog.objects.filter(text='test text').count(), 1) def test_mocker(self): # We will mock the datetime so the blog post was created on the date # we want it to mocker = Mock() co = mocker.replace('datetime.datetime') co.now() mocker.result(datetime.datetime(2012, 6, 12)) with mocker: res = Blog.objects.create(user__id=1, text='test') self.assertEqual(res.created_on, datetime.datetime(2012, 6, 12)) def tearDown(self): # Django takes care of this but to be strict I'll add it Blog.objects.all().delete() 

注意我为了这个例子使用了一些特定的技术(它没有经过testing)。

我必须坚持,这可能不是标准的最佳实践(我怀疑它存在),但它对我来说工作得很好。

我真的很喜欢@Hassek的build议,并且想强调一下,他对于Django许多方面显然缺乏标准实践提出了非常好的观点,而不仅仅是testing,因为我们所有人都以不同的关注点考虑到我们在devise应用程序时所具有的高度灵活性,我们通常会得到适用于相同问题的截然不同的解决scheme。

尽pipe如此,尽pipe如此,我们大多数人在testing我们的应用程序时仍然努力达到许多相同的目标,主要是:

  • 保持我们的testing模块整齐地组织
  • 创build可重用的断言和辅助方法,帮助减lesstesting方法LOC的帮助函数,使它们更加紧凑和可读
  • 显示有一个明显的,系统的方法来testing应用程序组件

像@Hassek,这些是我的偏好,可能会直接与您可能会应用的做法相冲突,但我觉得分享我们已经certificate的工作,如果只是在我们的情况下,是很好的。

没有testing案例夹具

应用程序夹具的工作很好,如果你有一定的模型数据,你想保证出现在数据库中,比如说一个城镇名称和邮局号码。

但是,我认为这是提供testing用例数据的不灵活的解决scheme。 testing装置非常冗长,模型突变迫使您要么经历复制夹具数据的漫长过程,要么执行繁琐的手动更改,并且保持参考完整性难以手动执行。

另外,你很可能在你的testing中使用多种固定装置,而不仅仅是模型:你希望存储来自API请求的响应主体,创build针对NoSQL数据库后端的装置,写入具有使用的装置填写表单数据等

最后,利用API来创build数据是简洁易读的,而且更容易发现关系,所以我们大多数人都采用工厂来dynamic创build灯具。

广泛使用工厂

工厂的function和方法比跺出你的testing数据更好。 您可以创build帮助程序工厂模块级function或testing用例方法,您可能希望在应用程序testing或整个项目中重复使用。 特别是,@Hassek提到的factory_boy为您提供了inheritance/扩展灯具数据的能力,并进行自动sorting,否则,如果您手动完成,可能会显得有些笨拙。

利用工厂的最终目标是减less代码重复,并简化您创buildtesting数据的方式。 我不能给你确切的指标,但是我确定,如果你用明智的眼光来检验你的testing方法,你会发现testing代码的很大一部分主要是准备你需要的数据来驱动你的testing。

当这样做是不正确的,阅读和维护testing成为一个耗尽的活动。 当数据突变导致全面的testing失败时,这种情况往往会升级,此时您将无法进行系统的重构。

我个人对这个问题的解决方法是从一个myproject.factory模块开始,该模块为我的模型创build易于访问的QuerySet.create方法的引用,也为我在大多数应用testing中经常使用的任何对象创build方法:

 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) 

大多数人对于减less代码重复都很严格,但是实际上我会有意识的介绍一些我认为有助于testing全面性的东西。 同样,无论采取哪种方法,工厂都要把每个testing方法的标题中引入的脑波最小化。

使用嘲笑,但明智地使用它们

我是一个mock ,因为我对作者的解决scheme感到欣慰,因为我认为他想解决的问题是。 软件包提供的工具允许您通过注入预期结果来形成testing断言。

 # 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以外的某个东西,那么有人可能不明白为什么testing方法突然失败。

所以拥有巨大的权力就有了很大的责任 – 如果你想成为一名聪明的人,并且使用模拟来避免根深蒂固的testing障碍,那么你可能会完全混淆你的testing失败的本质。

首先是一致的。 非常非常一致

这是最重要的一点。 绝对一致:

  • 如何在每个testing模块中组织代码
  • 如何为应用程序组件引入testing用例
  • 你如何引入testing方法来声明这些组件的行为
  • 你如何构造testing方法
  • 如何处理testing通用组件(基于类的视图,模型,表单等)
  • 你如何申请重复使用

对于大多数项目来说,关于如何协同testing的方法往往被忽视。 虽然应用程序代码本身看起来很完美 – 遵循风格指南,使用Python成语,重新应用Django自己的解决相关问题的方法,框架组件的教科书使用等 – 没有人真正努力弄清楚如何把testing代码转换成一个有效的,有用的通信工具,如果可能的话,只要有明确的testing代码指导就可以了。