编写一个 Python 单元测试,确保其不会并行运行。

7

tl;dr - 我想编写一个Python unittest函数,该函数将删除文件,运行测试,然后恢复文件。这会导致竞争条件,因为unittest会并行运行多个测试,删除和创建一个测试的文件会破坏同时进行的其他测试。

具体示例:

我有一个名为converter.py的Python模块,并且在test_converter.py中有相关的测试。如果与converter.py相同目录下有名为config_custom.csv的文件,则会使用自定义配置。如果没有自定义CSV配置文件,则内置在converter.py 中有一个默认配置。

我使用Python 2.7标准库中的unittest编写了一个单元测试来验证此行为。在setUp()中的单元测试将把config_custom.csv重命名为wrong_name.csv,然后运行测试(希望使用默认配置),然后在tearDown()中它会将文件重命名回原本的名称。

问题: Python单元测试并行运行,我遇到了可怕的竞态条件。文件config_custom.csv会以不确定的方式在其他单元测试中被重命名。这至少会导致90%的时间中出现一个错误或失败。

理想的解决方法是告诉unittest不要与其他测试并行运行此测试,这个测试很特殊,并需要完全隔离。

我的解决方法是向搜索配置文件的函数添加一个可选参数。测试套件才传递该参数。它忽略文件而不删除它。实际上删除测试文件更为优雅,这正是我要测试的内容。

5个回答

1
Django 3.2文档提供了一个官方解决方案来解决这个问题,使用django.test.testcases.SerializeMixin。这将强制运行某些测试并防止它们在尝试访问相同资源时出现错误。
根据文档,您可以像这样操作:
import os

from django.test import TestCase
from django.test.testcases import SerializeMixin

class ImageTestCaseMixin(SerializeMixin):
    lockfile = __file__

    def setUp(self):
        self.filename = os.path.join(temp_storage_dir, 'my_file.png')
        self.file = create_file(self.filename)

class RemoveImageTests(ImageTestCaseMixin, TestCase):
    def test_remove_image(self):
        os.remove(self.filename)
        self.assertFalse(os.path.exists(self.filename))

class ResizeImageTests(ImageTestCaseMixin, TestCase):
    def test_resize_image(self):
        resize_image(self.file, (48, 48))
        self.assertEqual(get_image_size(self.file), (48, 48))

1
我经常需要进行完全隔离的测试。我发现唯一可靠的方法是将这些测试放在单独的类中。同意,在测试内处理配置文件仍然有点麻烦。
对于我现在正在做的事情,我可能会尝试使用pytest-ordering以更确定的方式运行测试。

0

首先,大多数支持并行的测试框架也支持序列化某些重要测试的方法。例如,Python Fabric 支持一个串行注释(当您使用命令行标志 -P 启动测试作为并行套件时):

from fabric.api import *

def runs_in_parallel():
    pass

@serial
def runs_serially():
    pass

然而,我认为你不应该这样做,我认为你应该停下来思考一下。根据单一职责原则,我认为情况是这样的。

你现在处于一个情况中,测试基本上已经确定了你代码的某个条件,它为你突出了一点内聚性。

现在你应该意识到,“converter.py”有太多的职责,它不仅在幕后执行一些复杂的操作,还要负责管理自己的创建和设置,这可能对它来说太多了。

也许如果你有一个对象可以管理读取那个文件,你就可以轻松地模拟/存根它,并产生你想要测试的条件。


我知道 Fabric,但不知道@serial装饰器,所以谢谢你。关于单一职责原则:我觉得我已经很好地遵守了它。converter.py只是一个文件,但有多个函数(不是太多)。其中一个函数是load_config(),它只有一个职责。如果它找到配置文件,它就会加载它,否则它就会使用默认值。实际上它不需要任何参数,但我添加了 load_config(use_defaults=False),并且仅在单元测试期间传递True,这正是你建议的。但这并不是最佳选择。 - SerMetAla
使用这样的kwarg有两个问题:首先,converter.py的源代码变得更加复杂了。我刚刚向converter.py添加了一个功能,但实际上没有人会使用它,只有单元测试会使用它。这就是属于测试而不是代码本身的定义。其次:如果所有测试都通过了,但实际上找不到文件导致了严重崩溃怎么办?单元测试将无法发现这个问题。单元测试将通过。如果os.listdir()发生了变化并出现了回归,我将看不到它。 - SerMetAla
是的,我同意你的观点。很久以前回答过这个问题,现在对事情有不同的看法。 - MattSaw

0

最佳的测试策略是确保在不相交的数据集上进行测试。这将绕过任何竞态条件并使代码更简单。如果您使用上下文管理器,则还应模拟open__enter__/__exit__。这将允许您模拟文件不存在的情况。


-1
问题在于config_custom.csv的名称本身应该是可配置的参数。然后,每个测试只需寻找config_custom_<nonce>.csv,可以并行运行任意数量的测试。
整个套件的清理只需清除config_custom_*.csv,因为此时我们将不需要它们中的任何一个。

文件名实际上是可配置的。我真的想测试如果找不到任何东西的行为。我的解决方法,就像我原来的问题中提到的那样,基本上与这个建议类似,它实际上并没有测试我想要的行为。我的解决方法是告诉配置文件查找器“不要查找任何配置文件”。你的建议基本上是“只查找特定的配置文件”。我想测试它正常查找但找不到任何东西的行为。我不想关闭或限制搜索本身。 - SerMetAla
@SerMetAla 使用不同的文件是一种良好的方式,可以在不引入锁定的情况下完成操作。如果您无法使用不同的文件集,则应该使用锁定。 - U2EF1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接