检测不正确的断言方法

Detecting incorrect assertion methods

在最近的一次代码审查中,我偶然发现了一个不容易立即发现的问题 - 使用 assertTrue() 而不是 assertEqual() 基本上导致了一个测试什么都没测试。这是一个简化的例子:

from unittest import TestCase


class MyTestCase(TestCase):
    def test_two_things_equal(self):
        self.assertTrue("a", "b")

这里的问题是测试会通过;从技术上讲,该代码是有效的,因为 assertTrue has this optional msg argument(在这种情况下获得 "b" 值)。

我们能否比依靠审查代码的人来发现此类问题做得更好?有没有办法 自动检测 它使用静态代码分析 flake8pylint?

Python 现在有一个进行静态代码分析的类型提示系统。使用这个系统,你可以要求像 assertTrue 这样的函数的第一个参数总是布尔值。问题是 assertTrue 不是你定义的,而是由 unittest 包定义的。不幸的是,unittest 包没有添加类型提示。不过,有一种相当简单的方法可以解决这个问题:只需定义您自己的包装器即可。

from unittest import TestCase

class TestCaseWrapper(TestCase):
    def assertTrue(self, expr: bool, msg=None): #The ": bool" requires that the expr parameter is boolean.
        TestCase.assertTrue(self, expr, msg)

class MyTestCase(TestCaseWrapper):
    def test_two_things_equal(self):
        self.assertTrue("a", "b") #Would give a warning about the type of "a".

然后您可以 运行 类型检查器,如下所示:

python -m mypy my_test_case.py

这应该会警告您 "a" 是一个字符串,而不是一个布尔值。这样做的好处是它可以在自动化测试框架中自动 运行 。此外,PyCharm 将检查您的代码中的类型(如果您提供了类型)并突出显示任何错误的地方。

几年前,我想出了一个通用的 approach/methodology 来确保测试的质量。测试规范可以简化为两个子句:

  1. 必须通过才能正确实现被测试的功能,并且
  2. 它必须失败 incorrect/broken 正在测试的功能的实现

据我所知,虽然要求 1. 经常得到满足,但很少有人关注要求 2.

通常

  • 已创建测试套件,
  • 代码是运行反对它,
  • 任何失败(由于代码或测试中的错误)都已修复
  • 当我们相信我们的代码和测试是好的时,我们就会到达这种情况。

实际情况可能是(某些)测试包含错误,这些错误(会)阻止他们捕获代码中的错误。因此,看到测试通过对于关心系统质量的人来说应该不会太平静,直到他们确信测试确实能够检测到他们设计的问题1.一个简单的方法是实际引入此类问题并检查它们是否未被测试忽视!

在 TDD(测试驱动开发)中,这个想法只是部分遵循 - 建议在代码之前添加测试,看到它失败(它应该,因为还没有代码)然后修复它通过编写代码。但是由于缺少代码而导致测试失败并不自动意味着它在错误代码的情况下也会失败(这对您的情况来说似乎是正确的)!

因此,测试套件的质量可以用它能够检测到的错误的百分比来衡量。任何从测试套件中逃脱的合理2 错误都会建议一个涵盖该场景的新测试用例(或者,如果测试套件应该捕获该错误,则会发现测试套件中的错误) .这也意味着套件的每个测试都必须能够捕获至少一个错误(否则,该测试完全没有意义)。

我正在考虑实施一个有助于采用这种方法的软件系统(即允许在代码库中注入和维护人工错误并检查测试如何响应它们)。这个问题触发了我将立即开始研究它。希望在一周内把东西放在一起。敬请期待!

编辑

该工具的原型版本现已在 https://bitbucket.org/leon_manukyan/trit 上提供。我建议克隆存储库并 运行ning 演示流程。


1 此语句的更通用版本适用于更广泛的 systems/situations(通常都与 security/safety 相关):

A system designed against certain events must be routinely tested against such events, otherwise it is prone to degradation down to complete inability to react against the events of interest.

举个例子 - 你家里有火警系统吗?你上次是什么时候看到它起作用的?如果它在着火时也保持沉默怎么办?马上去房间里抽根烟!

2 在此方法的范围内,类似 bug 的后门(例如,当功能行为不当时 仅当 传入URL等于https://www.formatmyharddrive.com/?confirm=yesofcourse)不合理

一个快速的解决方案是提供一个检查正确性的 Mixin:

import unittest


class Mixin(object):
    def assertTrue(self, *args, **kwargs):
        if len(args) > 1:
            # TypeError is just an example, it could also do some warning/logging
            # stuff in here.
            raise TypeError('msg should be given as keyword parameter.')
        super().assertTrue(*args, **kwargs)


class TestMixin(Mixin, unittest.TestCase):  # Mixin before other parent classes
    def test_two_things_equal(self):
        self.assertTrue("a", "b")

Mixin 还可以检查传递的 expression 是否为布尔值:

class Mixin(object):
    def assertTrue(self, *args, **kwargs):
        if type(args[0]) is bool:
            raise TypeError('expression should be a boolean')
        if len(args) > 1:
            raise TypeError('msg should be given as keyword parameter.')
        super().assertTrue(*args, **kwargs)

但是这不是静态的,它需要手动更改您的测试 classes(添加 Mixin)和 运行 测试。它还会抛出很多误报,因为将消息作为关键字参数传递并不常见(至少在我见过它的地方)并且在很多情况下你想检查表达式的隐式真实性而不是明确的 bool。喜欢在 alistdict 等时检查非空 if a

您还可以使用一些 setUpteardown 代码来改变特定 class:

assertTrue 方法
import unittest


def decorator(func):
    def wrapper(*args, **kwargs):
        if len(args) > 1:
            raise TypeError()
        return func(*args, **kwargs)
    return wrapper


class TestMixin(unittest.TestCase):
    def setUp(self):
        self._old = self.assertTrue
        self.assertTrue = decorator(self.assertTrue)

    def tearDown(self):
        self.assertTrue = self._old

    def test_two_things_equal(self):
        self.assertTrue("a", "b")

但在应用任何这些方法之前请注意:在更改现有测试之前始终要小心。不幸的是,有时测试的文档记录很差,因此他们测试什么以及如何测试并不总是很明显。有时一个测试没有意义并且改变它是安全的,但有时它以一种奇怪的方式测试一个特定的特性并且当你改变它时你改变了正在测试的东西。因此,至少要确保在更改测试用例时覆盖范围没有变化。如有必要,请确保通过更新方法名称、方法文档或内联注释来阐明测试的目的。

解决此类问题的一种方法是使用"mutation testing"。这个想法是通过在代码中引入小的更改来自动生成 "mutants" 代码。然后你的测试套件 运行 针对这些突变体,如果它是好的,他们中的大多数应该被杀死,这意味着你的测试套件检测到突变并且测试失败。

突变测试实际上是评估测试的质量。在你的例子中,没有突变体会被杀死,你会很容易地检测到测试有问题。

在python中,有几个突变框架可用: