第28章-测试简介

python包含两个用于测试代码的内置模块。这两种方法被称为 doctest单元测试 .我们来看看怎么用 doctest 首先,在第二部分中,我们将介绍使用测试驱动开发技术的单元测试。

用doctest测试

doctest模块将在代码中搜索类似于交互式Python会话的文本片段。然后,它将执行这些会话以验证它们的工作方式是否与所写的完全一致。这意味着,如果在显示带有尾随空格或制表符的输出的docstring中编写了一个示例,那么函数的实际输出也必须具有尾随空格。大多数情况下,docstring是您想要放置测试的地方。将涵盖doctest的以下方面:

  • 如何从终端运行doctest

  • 如何在模块内使用doctest

  • 如何从单独的文件运行doctest

我们开始吧!

通过终端运行doctest

我们将从创建一个真正简单的函数开始,这个函数将使赋予它的值加倍。我们将在函数的 文档字符串 .这是代码(请确保并将其保存为“dtest1.py”):

# dtest1.py

def double(a):
    """
    >>> double(4)
    8
    >>> double(9)
    18
    """
    return a*2

现在我们只需要在 doctest .打开终端(或命令行)并将目录更改为包含脚本的文件夹。以下是我的截图:

_images/doctest.jpg

您会注意到,在第一个示例中,我执行了以下操作:

python -m doctest dtest1.py

测试完成后,屏幕上没有显示任何内容。当您没有看到任何打印内容时,这意味着所有测试都成功通过。第二个示例显示以下命令:

python -m doctest -v dtest1.py

“-v”意味着我们需要详细的输出,这正是我们收到的结果。再次打开代码,在docstring中的“18”后面添加一个空格。然后重新运行测试。这是我收到的输出:

_images/doctest_error.jpg

错误消息说它预期为“18”,得到“18”。这是怎么回事?我们在docstring的“18”后面加了一个空格,所以doctest实际上希望数字“18”后面跟一个空格。还要注意在docstring示例中,将字典作为输出。字典可以是任意顺序的,因此它与实际输出匹配的可能性不是很好。

在模块内运行doctest

让我们稍微修改一下示例,以便导入 doctest 模块和使用其 测试模型 功能。

def double(a):
    """
    >>> double(4)
    8
    >>> double(9)
    18
    """
    return a*2

if __name__ == "__main__":
    import doctest
    doctest.testmod(verbose=True)

这里我们进口 doctest 并打电话 doctest.testmod .我们把关键字参数传递给它 verbose=True 这样我们可以看到一些输出。否则,该脚本将在没有任何输出的情况下运行,这意味着测试运行成功。

如果不想硬编码verbose选项,也可以在命令行上执行:

python dtest2.py -v

现在我们准备学习如何将测试放入单独的文件中。

从单独的文件运行doctest

doctest模块还支持将测试放入单独的文件中。这允许我们将测试与代码分开。让我们从上一个示例中剥离测试,并将它们放入名为 tests.txt

The following are tests for dtest2.py

>>> from dtest2 import double
>>> double(4)
8
>>> double(9)
18

让我们在命令行上运行这个测试文件。方法如下:

_images/doctest_from_file.jpg

您将注意到,用文本文件调用doctest的语法与用python文件调用它的语法相同。结果也一样。在本例中,有三个测试而不是两个,因为我们还导入了一个模块。您还可以在Python解释器中运行文本文件中的测试。下面是一个例子:

_images/doctest_from_file_intepreter.jpg

这里我们只是进口 doctest 并调用它的 测试文件 方法。请注意,还需要将文件名或路径传递给 测试文件 功能。它将返回 TestResults 对象,该对象包含尝试的测试数和失败的测试数。

使用UnitTest进行测试驱动开发

在本节中,您将了解使用Python的内置UnitTest模块进行测试驱动开发(TDD)。我要感谢马特和亚伦的帮助,他们向我展示了TDD在现实世界中的工作原理。为了演示TDD的概念,我们将介绍如何在python中打保龄球。如果你还不知道保龄球的规则,你可以用谷歌来查找。一旦你知道了规则,现在是时候写一些测试了。如果您不知道,测试驱动开发背后的想法是在编写实际代码之前编写测试。在本章中,我们将编写一个测试,然后编写一些代码来通过测试。我们将在编写测试和代码之间来回迭代,直到完成为止。对于这一章,我们只写三个测试。我们开始吧!

第一次测试

我们的第一个测试将是测试我们的游戏对象,看看它是否能计算出正确的总数,如果我们滚动11次,每次只敲过一个别针。这应该给我们总共十一个。

import unittest

class TestBowling(unittest.TestCase):
    """"""

    def test_all_ones(self):
        """Constructor"""
        game = Game()
        game.roll(11, 1)
        self.assertEqual(game.score, 11)

这是一个非常简单的测试。我们创建一个游戏对象,然后调用它 roll 方法11次,每次1分。然后我们使用 断言相等 方法从 单元测试 测试游戏对象得分是否正确(即11分)的模块。下一步是编写最简单的代码来通过测试。下面是一个例子:

class Game:
    """"""

    def __init__(self):
        """Constructor"""
        self.score = 0

    def roll(self, numOfRolls, pins):
        """"""
        for roll in numOfRolls:
            self.score += pins

为了简单起见,您可以用测试将其复制并粘贴到同一个文件中。我们将把它们分成两个文件进行下一次测试。不管怎样,正如你所看到的,我们的 Game 课程非常简单。通过考试只需要分数属性和 roll 可以更新它的方法。

让我们运行测试,看看它是否通过!运行测试的最简单方法是将以下两行代码添加到文件底部:

if __name__ == '__main__':
    unittest.main()

然后只需通过命令行运行python文件,如果这样做,您应该得到如下内容:

E
======================================================================
ERROR: test_all_ones (__main__.TestBowling)
Constructor
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\Mike\Documents\Scripts\Testing\bowling\test_one.py",
 line 27, in test_all_ones
    game.roll(11, 1)
  File "C:\Users\Mike\Documents\Scripts\Testing\bowling\test_one.py",
 line 15, in roll
    for roll in numOfRolls:
TypeError: 'int' object is not iterable

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

哎呀!我们在某个地方出错了。看起来我们正在传递一个整数,然后尝试对其进行迭代。那不行!我们需要将游戏对象的滚动方法更改为以下方法以使其生效:

def roll(self, numOfRolls, pins):
    """"""
    for roll in range(numOfRolls):
        self.score += pins

如果现在运行测试,则应获得以下信息:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

注意“.”因为它很重要。这个小点意味着一个测试已经运行并且通过了。最后的“OK”也提示你了解这个事实。如果你研究原始的输出,你会发现它会以“e”开头,表示错误,并且没有点!让我们继续测试2。

第二次测试

在第二个测试中,我们将测试当我们受到攻击时会发生什么。不过,我们需要更改第一个测试,以使用每个帧中被击倒的管脚数量的列表,所以我们将在这里查看这两个测试。您可能会发现这是一个相当常见的过程,由于您要测试的内容发生了根本性的变化,您可能需要编辑一些测试。通常情况下,这只会发生在编码的开始,稍后您会变得更好,这样您就不需要这样做了。因为这是我第一次这么做,所以我想的不够远。不管怎样,让我们看一下代码:

from game import Game
import unittest

class TestBowling(unittest.TestCase):
    """"""

    def test_all_ones(self):
        """Constructor"""
        game = Game()
        pins = [1 for i in range(11)]
        game.roll(11, pins)
        self.assertEqual(game.score, 11)

    def test_strike(self):
        """
        A strike is 10 + the value of the next two rolls. So in this case
        the first frame will be 10+5+4 or 19 and the second will be
        5+4. The total score would be 19+9 or 28.
        """
        game = Game()
        game.roll(11, [10, 5, 4])
        self.assertEqual(game.score, 28)

if __name__ == '__main__':
    unittest.main()

让我们来看看我们的第一个测试以及它是如何变化的。是的,在TDD方面,我们有点违反了规则。不要更改第一个测试,看看有什么中断。在 test_all_ones 方法,我们设置 pins 变量等于创建了11个列表的列表理解。然后我们把它传给我们的 game 对象的 roll 方法以及卷数。

在第二个测试中,我们在第一个测试中打出一个好球,在第二个测试中打出五个,在第三个测试中打出四个。注意,我们走了一个头,告诉它,我们要经过11卷,但我们只通过了3卷。这意味着我们需要将其他八个滚动设置为零。接下来,我们用我们的信任 断言相等 方法检查我们是否得到正确的总数。最后,请注意,我们现在正在导入 Game 类而不是将其与测试保持在一起。现在我们需要实现通过这两个测试所必需的代码。让我们来看一个可能的解决方案:

class Game:
    """"""

    def __init__(self):
        """Constructor"""
        self.score = 0
        self.pins = [0 for i in range(11)]

    def roll(self, numOfRolls, pins):
        """"""
        x = 0
        for pin in pins:
            self.pins[x] = pin
            x += 1
        x = 0
        for roll in range(numOfRolls):
            if self.pins[x] == 10:
                self.score = self.pins[x] + self.pins[x+1] + self.pins[x+2]
            else:
                self.score += self.pins[x]
            x += 1
        print(self.score)

马上,您会注意到我们有一个新的类属性,名为 self.pins 它保存了默认的pin列表,即11个零。然后在我们的 roll 方法,在第一个循环的self.pins列表中,将正确的分数添加到正确的位置。然后在第二个循环中,我们检查撞倒的销是否等于10。如果是这样,我们加上它和接下来的两个分数来得分。否则,我们会像以前那样做。在方法的最后,我们打印出分数来检查它是否是我们所期望的。此时,我们已经准备好编写最终测试的代码了。

第三次(也是最后一次)测试

我们的最后一个测试将测试如果有人投了一个空位会得到正确的分数。测试很容易,解决方案稍微困难一些。当我们在做这件事时,我们将对测试代码进行一点重构。像往常一样,我们先看一下测试。

from game_v2 import Game
import unittest

class TestBowling(unittest.TestCase):
    """"""

    def setUp(self):
        """"""
        self.game = Game()

    def test_all_ones(self):
        """
        If you don't get a strike or a spare, then you just add up the
        face value of the frame. In this case, each frame is worth
        one point, so the total is eleven.
        """
        pins = [1 for i in range(11)]
        self.game.roll(11, pins)
        self.assertEqual(self.game.score, 11)

    def test_spare(self):
        """
        A spare is worth 10, plus the value of your next roll. So in this
        case, the first frame will be 5+5+5 or 15 and the second will be
        5+4 or 9. The total is 15+9, which equals 24,
        """
        self.game.roll(11, [5, 5, 5, 4])
        self.assertEqual(self.game.score, 24)

    def test_strike(self):
        """
        A strike is 10 + the value of the next two rolls. So in this case
        the first frame will be 10+5+4 or 19 and the second will be
        5+4. The total score would be 19+9 or 28.
        """
        self.game.roll(11, [10, 5, 4])
        self.assertEqual(self.game.score, 28)

if __name__ == '__main__':
    unittest.main()

首先,我们添加了 设置 方法,将为每个测试为我们创建self.game对象。如果我们访问一个数据库或类似的东西,我们可能也有一个关闭连接、文件或类似的东西的下拉方法。它们分别在每个测试的开始和结束时运行,如果它们存在的话。这个 test_all_onestest_strike 测试基本上是相同的,除了他们现在正在使用“self.game”。唯一的新测试是 test_spare .docstring解释了备件是如何工作的,代码只有两行。是的,你可以想出来。让我们来看看我们需要通过这些测试的代码:

# game_v2.py

class Game:
    """"""

    def __init__(self):
        """Constructor"""
        self.score = 0
        self.pins = [0 for i in range(11)]

    def roll(self, numOfRolls, pins):
        """"""
        x = 0
        for pin in pins:
            self.pins[x] = pin
            x += 1
        x = 0
        spare_begin = 0
        spare_end = 2
        for roll in range(numOfRolls):
            spare = sum(self.pins[spare_begin:spare_end])
            if self.pins[x] == 10:
                self.score = self.pins[x] + self.pins[x+1] + self.pins[x+2]
            elif spare == 10:
                self.score = spare + self.pins[x+2]
                x += 1
            else:
                self.score += self.pins[x]
            x += 1
            if x == 11:
                break
            spare_begin += 2
            spare_end += 2
        print(self.score)

对于这部分的难题,我们在循环中添加了条件语句。为了计算备件的价值,我们使用 spare_beginspare_end 列出位置以从列表中获得正确的值,然后对它们进行汇总。这就是 备用的 变量用于。这也许更适合放在elif中,但我会把它留给读者去试验。从技术上讲,这只是空余分数的前半部分。下半部分是接下来的两个部分,您将在当前代码的elif部分的计算中找到这个部分。其余代码是相同的。

其他音符

正如您可能已经猜到的,UnitTest模块比这里介绍的要多得多。您可以使用许多其他断言来测试结果。您可以跳过测试,从命令行运行测试,使用testloader创建一个测试套件等等。一定要读完整的 documentation 当你有机会使用本教程时,几乎不会触及这个库的表面。

总结

此时,您应该能够理解如何在自己的代码中有效地使用doctest和unittest模块。您应该阅读关于这两个模块的python文档,因为您可能会发现其他选项和功能的附加信息是有用的。在编写自己的脚本时,您还知道一些如何使用测试驱动开发的概念。