【问题标题】:A DRY way of writing similar unit tests in python在 python 中编写类似单元测试的 DRY 方式
【发布时间】:2015-11-18 20:48:21
【问题描述】:

我在 python 中有一些类似的单元测试。 如此相似,以至于只有一个论点在改变。

class TestFoo(TestCase):
    def test_typeA(self):
        self.assertTrue(foo(bar=TYPE_A))

    def test_typeB(self):
        self.assertTrue(foo(bar=TYPE_B))

    def test_typeC(self):
        self.assertTrue(foo(bar=TYPE_C))

    ...

显然这不是很干,如果你有 4-5 个不同的选项,代码将会非常重复

现在我可以做这样的事情了

class TestFoo(TestCase):
    BAR_TYPES = (
        TYPE_A,
        TYPE_B,
        TYPE_C,
        ...
    )

    def _foo_test(self, bar_type):
        self.assertTrue(foo(bar=bar_type))

    def test_foo_bar_type(self):
        for bar_type in BAR_TYPES:
            _foo_test(bar=bar_type))

这可行,但是当引发异常时,我如何知道 _foo_test 是否使用参数 TYPE_A、TYPE_B 或 TYPE_C 失败?

也许有更好的方法来构建这些非常相似的测试?

【问题讨论】:

  • 在测试中重复是可以的,只要它保持简单易懂。您不想为您的测试编写测试吗?
  • 我在一些地方读到,在测试中重复是可以的。但后来这一切都只是 python 代码。上面的例子显然是微不足道的。有时我可能会在一个测试套件中为一个bar_type 进行 5 个不同的系统测试,每个测试可以运行到 10 行。因此,对于 X 个bar_type,现在必须复制和粘贴 X 次 50 行代码。然后我必须手动编辑每个“bar_type”引用。任何时候我添加另一个“bar_type”我现在必须去复制和粘贴数百个测试,这样我就可以覆盖另一个条件。绝对不干。

标签: python django python-2.7 unit-testing testing


【解决方案1】:

大多数 TestCase 断言方法,包括 assertTrue,都采用可选的 msg 参数。

如果您更改 BAR_TYPES 元组以包含变量名称,那么您可以将其包含在断言失败时显示的消息中。

class TestProcessCreateAgencyOfferAndDispatch(TestCase):
    BAR_TYPES = (
        ('TYPE_A', TYPE_A),
        ('TYPE_B', TYPE_B),
        ('TYPE_C', TYPE_C),
        ...
    )

    def _foo_test(self, var_name, bar_type):
        self.assertTrue(foo(bar=bar_type), var_name)

    def test_foo_bar_type(self):
        for (var_name, bar_type) in BAR_TYPES:
            _foo_test(bar=bar_type), var_name=var_name)

【讨论】:

  • 好主意。我的问题是,如果在self.assertTrue 调用之前foo 函数内部发生故障,那么您不知道是哪个bar_type 测试导致它,您只会收到错误test_foo_bar_type failed
  • 好点。包含var_name 意味着一旦测试失败,您可以通过添加print(var_name) 更快地进行调试,但我同意这并不理想。
【解决方案2】:

你想要做的本质上是一个参数化的测试。此功能不包含在标准 django 或 python unittest 模块中,但许多库提供它:nose-parameterizedpy.testddt

到目前为止,我最喜欢的是 ddt:它最类似于 NUnit-JUnit 风格的参数化测试,非常轻量级,不会妨碍您,也不需要专门的测试运行器(如 nose-parameterized 那样)。它可以帮助您的方式是修改测试名称以包含所有参数,因此您可以通过查看测试名称清楚地看到哪个测试用例失败。

使用 ddt,您的示例将如下所示:

import ddt

@ddt.ddt
class TestProcessCreateAgencyOfferAndDispatch(TestCase):

    @ddt.data(TYPE_A, TYPE_B, TYPE_C)
    def test_foo_bar_type(self, type):
        self.assertTrue(foo(bar=type))

在这种情况下,名称将类似于test_foo_bar_type__TYPE_A(从技术上讲,它的构造类似于[test_name]__[repr(parameter_1)]__[repr(parameter_2)])。

作为奖励,它更简洁(没有辅助方法),并且您获得了三种方法而不是一种。这里的好处是你可以在一个方法中测试各种代码路径,每个路径得到一个测试用例(但需要一定的思考,有时对某些代码路径进行专门的测试会更好)

【讨论】:

  • ddt 确实有效。虽然我发现鼻子参数化对于传递已经在元组中定义的参数更容易一些。谢谢约翰。
  • @luke_aus 不客气。使用 ddt,您可以使用 @data(*PARAMETERS_TUPLE)(注意星号)传递在元组中定义的参数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-10-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-13
  • 1970-01-01
相关资源
最近更新 更多