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

时间:2012-07-18 13:57:50

标签: django testing tdd automated-tests django-testing

刚接触测试,但是对于在Django中测试不同图层的建议很混乱。

有人建议(并且他们是正确的)避免模型中的 Doctests ,因为它们无法维护......

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

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

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

是否 ANY 可维护,可行且适当的方法来测试不同的图层?

更新

我在PyCon 2012上了解Carl Meyer's talk ..

2 个答案:

答案 0 :(得分:44)

更新08-07-2012

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

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

2.-使用工厂创建对象,我个人喜欢FactoryBoy(这来自FactoryGirl,这是一个红宝石库)。我为每个保存所有这些对象的应用程序创建了一个名为factories.py的独立文件。这样我就可以将测试文件保存在我需要的所有对象中,从而使其更易读,更易于维护。关于这种方法的一个很酷的事情是,如果你想根据工厂中的某个对象测试其他东西,你可以创建一个可以修改的基础对象。它也不依赖于django所以当我开始使用mongodb并且需要测试它们时我迁移这些对象时,一切都很顺利。现在,在阅读了有关工厂之后,我们常常会说"为什么我会想要使用灯具呢?#34;。由于这些灯具永远不会改变所有来自工厂的额外好东西都是无用的,并且django非常好地支持固定装置。

3.-我Mock调用外部服务,因为这些调用使我的测试非常慢,并且它们依赖于与我的代码无关的事情。例如,如果我在我的测试中发推文,我会测试它正确推文,复制响应并模拟该对象,以便每次都返回那个确切的响应而不进行实际调用。当事情出错时,有时候测试也很好。嘲笑很有用。

4.-我使用集成服务器(jenkins是我的建议),每当我推送到我的登台服务器时运行测试,如果它们失败,它会给我发送一封电子邮件。这真是太棒了,因为在我最后一次改变中我打破了其他东西并且忘记运行测试了。它还为您提供了覆盖率报告,pylint / jslint / pep8验证等其他好处,并且您可以设置许多插件来设置不同的统计信息。

关于测试前端的问题,django附带了一些helper functions以基本方式处理这个问题。

这是我个人使用的,你可以点火,发帖,登录用户等对我来说足够了。我不倾向于使用完整的前端测试引擎,比如硒,因为我认为测试除业务层之外的任何其他东西都是过度的。我相信有些人会有所不同,这取决于你在做什么。

除了我的观点,django 1.4为浏览器内框架提供了非常方便的integration

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

<强>结构

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()

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

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

答案 1 :(得分:20)

我非常喜欢来自@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自己解决相关问题的方法,使用框架组件的教科书等等 - 没有人真正努力弄清楚如何将测试代码转换为有用的,有用的通信工具,如果可能有明确的测试代码指南,那就太遗憾了。