自动检测测试耦合
Automatically detect test coupling
我们有一个大型测试代码库,其中包含针对 Python/Django 应用程序的 1500 多个测试。大多数测试使用 factory-boy
为项目模型生成数据。
目前,我们正在使用 nose
test runner, but open to switching to py.test
。
问题是,有时,当 运行 进行部分测试时,测试组合会遇到意外的测试失败,这些失败在 运行 进行所有测试或这些测试时无法重现个别地。
看起来 测试实际上是耦合的 。
问题:是否可以自动检测项目中所有的耦合测试?
我目前的想法是 运行 以不同的随机组合或排序并报告失败的所有测试,nose
或 py.test
可以帮助吗?
要获得明确的答案,您必须 运行 每个测试都与其他测试完全隔离。
使用 pytest
,这是我使用的,您可以实现一个脚本,首先 运行 将其与 --collect-only
一起使用,然后使用返回的测试节点 ID 启动个人pytest
运行 他们每个人。
这将花费您 1500 次测试的好一段时间,但只要您完全重新创建它就可以完成工作
每次单独测试之间的系统状态。
要获得大概的答案,您可以尝试 运行随机排列您的测试,看看有多少开始失败。我最近有一个类似的问题,所以我尝试了两个 pytest
插件 -- pytest-randomly
和 pytest-random
:
https://pypi.python.org/pypi/pytest-randomly/
https://pypi.python.org/pypi/pytest-random/
两者相比,pytest-randomly
比较成熟,甚至支持通过接受seed
参数来重复某个顺序。
这些插件在随机化测试顺序方面做得很好,但对于大型测试套件,完全随机化可能不太可行,因为你有太多失败的测试,你不知道从哪里开始。
我编写了自己的插件,允许我控制测试可以随机更改顺序(模块、包或全局)的级别。它被称为 pytest-random-order
: https://pypi.python.org/pypi/pytest-random-order/
更新。在您的问题中,您说 运行ning 单独测试时无法重现失败。可能是您没有完全重新创建单独测试的环境 运行。我认为有些测试会使状态变脏是可以的。每个测试用例都有责任根据需要设置环境,并且不必在事后清理,因为性能开销会导致后续测试或仅仅是因为这样做的负担。
如果测试 X 作为较大测试套件的一部分失败,然后在单独 运行ning 时没有失败,则此测试 X 在设置测试环境方面做得不够好。
由于您已经在使用 nosetests
框架,也许您可以使用 nose-randomly
(https://pypi.python.org/pypi/nose-randomly) 以随机顺序 运行 测试用例。
每次你 运行 鼻子用 nose-randomly
测试时,每个 运行 都被标记为 随机种子 您可以使用它来 重复 运行 测试的相同顺序 。
因此,您 运行 使用此插件多次测试用例并记录随机种子。每当您看到任何特定顺序的失败时,您总是可以通过 运行 使用随机种子将它们重现。
理想情况下,除非您 运行 1500 个测试的所有组合 2^1500-1,否则无法识别测试依赖项和失败。
因此,当您 运行 时,请养成 运行 随机启用测试的习惯。在某些时候,你会遇到失败并保持 运行 失败,直到你捕捉到尽可能多的失败。
除非失败正在捕获您产品的真正错误,否则修复它们并尽可能减少测试依赖性始终是一个好习惯。这将保持测试结果的一致性,您始终可以 运行 并独立验证测试用例,并确保围绕该场景的产品质量。
希望对您有所帮助,这就是我们在工作场所所做的,以实现与您想要实现的完全相同的情况。
当测试没有正确拆除其环境时,就会发生这种情况。
即:在测试的设置阶段,在测试数据库中创建了一些对象,可能写入了一些文件,打开了网络连接等等,但是没有正确地重置状态,从而传递了后续测试的信息,然后由于对其输入数据的错误假设而失败。
而不是专注于测试之间的耦合(在上面的例子中这将有点没有实际意义,因为它可能取决于它们在 运行 中的顺序),也许 运行 测试每个测试的拆卸例程的例程。
这可以通过包装您的原始测试 class 并覆盖拆卸函数以包括某种通用测试来完成,该测试环境已针对给定测试正确重置。
类似于:
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"
然后在您的测试文件中将原始测试 class 的导入语句替换为新的:
# old import statement for OriginalTestClass
from new_test_class import NewTestclass as OriginalTestClass
随后 运行 测试应该导致造成污染的测试失败。
另一方面,如果您想让测试变得有些脏,那么您可以将问题视为失败测试的测试环境设置错误。
从后面的角度来看,失败的测试是写得不好的测试,需要单独修复。
这两种观点在某种程度上是阴阳两极,你可以采取任何一种观点。我尽可能支持后者,因为它更健壮。
我已经解决了一个大型 Django 项目的类似问题,该项目也使用了 nose 运行ner 和 factory-boy。我无法告诉您如何自动检测测试耦合,就像所问的问题一样,但事后我可以告诉您一些在我的案例中导致耦合的问题:
检查 TestCase
的所有导入并确保它们使用 Django 的 TestCase
而不是 unittest 的 TestCase
. 如果团队中的某些开发人员正在使用 PyCharm,它有一个方便的自动导入功能,很容易不小心从错误的地方导入名称。 unittest TestCase
会很高兴 运行 在大型 Django 项目的测试套件中,但您可能无法获得 Django 测试用例所具有的良好提交和回滚功能。
确保覆盖 setUp
、tearDown
、setUpClass
、tearDownClass
的任何测试 class 也委托给 super
. 我知道这听起来很明显,但很容易忘记!
可变状态也有可能由于工厂男孩而潜入。小心使用工厂序列,它看起来像:
name = factory.Sequence(lambda n: 'alecxe-{0}'.format(n))
即使数据库是干净的,如果其他测试事先有 运行,序列也可能不会从 0 开始。如果您对工厂男孩创建的 Django 模型的值是什么做出了错误假设的断言,这可能会伤害您。
同样,您不能对主键进行假设。假设一个 django 模型 Potato
是一个自动字段,并且在测试开始时没有 Potato
行,工厂男孩创建了一个土豆,即你在setUp
。令人惊讶的是,您不能保证主键为 1。您应该持有对工厂返回的实例的引用,并针对该实际实例进行断言。
对 RelatedFactory
和 SubFactory
也要非常小心。工厂男孩习惯于选择任何旧实例来满足关系,如果数据库中已经存在的话。这意味着您作为相关对象获得的内容有时是不可重复的 - 如果在 setUpClass
或固定装置中创建了其他对象,则工厂选择(或创建)的相关对象可能是不可预测的,因为测试的顺序是任意的.
Django 模型有 @receiver
装饰器和 post_save
或 pre_save
钩子的情况很难用 factory boy 正确处理。为了更好地控制相关对象,包括仅抓取任何旧实例可能不正确的情况,有时您必须通过覆盖工厂 and/or 上的 _generate
class 方法来自己处理细节使用 @factory.post_generation
装饰器实现你自己的钩子。
我们有一个大型测试代码库,其中包含针对 Python/Django 应用程序的 1500 多个测试。大多数测试使用 factory-boy
为项目模型生成数据。
目前,我们正在使用 nose
test runner, but open to switching to py.test
。
问题是,有时,当 运行 进行部分测试时,测试组合会遇到意外的测试失败,这些失败在 运行 进行所有测试或这些测试时无法重现个别地。
看起来 测试实际上是耦合的 。
问题:是否可以自动检测项目中所有的耦合测试?
我目前的想法是 运行 以不同的随机组合或排序并报告失败的所有测试,nose
或 py.test
可以帮助吗?
要获得明确的答案,您必须 运行 每个测试都与其他测试完全隔离。
使用 pytest
,这是我使用的,您可以实现一个脚本,首先 运行 将其与 --collect-only
一起使用,然后使用返回的测试节点 ID 启动个人pytest
运行 他们每个人。
这将花费您 1500 次测试的好一段时间,但只要您完全重新创建它就可以完成工作
每次单独测试之间的系统状态。
要获得大概的答案,您可以尝试 运行随机排列您的测试,看看有多少开始失败。我最近有一个类似的问题,所以我尝试了两个 pytest
插件 -- pytest-randomly
和 pytest-random
:
https://pypi.python.org/pypi/pytest-randomly/
https://pypi.python.org/pypi/pytest-random/
两者相比,pytest-randomly
比较成熟,甚至支持通过接受seed
参数来重复某个顺序。
这些插件在随机化测试顺序方面做得很好,但对于大型测试套件,完全随机化可能不太可行,因为你有太多失败的测试,你不知道从哪里开始。
我编写了自己的插件,允许我控制测试可以随机更改顺序(模块、包或全局)的级别。它被称为 pytest-random-order
: https://pypi.python.org/pypi/pytest-random-order/
更新。在您的问题中,您说 运行ning 单独测试时无法重现失败。可能是您没有完全重新创建单独测试的环境 运行。我认为有些测试会使状态变脏是可以的。每个测试用例都有责任根据需要设置环境,并且不必在事后清理,因为性能开销会导致后续测试或仅仅是因为这样做的负担。
如果测试 X 作为较大测试套件的一部分失败,然后在单独 运行ning 时没有失败,则此测试 X 在设置测试环境方面做得不够好。
由于您已经在使用 nosetests
框架,也许您可以使用 nose-randomly
(https://pypi.python.org/pypi/nose-randomly) 以随机顺序 运行 测试用例。
每次你 运行 鼻子用 nose-randomly
测试时,每个 运行 都被标记为 随机种子 您可以使用它来 重复 运行 测试的相同顺序 。
因此,您 运行 使用此插件多次测试用例并记录随机种子。每当您看到任何特定顺序的失败时,您总是可以通过 运行 使用随机种子将它们重现。
理想情况下,除非您 运行 1500 个测试的所有组合 2^1500-1,否则无法识别测试依赖项和失败。
因此,当您 运行 时,请养成 运行 随机启用测试的习惯。在某些时候,你会遇到失败并保持 运行 失败,直到你捕捉到尽可能多的失败。
除非失败正在捕获您产品的真正错误,否则修复它们并尽可能减少测试依赖性始终是一个好习惯。这将保持测试结果的一致性,您始终可以 运行 并独立验证测试用例,并确保围绕该场景的产品质量。
希望对您有所帮助,这就是我们在工作场所所做的,以实现与您想要实现的完全相同的情况。
当测试没有正确拆除其环境时,就会发生这种情况。
即:在测试的设置阶段,在测试数据库中创建了一些对象,可能写入了一些文件,打开了网络连接等等,但是没有正确地重置状态,从而传递了后续测试的信息,然后由于对其输入数据的错误假设而失败。
而不是专注于测试之间的耦合(在上面的例子中这将有点没有实际意义,因为它可能取决于它们在 运行 中的顺序),也许 运行 测试每个测试的拆卸例程的例程。
这可以通过包装您的原始测试 class 并覆盖拆卸函数以包括某种通用测试来完成,该测试环境已针对给定测试正确重置。
类似于:
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"
然后在您的测试文件中将原始测试 class 的导入语句替换为新的:
# old import statement for OriginalTestClass
from new_test_class import NewTestclass as OriginalTestClass
随后 运行 测试应该导致造成污染的测试失败。
另一方面,如果您想让测试变得有些脏,那么您可以将问题视为失败测试的测试环境设置错误。
从后面的角度来看,失败的测试是写得不好的测试,需要单独修复。
这两种观点在某种程度上是阴阳两极,你可以采取任何一种观点。我尽可能支持后者,因为它更健壮。
我已经解决了一个大型 Django 项目的类似问题,该项目也使用了 nose 运行ner 和 factory-boy。我无法告诉您如何自动检测测试耦合,就像所问的问题一样,但事后我可以告诉您一些在我的案例中导致耦合的问题:
检查 TestCase
的所有导入并确保它们使用 Django 的 TestCase
而不是 unittest 的 TestCase
. 如果团队中的某些开发人员正在使用 PyCharm,它有一个方便的自动导入功能,很容易不小心从错误的地方导入名称。 unittest TestCase
会很高兴 运行 在大型 Django 项目的测试套件中,但您可能无法获得 Django 测试用例所具有的良好提交和回滚功能。
确保覆盖 setUp
、tearDown
、setUpClass
、tearDownClass
的任何测试 class 也委托给 super
. 我知道这听起来很明显,但很容易忘记!
可变状态也有可能由于工厂男孩而潜入。小心使用工厂序列,它看起来像:
name = factory.Sequence(lambda n: 'alecxe-{0}'.format(n))
即使数据库是干净的,如果其他测试事先有 运行,序列也可能不会从 0 开始。如果您对工厂男孩创建的 Django 模型的值是什么做出了错误假设的断言,这可能会伤害您。
同样,您不能对主键进行假设。假设一个 django 模型 Potato
是一个自动字段,并且在测试开始时没有 Potato
行,工厂男孩创建了一个土豆,即你在setUp
。令人惊讶的是,您不能保证主键为 1。您应该持有对工厂返回的实例的引用,并针对该实际实例进行断言。
对 RelatedFactory
和 SubFactory
也要非常小心。工厂男孩习惯于选择任何旧实例来满足关系,如果数据库中已经存在的话。这意味着您作为相关对象获得的内容有时是不可重复的 - 如果在 setUpClass
或固定装置中创建了其他对象,则工厂选择(或创建)的相关对象可能是不可预测的,因为测试的顺序是任意的.
Django 模型有 @receiver
装饰器和 post_save
或 pre_save
钩子的情况很难用 factory boy 正确处理。为了更好地控制相关对象,包括仅抓取任何旧实例可能不正确的情况,有时您必须通过覆盖工厂 and/or 上的 _generate
class 方法来自己处理细节使用 @factory.post_generation
装饰器实现你自己的钩子。