自动检测测试耦合

时间:2016-12-24 02:25:10

标签: python django testing pytest nose

我们有一个庞大的测试代码库,其中包含超过1500个Python / Django应用程序测试。大多数测试使用factory-boy来生成项目模型的数据。

目前,我们正在使用nose test runner,但可以切换到py.test

问题在于,当运行部分测试时,我们会遇到意外的测试失败,这些失败在运行所有测试或单独测试时都不会被重现。

看起来测试实际上是耦合的

问题:是否可以自动检测项目中的所有耦合测试?

我目前的想法是以不同的随机组合或顺序运行所有测试并报告失败,可以nosepy.test帮助吗?

4 个答案:

答案 0 :(得分:5)

要获得明确答案,您必须完全隔离其他测试。

使用pytest,这是我使用的,您可以实现一个首先使用--collect-only运行它的脚本,然后使用返回的测试节点ID启动单个pytest运行他们每个人。 对于1500次测试,这将花费很长时间,但只要您完全重新创建,它就应该完成工作 每个单独测试之间的系统状态。

对于近似答案,您可以尝试以随机顺序运行测试,并查看有多少开始失败。我最近有一个类似的问题,所以我尝试了两个pytest插件 - pytest-randomlypytest-randomhttps://pypi.python.org/pypi/pytest-randomly/ https://pypi.python.org/pypi/pytest-random/

在这两者中,pytest-randomly看起来更成熟,甚至支持通过接受seed参数重复某个订单。

这些插件在测试顺序随机化方面做得很好,但是对于大型测试套件来说,完全随机化可能不太可行,因为你有太多的失败测试但你不知道从哪里开始。

我编写了自己的插件,允许我控制测试可以随机更改顺序的级别(模块,包或全局)。它被称为pytest-random-orderhttps://pypi.python.org/pypi/pytest-random-order/

<强>更新即可。在您的问题中,您说在单独运行测试时无法重现故障。可能是您没有完全重新创建单个测试运行的环境。我认为一些测试可以让状态变脏。每个测试用例都有责任根据需要设置环境,之后不一定要清理,因为这会导致后续测试的性能开销,或者只是因为这样做的负担。

如果测试X作为较大测试套件的一部分失败,然后在单独运行时没有失败,那么此测试X在为测试设置环境方面做得不够好。

答案 1 :(得分:2)

由于您已经在使用nosetests框架,或许您可以使用nose-randomlyhttps://pypi.python.org/pypi/nose-randomly)以随机顺序运行测试用例。

每次使用nose-randomly进行鼻子测试时,每次运行都会使用 随机种子 进行标记,您可以将其用于 重复运行测试的相同顺序

因此,您使用此插件多次运行测试用例并记录随机种子。每当您看到特定订单的任何失败时,您始终可以通过使用随机种子运行它们来重现它们。

理想情况下,除非您运行 2 ^ 1500-1 的1500个测试的所有组合,否则无法识别测试依赖项和失败。

因此,养成在运行测试时随机启用测试的习惯。在某些时候,你会遇到失败并继续运行它们,直到你发现尽可能多的失败。

除非失败正在捕捉产品的真正缺陷,否则修复它们并尽可能减少测试依赖性始终是一个好习惯。这将保持测试结果的一致性,您可以随时独立运行和验证测试用例,并确保产品质量与该方案相关。

希望有所帮助,这就是我们在工作场所所做的事情,以达到你想要达到的完全相同的状态。

答案 2 :(得分:1)

我已经解决了一个大型Django项目的类似问题,该项目也使用了鼻子和工厂男孩。我无法告诉你如何自动检测测试耦合,就像要求的问题一样,但我事后才能说明在我的情况下引起耦合的一些问题:

检查TestCase的所有导入,并确保他们使用Django的TestCase而不是unittest的TestCase如果团队中的某些开发人员正在使用PyCharm,有一个方便的自动导入功能,很容易从错误的地方意外导入名称。单元测试TestCase将很乐意在Django项目的大型测试套件中运行,但您可能无法获得Django测试用例具有的良好提交和回滚功能。

确保覆盖setUptearDownsetUpClasstearDownClass的任何测试类也委托给super我知道这听起来很明显,但很容易忘记!

由于工厂男孩,可变状态也可能潜入。小心使用工厂序列,看起来像:

name = factory.Sequence(lambda n: 'alecxe-{0}'.format(n))

即使db是干净的,如果其他测试事先已经运行,序列也可能不会从0开始。如果您对工厂男孩创建的Django模型的值有不正确的假设,那么这可能会让您感到困惑。

同样,您无法对主键进行假设。假设一个django模型Potato被键入了一个自动字段,并且在测试开始时没有Potato行,并且工厂男孩创建了一个马铃薯,即你使用了PotatoFactory() setUp。令人惊讶的是,您无法保证主键为1。您应该持有对工厂返回的实例的引用,并对该实际实例进行断言。

RelatedFactorySubFactory也要非常小心。工厂男孩习惯于挑选任何旧实例以满足关系,如果已经存在于数据库中。这意味着您作为相关对象获得的内容有时是不可重复的 - 如果在setUpClass或者工具中创建了其他对象,则工厂选择(或创建)的相关对象可能是不可预测的,因为测试的顺序是任意的。

Django模型具有带@receiverpost_save钩子的pre_save装饰器的情况对于工厂男孩正确处理非常棘手。为了更好地控制相关对象,包括只抓取任何旧实例的情况可能不正确,有时您必须通过覆盖工厂上的_generate类方法和/或使用{实现自己的钩子来自己处理细节。 {1}}装饰者。

答案 3 :(得分:0)

当测试未正确拆除其环境时会发生这种情况。

即:在测试的设置阶段,一个人在测试数据库中创建一些对象,可能写入一些文件,打开网络连接等等,但是没有正确地重置状态,因此将信息传递给后续测试,然后由于输入数据的错误假设而失败。

而不是专注于测试之间的耦合(在上面的情况下会因为它可能取决于它们运行的​​顺序而有点没有意义),也许最好运行一个测试拆卸例程的例程每次测试。

这可以通过包装原始的Test类并覆盖拆卸功能来实现,包括某种通用测试,测试环境已经针对给定测试正确重置。

类似的东西:

class NewTestClass(OriginalTestClass):
   ...

    def tearDown(self, *args, **kwargs):
        super(NewTestClass, self).tearDown(*args, **kwargs)
        assert self.check_test_env_reset() is True, "IM A POLLUTER"

然后在您的测试文件中用新的替换原始测试类的import语句:

# old import statement for OriginalTestClass
from new_test_class import NewTestclass as OriginalTestClass

随后运行测试应导致导致污染的失败。

另一方面,如果您希望允许测试有点脏,那么您可以将问题视为失败测试的测试环境的错误设置。

从后面的角度来看,失败的测试是编写得很糟糕的测试,需要单独修复。

这两种观点在某种程度上是阴阳,你可以采用任何一种观点。我尽可能支持后者,因为它更强大。