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

Soa*_*ask 37 testing django tdd automated-tests django-testing

不是新手测试,但是对于在Django中测试不同层的一堆建议感到非常困惑.

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

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

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

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

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

UPDATE

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

Has*_*sek 45

更新08-07-2012

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

1.- 仅使用Fixtures来获取测试所需的信息,但不会改变,例如,您需要每个测试都需要一个用户,因此使用基础夹具来创建用户.

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

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

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

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

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

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

我将设置一个示例应用程序,我可以应用此实践,因此它更容易理解.让我们创建一个非常基本的博客应用程序:

结构体

blogger/
    __init__.py
    models.py
    fixtures/base.json
    factories.py
    tests.py
Run Code Online (Sandbox Code Playgroud)

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())
Run Code Online (Sandbox Code Playgroud)

灯具/ 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"
    }
}
]
Run Code Online (Sandbox Code Playgroud)

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"
Run Code Online (Sandbox Code Playgroud)

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()
Run Code Online (Sandbox Code Playgroud)

请注意,为了示例,我使用了一些特定的技术(尚未经过测试).

我必须坚持,这可能不是标准的最佳实践(我怀疑它存在),但它对我来说非常好.

  • 您可以提出另一个问题,因为它有点长,但简而言之:您希望项目架构尽可能模块化.你想如何做到这一点是在django/python中将一切都作为便携式自包含应用程序.理想情况下,您的项目应该比settings.py和urls.py多得多.我的应用程序有3个阶段:core,libs和contrib(第三方包括自制的开源库).Libs在结构上与contrib相同,它们位于项目之外,并拥有自己的setup.py和git repo.您希望代码从左向右移动.捆绑你的测试...... (3认同)
  • 希望这能让你前进,去测试吧!嘿 (2认同)

Fil*_*vić 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
Run Code Online (Sandbox Code Playgroud)

这反过来允许我做这样的事情:

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)
Run Code Online (Sandbox Code Playgroud)

大多数人都严格要求减少代码重复,但实际上,每当我认为它有助于测试全面性时,我会故意介绍一些.再次,无论您采用哪种方法进入工厂,目标都是最大限度地减少您在每种测试方法的标题中引入的脑量.

使用模拟,但明智地使用它们

我很喜欢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)
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,模拟确实很有帮助,但它们有一个令人讨厌的副作用:你的模拟清楚地表明了你对应用程序行为的假设,这引入了耦合.如果Converter重构使用除了以外的东西CurrencyApi,有人可能不明白为什么测试方法突然失败.

因此,强大的功能带来了巨大的责任 - 如果您将成为一名智能手机并使用模拟来避免根深蒂固的测试障碍,您可能会完全模糊测试失败的真实性质.

最重要的是,要保持一致.非常非常一致

这是最重要的一点.绝对符合一切:

  • 如何在每个测试模块中组织代码
  • 如何为应用程序组件引入测试用例
  • 如何引入用于断言这些组件行为的测试方法
  • 你如何构建测试方法
  • 如何处理测试常见组件(基于类的视图,模型,表单等)
  • 你如何应用重用

对于大多数项目而言,关于如何协同进行测试的经常被忽略.虽然应用程序代码本身看起来很完美 - 坚持使用样式指南,使用Python习语,重新应用Django自己的解决相关问题的方法,使用框架组件的教科书等等 - 没有人真正努力弄清楚如何将测试代码转换为有用的,有用的通信工具,如果可能有明确的测试代码指南,那就太遗憾了.