【问题标题】:What are the best practices for testing "different layers" in Django? [closed]在 Django 中测试“不同层”的最佳实践是什么? [关闭]
【发布时间】:2012-07-17 14:11:45
【问题描述】:

不是刚接触测试,但对在 Django 中测试不同层的一堆乱七八糟的建议感到非常困惑。

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

其他人说不要使用 fixtures,因为它们不如 helper functions 灵活,例如..

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

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

是否有ANY可维护、可扩展和适当的方法来测试不同的层?

更新

我知道Carl Meyer's talk 在 PyCon 2012..

【问题讨论】:

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


    【解决方案1】:

    2012 年 8 月 7 日更新

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

    1.- 仅将Fixtures 用于测试所需但不会更改的信息,例如,您需要为每个测试提供用户,因此使用 base 固定装置创建用户。

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

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

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

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

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

    除了我的意见,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())
    

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

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

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

    【讨论】:

    • 您可以为每个应用程序分离应用程序测试并在那里创建一个工厂文件,或者为每个应用程序创建不同的工厂文件,例如 dashboard_factory。但是,如果您处于中等项目并认真进行测试,那么为您的所有工厂创建一个文件将会变得非常庞大
    • 希望这能让你继续前进,去测试吧!哈哈
    • 您可以问另一个问题,因为它有点长,但简而言之:您希望项目架构尽可能模块化。你想在 django/python 中如何做到这一点,就是让一切都成为可移植的自包含应用程序。理想情况下,您的项目应该不会比 settings.py 和 urls.py 多多少。我的应用程序有 3 个阶段:核心、库和贡献(第三方包括自制的开源库)。 Libs 在结构上与 contrib 相同,它们位于项目之外并且有自己的 setup.py 和 git repo。您希望您的代码从左向右移动。并捆绑您的测试...
    • 我使用mocker
    • 在这种情况下,django 会处理它,但更严格地说,您可以自己将 Blog 对象放在 tearDown 中。我刚刚用它更新了答案
    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-07
      • 1970-01-01
      • 2012-06-30
      • 2010-09-17
      相关资源
      最近更新 更多