您正在查看: Python 分类下的文章

练习 47: 自动化测试

练习 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 将会打印出一个错误信息,这样你就可以找到出错的地方并且修正过来。

测试指南

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

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

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

    OK

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

    加分习题

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

    Table Of Contents

    • 练习 47: 自动化测试
      • 撰写测试用例
      • 测试指南
      • 你应该看到的结果
      • 加分习题

    Previous topic

    习题 46: 一个项目骨架

    Next topic

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

    This Page

    • Show Source

    Quick search

    Enter search terms or a module, class or function name.

    下一步

    下一步

    现在还不能说你是一个程序员。这本书的目的相当于给你一个“编程棕带”。你已经了解了足够的编程基础,并且有能力阅读别的编程书籍了。读完这本书,你应该已经掌握了一些学习的方法,并且具备了该有的学习态度,这样你在阅读其他 python 书籍时也许会更顺利,而且能学到更多东西。

    在 http://learnpythonthehardway.org/ 网站列出了一些你可以进一步阅读的免费书籍,试着阅读它们,看看自己可以走多远。

    或许,你现在已经可以开始鼓捣一些程序出来了。如果你手上有需要解决的问题,试着写个程序解决一下。你一开始写的东西可能很挫,不过这没有关系。以我为例,我在学每一种语言的初期都是很挫的。没有哪个初学者能写出完美的代码来,如果有人告诉你他有这本事,那他只是在厚着脸皮撒谎而已。

    最后,记住学习编程是要投入时间的,你可能需要至少每天晚上练习几个小时。顺便告诉你,当你每晚学习 Python 的时候,我在努力学习弹吉他。我每天练习2 到 4 小时,而且还在学习基本的音阶。

    每个人都是某一方面的菜鸟。

    Project Versions

    Previous topic

    习题 52: 把你会的教给别人

    Next topic

    老程序员的建议

    This Page

    • Show Source

    Quick search

    Enter search terms or a module, class or function name.

    老程序员的建议

    老程序员的建议

    你已经完成了这本书而且打算继续编程。也许这会成为你的一门职业,也许你只是作为业余爱好玩玩。无论如何,你都需要一些建议以保证你在正确的道路上继续前行,并且让这项新的爱好为你带来最大程度的享受。

    我从事编程已经太长时间,长到对我来说编程已经是非常乏味的事情了。我写这本书的时候,已经懂得大约 20 种编程语言,而且可以在大约一天或者一个星期内学会一门编程语言(取决于这门语言有多古怪)。现在对我来说编程这件事情已经很无聊,已经谈不上什么兴趣了。

    在这么久的旅程下来我的体会是:编程语言这东西并不重要,重要的是你用这些语言做的事情。事实上我一直知道这一点,不过以前我会周期性地被各种编程语言分神而忘记了这一点。现在我是永远不会忘记这一点了,你也不应该忘记这一点。

    你学到和用到的编程语言并不重要。不要被围绕某一种语言的宗教把你扯进去,这只会让你忘掉了语言的真正目的,也就是作为你的工具来实现有趣的事情。

    编程作为一项智力活动,是唯一一种能让你创建交互式艺术的艺术形式。你可以创建项目让别人使用,而且你可以间接地和使用者沟通。没有其他的艺术形式能做到如此程度的交互性。电影领着观众走向一个方向,绘画是不会动的。而代码却是双向互动的。

    编程作为一项职业只是一般般有趣而已。编程可能是一份好工作,但如果你想赚更多的钱而且过得更快乐,你其实开一间快餐分店就可以了。你最好的选择是将你的编程技术作为你其他职业的秘密武器。

    技术公司里边会编程的人多到一毛钱一打,根本得不到什么尊敬。而在生物学、医药学、政府部门、社会学、物理学、数学等行业领域从事编程的人就能得到足够的尊敬,而且你可以使用这项技能在这些领域做出令人惊异的成就。

    当然,所有的这些建议都是没啥意义的。如果你跟着这本书学习写软件而且觉得很喜欢这件事情的话,那你完全可以将其当作一门职业去追求。你应该继续深入拓展这个近五十年来极少有人探索过的奇异而美妙的智力工作领域。若能从中得到乐趣当然就更好了。

    最后我要说的是学习创造软件的过程会改变你而让你与众不同。不是说更好或更坏,只是不同了。你也许会发现因为你会写软件而人们对你的态度有些怪异,也许会用“怪人”这样的词来形容你。也许你会发现因为你会戳穿他们的逻辑漏洞而他们开始讨厌和你争辩。甚至你可能会发现有人因为你懂得计算机怎么工作而觉得你是个讨厌的怪人。

    对于这些我只有一个建议: 让他们去死吧。这个世界需要更多的怪人,他们知道东西是怎么工作的而且喜欢找到答案。当他们那样对你时,只要记住这是你的旅程,不是他们的。“与众不同”不是谁的错,告诉你“与众不同是一种错”的人只是嫉妒你掌握了他们做梦都不能想到的技能而已。

    你会编程。他们不会。这真他妈的酷。

    Project Versions

    Previous topic

    下一步

    This Page

    • Show Source

    Quick search

    Enter search terms or a module, class or function name.

    习题 49: 创建句子

    习题 49: 创建句子

    从我们这个小游戏的词汇扫描器中,我们应该可以得到类似下面的列表:

    >>> from ex48 import lexicon
    >>> print lexicon.scan("go north")
    [('verb', 'go'), ('direction', 'north')]
    >>> print lexicon.scan("kill the princess")
    [('verb', 'kill'), ('stop', 'the'), ('noun', 'princess')]
    >>> print lexicon.scan("eat the bear")
    [('verb', 'eat'), ('stop', 'the'), ('noun', 'bear')]
    >>> print lexicon.scan("open the door and smack the bear in the nose")
    [('error', 'open'), ('stop', 'the'), ('noun', 'door'), ('error', 'and'),
    ('error', 'smack'), ('stop', 'the'), ('noun', 'bear'), ('stop', 'in'),
    ('stop', 'the'), ('error', 'nose')]
    >>>
    

    现在让我们把它转化成游戏可以使用的东西,也就是一个 Sentence 类。

    如果你还记得学校学过的东西的话,一个句子是由这样的结构组成的:

    主语(Subject) + 谓语(动词 Verb) + 宾语(Object)

    很显然实际的句子可能会比这复杂,而你可能已经在英语的语法课上面被折腾得够呛了。我们的目的,是将上面的元组列表转换为一个 Sentence 对象,而这个对象又包含主谓宾各个成员。

    匹配(Match)和窥视(Peek)

    为了达到这个效果,你需要四样工具:

  • 循环访问元组列表的方法,这挺简单的。
  • 匹配我们的主谓宾设置中不同种类元组的方法。
  • 一个“窥视”潜在元组的方法,以便做决定时用到。
  • 跳过(skip)我们不在乎的内容的方法,例如形容词、冠词等没有用处的词汇。
  • 我们使用 peek 函数来查看元组列表中的下一个成员,做匹配以后再对它做下一步动作。让我们先看看这个 peek 函数:

    def peek(word_list):
     if word_list:
      word = word_list[0]
      return word[0]
     else:
      return None
    

    很简单。再看看 match 函数:

    def match(word_list, expecting):
     if word_list:
      word = word_list.pop(0)
    

    if word[

    还是很简单,最后我们看看 skip 函数:

    def skip(word_list, word_type):
     while peek(word_list) == word_type:
      match(word_list, word_type)
    

    以你现在的水平,你应该可以看出它们的功能来。确认自己真的弄懂了它们。

    句子的语法

    有了工具,我们现在可以从元组列表来构建句子(Sentence)对象了。我们的处理流程如下:

  • 使用 peek 识别下一个单词。
  • 如果这个单词和我们的语法匹配,我们就调用一个函数来处理这部分语法。假设函数的名字叫 parse_subject 好了。
  • 如果语法不匹配,我们就 raise 一个错误,接下来你会学到这方面的内容。
  • 全部分析完以后,我们应该能得到一个 Sentence 对象,然后可以将其应用在我们的游戏中。
  • 演示这个过程最简单的方法是把代码展示给你让你阅读,不过这节习题有个不一样的要求,前面是我给你测试代码,你照着写出程序来,而这次是我给你的程序,而你要为它写出测试代码来。

    以下就是我写的用来解析简单句子的代码,它使用了 ex48.lexicon 这个模组。

    class ParserError(Exception):
     pass
    

    class Sentence(object):

    def init(self, subject, verb, object):

    remember we take ('noun','princess') tuples and convert them

    self.subject = subject[1]
    self.verb = verb[1]
    self.object = object[1]

    def peek(word_list):
    if word_list:
    word = word_list[

    def match(word_list, expecting):
    if word_list:
    word = word_list.pop(0)

    if word[

    def skip(word_list, word_type):
    while peek(word_list) == word_type:
    match(word_list, word_type)

    def parse_verb(word_list):
    skip(word_list, 'stop')

    if peek(word_list) == 'verb':
    return match(word_list, 'verb')
    else:
    raise ParseError("Expected a verb next.")

    def parse_object(word_list):
    skip(word_list, 'stop')
    next = peek(word_list)

    if next == 'noun':
    return match(word_list, 'noun')
    if next == 'direction':
    return match(word_list, 'direction')
    else:
    raise ParseError("Expected a noun or direction next.")

    def parse_subject(word_list, subj):
    verb = parse_verb(word_list)
    obj = parse_object(word_list)

    return Sentence(subj, verb, obj)

    def parse_sentence(word_list):
    skip(word_list, 'stop')

    start = peek(word_list)

    if start == 'noun':
    subj = match(word_list, 'noun')
    return parse_subject(word_list, subj)
    elif start == 'verb':

    assume the subject is the player then

    return parse_subject(word_list, ('noun', 'player'))
    else:
    raise ParserError("Must start with subject, object, or verb not: %s" % start)

    关于异常(Exception)

    你已经简单学过关于异常的一些东西,但还没学过怎样抛出(raise)它们。这节的代码演示了如何 raise。首先在最前面,你要定义好 ParserException 这个类,而它又是 Exception 的一种。另外要注意我们是怎样使用 raise 这个关键字来抛出异常的。

    你的测试代码应该也要测试到这些异常,这个我也会演示给你如何实现。

    你应该测试的东西

    为《习题 49》写一个完整的测试方案,确认代码中所有的东西都能正常工作,其中异常的测试——输入一个错误的句子它会抛出一个异常来。

    使用 assert_raises 这个函数来检查异常,在 nose 的文档里查看相关的内容,学着使用它写针对“执行失败”的测试,这也是测试很重要的一个方面。从 nose 文档中学会使用 assert_raises,以及一些别的函数。

    写完测试以后,你应该就明白了这段程序的工作原理,而且也学会了如何为别人的程序写测试代码。 相信我,这是一个非常有用的技能。

    加分习题

  • 修改 parse_ 函数(方法),将它们放到一个类里边,而不仅仅是独立的方法函数。这两种程序设计你喜欢哪一种呢?
  • 提高 parser 对于错误输入的抵御能力,这样即使用户输入了你预定义语汇之外的词语,你的程序也能正常运行下去。
  • 改进语法,让它可以处理更多的东西,例如数字。
  • 想想在游戏里你的 Sentence 类可以对用户输入做哪些有趣的事情。
  • Project Versions

    Table Of Contents

    • 习题 49: 创建句子
      • 匹配(Match)和窥视(Peek)
      • 句子的语法
      • 关于异常(Exception)
      • 你应该测试的东西
      • 加分习题

    Previous topic

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

    Next topic

    习题 50: 你的第一项工作任务

    This Page

    • Show Source

    Quick search

    Enter search terms or a module, class or function name.

    习题 50: 你的第一项工作任务

    习题 50: 你的第一项工作任务

    现在我将为你分配一项工作任务,就跟给专业程序员分配工作任务一样。这个任务将是一个 小游戏,假设我是客户,我把要求一一列给你,你把这个游戏写出来,如果你完成的不错, 我也许会买你的游戏。你要把我的一些模糊的描述变成一样真正能用的东西。

    这节练习的目的是看你是否已经掌握了你学过的所有概念。还有两节练习这本书就结束了, 这节习题是你的最后一个代码练习作业。

    回顾你学过的内容

    到现在为止,你应该已经学会了下面的东西:

    • 创建“类(class)”,用类实现房间的构建。
    • 抛出异常(Throw/Raise Exceptions)。
    • 使用函数(function)、变量(variable)、字典(dictionary)、列表(list)、以及元组(tuple)。
    • 使用词汇扫描器,将用户的输入转换为元组列表。
    • 使用解析器(parser)将元组列表转换为一个句子(Sentence)对象。

    你在任务中会用到这些知识,因此你要确认学会了这些东西。

    功能列表的实现

    软件工作中特征性的一种情况,就是有人给你一份模糊不清前后矛盾的功能需求。实话实说,这种事情很挫。一般人没有能力可以清晰地构想出他的需求来,说到描述需求的能力,那就更差了。

    作为一个程序员,你的任务就是接受这些模糊的描述,利用这些描述创造出他们想要的东西来。其实有的时候,连你自己都说不清你需要什么。

    实现功能列表的最好的方法,就是照着下面的样板流程来:

  • 让别人把所有的关于需求的想法都告诉你,整个过程中不要去评判这些想法。
  • 一边倾听,一边把所有这些东西写下来,你可以放到表单里,也可以写在索引卡上。
  • 休息一会,把所有的功能需求整理到一起,为它们设立优先等级,一种是“必须有”的功能, 一种是“有了比较好”的功能。
  • 将列表的内容归类排序,将“必须功能”和“可选功能”区分开。
  • 选择一个必须功能,开始将其实现。
  • 过大约一个星期以后,把你写的软件展示给提出需求的人看,得到他们的反馈信息,依据 他们的反馈修改功能列表,并重复前面的步骤。
  • 在各种各样的“编程方法论”中,你都可以看到这个样板流程的身影。

    接下来我会给你一份功能列表,你需要在游戏中实现它们。我给它们的优先度都是“必须”, 你的任务是花一个星期实现这份列表上提到的所有功能。

    功能列表

    • 这个游戏的发生地点是外星人的宇宙飞船。
    • 游戏中应该有一个人类英雄,他的任务是逃离外星人的奴役。
    • 这因该是一个文字类的冒险游戏,和我们的“熊和阔剑”的游戏类似。我就喜欢这样的游戏。
    • 哦,激光。激光武器是必不可少的。
    • 激光武器也不能太厉害了。游戏的难度应该比较高才对。
    • 不过也不能太难了,不然大家就没兴趣了嘛。你懂的,《魔兽世界》你知道吧,听说这游戏的开发商可赚钱了。
    • 飞船一开始应该有大约 10 个房间,接下来我们还要扩展它们。
    • 我想随便修改房间的描述,可我又不想改代码,这个你能做吗?
    • 有朋友告诉我农场游戏很流行,你就做一个有农场的场景出来吧。
    • 外星人应该有类似黑手党的性质,这年头黑手党也是流行话题,是吧?
    • 我应该可以使用句子在游戏中移动位置,例如“go north”、“open door”等。
    • 在代码中设计好房间和路径是没问题的,不过玩家需要一份打印出来的地图,你的游戏能提供地图查询吗?
    • 游戏应该有个故事背景,最好是爱情相关的。
    • 最好包含点 geek 文化在里边,不过别提《星际迷航(Star Trek)》!好了,我承认第一季不错,但后面都是垃圾。
    • 我要在我的电脑上安装这个游戏,你能不能让它通过 setup.py 安装起来呢?这样以后打包发行也比较容易。

    完成任务列表的小技巧

  • 先在纸上设计出地图、人物角色、以及故事主线。
  • 撰写单元测试,这样你后面就无需手动检查代码了。
  • 设计你的游戏引擎,让它便于测试。
  • 每隔一段时间,自己亲自玩一下你写的游戏。
  • 如果遇到疑惑,就向别人寻求帮助,并把你的代码给别人看。
  • 祝你好运,下周评审游戏时再见。

    Project Versions

    Table Of Contents

    • 习题 50: 你的第一项工作任务
      • 回顾你学过的内容
      • 功能列表的实现
      • 功能列表
      • 完成任务列表的小技巧

    Previous topic

    习题 49: 创建句子

    Next topic

    习题 51: 评审你的游戏

    This Page

    • Show Source

    Quick search

    Enter search terms or a module, class or function name.