38

我对测试并不陌生,但对在 Django 中测试不同层的一堆建议感到非常困惑。

有些人建议(他们是对的)避免模型中的Doctests,因为它们不可维护......

其他人说不要使用固定装置,因为它们不如辅助功能灵活,例如..

还有两组人为使用Mock对象而战。第一组相信使用 Mock 并隔离系统的其余部分,而另一组更喜欢 停止 Mocking 并开始测试..

我上面提到的主要是关于测试模型。功能测试是另一回事(使用 test.Client() VS webTest VS etc.)

是否有任何可维护、可扩展和正确的方法来测试不同的层?

更新

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

4

2 回答 2

45

更新 08-07-2012

我可以告诉你我的单元测试实践对我自己的目的非常有效,我会给你我的理由:

1.- 仅将Fixtures用于测试所需但不会更改的信息,例如,每次测试都需要一个用户,因此使用基本Fixture 来创建用户。

2.- 使用工厂来创建你的对象,我个人喜欢FactoryBoy(这来自 FactoryGirl,它是一个 ruby​​ 库)。我为保存所有这些对象的每个应用程序创建了一个名为 factory.py 的单独文件。通过这种方式,我将所有需要的对象都放在测试文件之外,这使得它更具可读性和易于维护。这种方法很酷的一点是,您可以创建一个基础对象,如果您想基于工厂中的某个对象测试其他内容,则可以对其进行修改。它也不依赖于 django,所以当我开始使用 mongodb 并需要测试它们时迁移这些对象时,一切都很顺利。现在,在阅读了有关工厂的信息后,通常会说“那我为什么要使用固定装置”。由于这些灯具永远不应该改变所有来自工厂的额外好东西都是无用的,而且 django 开箱即用地支持灯具。

3.- 我模拟对外部服务的调用,因为这些调用使我的测试非常缓慢,并且它们依赖于与我的代码是对还是错无关的事情。例如,如果我在测试中发推文,我会测试它是否正确发推文,复制响应并模拟该对象,以便它每次都返回准确的响应而不进行实际调用。有时也可以在出现问题时进行测试,而嘲讽对此非常有用。

4.- 我使用一个集成服务器(jenkins是我在这里推荐的),每次我推送到我的登台服务器时都会运行测试,如果它们失败,它会向我发送一封电子邮件。这太好了,因为我经常在上次更改中破坏其他东西并且忘记运行测试。它还为您提供其他好东西,例如覆盖率报告、pylint / jslint / pep8验证,并且存在许多插件,您可以在其中设置不同的统计信息。

关于您测试前端的问题,django 附带了一些辅助函数以基本方式处理此问题。

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

除了我的意见之外,django 1.4 还为浏览器内框架提供了非常方便的集成

我将设置一个示例应用程序,我可以在其中应用这种做法,以便更容易理解。让我们创建一个非常基本的博客应用程序:

结构体

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

模型.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"
    }
}
]

工厂.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"

测试.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()

请注意,为了示例,我使用了一些特定的技术(顺便说一句,尚未测试)。

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

于 2012-07-18T16:37:05.597 回答
20

我真的很喜欢@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 自己的方法来解决相关问题、使用框架组件的教科书等——但没有人真正努力弄清楚如何将测试代码变成有效的、有用的通信工具,如果只需要为测试代码制定明确的指导方针,那就太可惜了。

于 2012-08-07T18:31:41.410 回答