26

我正在尝试提高我的 Python 项目中测试的数量和质量。随着测试数量的增加,我遇到的困难之一是知道每个测试的作用以及它应该如何帮助发现问题。我知道跟踪测试的一部分是更好的单元测试名称(已在其他地方解决),但我也有兴趣了解文档和单元测试如何结合在一起。

当这些测试在未来失败时,如何记录单元测试以提高它们的实用性?具体来说,什么是好的单元测试文档字符串?

我很欣赏描述性答案和具有出色文档的单元测试示例。虽然我只使用 Python,但我对其他语言的实践持开放态度。

4

5 回答 5

17

我专门用方法名称记录了我的单元测试:

testInitializeSetsUpChessBoardCorrectly()
testSuccessfulPromotionAddsCorrectPiece()

对于我几乎 100% 的测试用例,这清楚地解释了单元测试正在验证什么,这就是我使用的全部内容。但是,在一些更复杂的测试用例中,我将在整个方法中添加一些注释来解释几行的作用。

我以前见过一个工具(我相信它是用于 Ruby 的),它可以通过解析项目中所有测试用例的名称来生成文档文件,但我不记得这个名称了。如果您有国际象棋皇后课程的测试用例:

testCanMoveStraightUpWhenNotBlocked()
testCanMoveStraightLeftWhenNotBlocked()

该工具会生成一个 HTML 文档,其内容如下:

Queen requirements:
 - can move straight up when not blocked.
 - can move straight left when not blocked.
于 2009-11-13T01:47:04.690 回答
15

也许问题不在于如何最好地编写测试文档字符串,而在于如何编写测试本身?以自我记录的方式重构测试可以有很长的路要走,并且当代码更改时,您的文档字符串不会过时。

您可以做一些事情来使测试更清晰:

  • 清晰和描述性的测试方法名称(已经提到过)
  • 测试主体应该清晰简洁(自我记录)
  • 在方法中抽象出复杂的设置/拆卸等
  • 更多的?

例如,如果您有这样的测试:

def test_widget_run_returns_0():
    widget = Widget(param1, param2, "another param")
    widget.set_option(true)
    widget.set_temp_dir("/tmp/widget_tmp")
    widget.destination_ip = "10.10.10.99"

    return_value = widget.run()

    assert return_value == 0
    assert widget.response == "My expected response"
    assert widget.errors == None

您可以用方法调用替换设置语句:

def test_widget_run_returns_0():
    widget = create_basic_widget()
    return_value = widget.run()
    assert return_value == 0
    assert_basic_widget(widget)

def create_basic_widget():
    widget = Widget(param1, param2, "another param")
    widget.set_option(true)
    widget.set_temp_dir("/tmp/widget_tmp")
    widget.destination_ip = "10.10.10.99"
    return widget

def assert_basic_widget():
    assert widget.response == "My expected response"
    assert widget.errors == None

请注意,您的测试方法现在由一系列具有意图揭示名称的方法调用组成,这是一种特定于您的测试的 DSL。这样的测试还需要文档吗?

需要注意的另一件事是,您的测试方法主要处于一个抽象级别。阅读测试方法的人会看到算法是:

  • 创建小部件
  • 在小部件上调用 run
  • 断言代码符合我们的预期

他们对测试方法的理解并没有被设置小部件的细节所混淆,这是比测试方法低一级的抽象。

测试方法的第一个版本遵循Inline Setup模式。第二个版本遵循创建方法委托设置模式。

一般来说,我反对评论,除非他们解释了代码的“原因”。阅读 Bob Martin 叔叔的Clean Code让我相信了这一点。有一章是关于评论的,还有一章是关于测试的。我推荐它。

有关自动化测试最佳实践的更多信息,请查看xUnit 模式

于 2009-11-13T02:40:10.160 回答
4

测试方法的名称应该准确描述您正在测试的内容。文档应该说明是什么导致测试失败。

于 2009-11-13T02:00:18.103 回答
0

您应该在文档字符串中使用描述性方法名称和注释的组合。一个好的方法是在文档字符串中包含基本程序和验证步骤。然后,如果您从某种自动运行测试和收集结果的测试框架运行这些测试,您可以让框架记录每个测试方法的文档字符串的内容及其 stdout+stderr。

这是一个基本示例:

class SimpelTestCase(unittest.TestCase):
    def testSomething(self):
        """ Procedure:
            1. Print something
            2. Print something else
            ---------
            Verification:
            3. Verify no errors occurred
        """
        print "something"
        print "something else"

拥有测试程序可以更容易地弄清楚测试在做什么。而且,如果您在测试输出中包含文档字符串,那么以后查看结果时找出问题所在会变得更加容易。我以前工作的地方做过类似的事情,并且在发生故障时效果很好。我们使用 CruiseControl 在每次签到时自动运行单元测试。

于 2009-11-13T02:35:59.273 回答
0

当测试失败时(应该在它通过之前),您应该会看到错误消息并能够知道发生了什么。只有当你这样计划时才会发生这种情况。

这完全是测试类、测试方法和断言消息的命名问题。当一个测试失败时,你无法从这三个线索中判断出发生了什么,然后重命名一些东西或分解一些测试类。

如果夹具的名称是 ClassXTests 并且测试的名称是 TestMethodX 并且错误消息是“预期为真,返回假”,则不会发生这种情况。这是草率测试写作的标志。

大多数情况下,您不必阅读测试或任何评论即可知道发生了什么。

于 2009-11-13T20:01:28.150 回答