习题 47: 自动化测试

为了确认游戏的功能是否正常,你需要一遍一遍地在你的游戏中输入命令。这个过程是很枯燥无味的。如果能写一小段代码用来测试你的代码岂不是更好?然后只要你对程序做了任何修改,或者添加了什么新东西,你只要“跑一下你的测试”,而这些测试能确认程序依然能正确运行。这些自动测试不会抓到所有的 bug,但可以让你无需重复输入命令运行你的代码,从而为你节约很多时间。

从这一章开始,以后的练习将不会有“你应该看到的结果”这一节,取而代之的是一个“你应该测试的东西”一节。从现在开始,你需要为自己写的所有代码写自动化测试,而这将让你成为一个更好的程序员。

我不会试图解释为什么你需要写自动化测试。我要告诉你的是,你想要成为一个程序员,而程序的作用是让无聊冗繁的工作自动化,测试软件毫无疑问是无聊冗繁的,所以你还是写点代码让它为你测试的更好。

这应该是你需要的所有的解释了。因为你写单元测试的原因是让你的大脑更加强健。你读了这本书,写了很多代码让它们实现一些事情。现在你将更进一步,写出懂得你写的其他代码的代码。这个写代码测试你写的其他代码的过程将强迫你清楚的理解你之前写的代码。这会让你更清晰地了解你写的代码实现的功能及其原理,而且让你对细节的注意更上一个台阶。

撰写测试用例

我们将拿一段非常简单的代码为例,写一个简单的测试,这个测试将建立在上节我们创建的项目骨架上面。

首先从你的项目骨架创建一个叫做 ex47 的项目。确认该改名称的地方都有改过,尤其是 tests/ex47_tests.py 这处不要写错,另外运行 nosetest 确认一下没有错误信息。检查一下 tests/skel_tests.pyc 这个文件,有的话就把它删掉,这一点需要尤其注意。

接下来创建一个简单的 ex47/game.py 文件,里边放一些用来被测试的代码。我们现在放一个傻乎乎的小 class 进去,用来作为我们的测试对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Room(object):

    def __init__(self, name, description):
        self.name = name
        self.description = description
        self.paths = {}

    def go(self, direction):
        return self.paths.get(direction, None)

    def add_paths(self, paths):
        self.paths.update(paths)

准备好了这个文件,接下来把测试骨架改成这样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from nose.tools import *
from ex47.game import Room


def test_room():
    gold = Room("GoldRoom", 
                """This room has gold in it you can grab. There's a
                door to the north.""")
    assert_equal(gold.name, "GoldRoom")
    assert_equal(gold.paths, {})

def test_room_paths():
    center = Room("Center", "Test room in the center.")
    north = Room("North", "Test room in the north.")
    south = Room("South", "Test room in the south.")

    center.add_paths({'north': north, 'south': south})
    assert_equal(center.go('north'), north)
    assert_equal(center.go('south'), south)
    
def test_map():
    start = Room("Start", "You can go west and down a hole.")
    west = Room("Trees", "There are trees here, you can go east.")
    down = Room("Dungeon", "It's dark down here, you can go up.")

    start.add_paths({'west': west, 'down': down})
    west.add_paths({'east': start})
    down.add_paths({'up': start})

    assert_equal(start.go('west'), west)
    assert_equal(start.go('west').go('east'), start)
    assert_equal(start.go('down').go('up'), start)

这个文件 import 了你在 ex47.game 创建的 Room 这个类,接下来我们要做的就是测试它。于是我们看到一系列的以 test_ 开头的测试函数,它们就是所谓的“测试用例(test case)”,每一个测试用例里面都有一小段代码,它们会创建一个或者一些房间,然后去确认房间的功能和你期望的是否一样。它测试了基本的房间功能,然后测试了路径,最后测试了整个地图。

这里最重要的函数时 assert_equal,它保证了你设置的变量,以及你在 Room 里设置的路径和你的期望相符。如果你得到错误的结果的话, nosetests 将会打印出一个错误信息,这样你就可以找到出错的地方并且修正过来。

测试指南

在写测试代码时,你可以照着下面这些不是很严格的指南来做:

  1. 测试脚本要放到 tests/ 目录下,并且命名为 BLAH_tests.py ,否则 nosetests 就不会执行你的测试脚本了。这样做还有一个好处就是防止测试代码和别的代码互相混掉。
  2. 为你的每一个模组写一个测试。
  3. 测试用例(函数)保持简短,但如果看上去不怎么整洁也没关系,测试用例一般都有点乱。
  4. 就算测试用例有些乱,也要试着让他们保持整洁,把里边重复的代码删掉。创建一些辅助函数来避免重复的代码。当你下次在改完代码需要改测试的时候,你会感谢我这一条建议的。重复的代码会让修改测试变得很难操作。
  5. 最后一条是别太把测试当做一回事。有时候,更好的方法是把代码和测试全部删掉,然后重新设计代码。

你应该看到的结果

~/projects/simplegame $ nosetests
...
----------------------------------------------------------------------
Ran 3 tests in 0.007s

OK

如果一切工作正常的话,你看到的结果应该就是这样。试着把代码改错几个地方,然后看错误信息会是什么,再把代码改正确。

加分习题

  1. 仔细读读 nosetest 相关的文档,再去了解一下其他的替代方案。
  2. 了解一下 Python 的 “doc tests” ,看看你是不是更喜欢这种测试方式。
  3. 改进你游戏里的 Room,然后用它重建你的游戏,这次重写,你需要一边写代码,一边把单元测试写出来。

常见问题回答

运行 nosetests 时出现语法错误(SyntaxError)。

看看错误信息的具体内容,把对应哪行的语法错误改正过来。 nosetests 这类工具会运行 你写的程序代码以及测试代码,所以和 python 一样,它也会找出你的语法错误。

无法 import ex47.game

确认你创建了 ex47/__init__.py 文件,回到前面的内容看看如何创建。

运行 nosetests 时看到 UserWarning

你也许装了两个版本的 Python,或者你不是用的 distribute,回去照着《习题 46》装一下 distribute or pip 就可以了。

Project Versions

Table Of Contents

Previous topic

习题 46: 一个项目骨架

Next topic

习题 48: 更复杂的用户输入

This Page