【python3小白上路系列】测试代码

【早起挑战第4天】7点,算是成功一天吧,珍惜时间。

第11章 测试代码

编写函数或类时,还可为其编写测试。通过测试,可确定代码面对各种输入都能够按要求的那样工作。测试让你信心满满,深信即便有更多的人使用你的程序,它也能正确地工作。在程序中添加新代码时,你也可以对其进行测试,确认它们不会破坏程序既有的行为。程序员都会犯错,因此每个程序员都必须经常测试其代码,在用户发现问题前找出它们。

在本章中,你将学习如何使用Python模块unittest 中的工具来测试代码。你将学习编写测试用例,核实一系列输入都将得到预期的输出。你将看到测试通过了是什么样子,测试未通过又是什么样子,还将知道测试未通过如何有助于改进代码。你将学习如何测试函数和类,并将知道该为项目编写多少个测试。

11.1 测试函数

要学习测试,得有要测试的代码。下面是一个简单的函数,它接受名和姓并返回整洁的姓名:

def get_formatted_name(first,last):
    full_name = first + ' ' + last
    return full_name.title()

函数get_formatted_name() 将名和姓合并成姓名,在名和姓之间加上一个空格,并将它们的首字母都大写,再返回结果。为核实get_formatted_name() 像期望的那样工作,我们来编写一个使用这个函数的程序。程序names.py让用户输入名和姓,并显示整洁的全名:

from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")
while True:
    first = input("\nPlease give me a first name:")
    if first == 'q':
        break
    last = input("please give me a last name:")
    if last == 'q':
        break

    formatted_name = get_formatted_name(first,last)
    print("\tNeatly formatted name: " + formatted_name + '.')

运行结果:



从上述输出可知,合并得到的姓名正确无误。现在假设我们要修改get_formatted_name() ,使其还能够处理中间名。这样做时,我们要确保不破坏这个函数处理只有名和姓的姓名的方式。为此,我们可以在每次修改get_formatted_name() 后都进行测试:运行程序names.py,并输入像Janis Joplin 这样的姓名,但这太烦琐了。所幸Python提供了一种自动测试函数输出的高效方式。倘若我们能对get_formatted_name() 进行自动测试,就能始终信心满满,确信给这个函数提供我们测试过的姓名时,它都能正确地工作。

11.1.1 单元测试和测试用例

Python标准库中的模块unittest 提供了代码测试工具。单元测试用于核实函数的某个方面没有问题;测试用例是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试。全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要实现全覆盖可能很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。

11.1.2 可通过的测试

创建测试用例的语法需要一段时间才能习惯,但测试用例创建后,再添加针对函数的单元测试就很简单了。要为函数编写测试用例,可先导入模块unittest 以及要测试的函数,再创建一个继承unittest.TestCase 的类,并编写一系列方法对函数行为的不同方面进行测试。

下面是一个只包含一个方法的测试用例,它检查函数get_formatted_name() 在给定名和姓时能否正确地工作:

import unittest
from name_function import get_formatted_name

#测试name_function.py
class NameTestCase(unittest.TestCase):
    def test_first_last_name(self):
        formatted_name = get_formatted_name('janis','joplin')
        self.assertEqual(formatted_name,'Janis Joplin')

unittest.main()

运行结果:



上述输出表明,给定包含名和姓的姓名时,函数get_formatted_name() 总是能正确地处理。

11.1.3 不能通过的测试

测试未通过时结果是什么样的呢?我们来修改get_formatted_name() ,使其能够处理中间名,但这样做时,故意让这个函数无法正确地处理像Janis Joplin这样只有名和姓的姓名。
下面是函数get_formatted_name() 的新版本,它要求通过一个实参指定中间名:

def get_formatted_name(first,middle,last):
    full_name = first + ' ' +middle + ' ' + last
    return full_name.title()

运行结果:


11.1.4 测试未通过时怎么办

测试未通过时怎么办呢?如果你检查的条件没错,测试通过了意味着函数的行为是对的,而测试未通过意味着你编写的新代码有错。因此,测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚对函数所做的修改,找出导致函数行为不符合预期的修改。

在这个示例中,get_formatted_name() 以前只需要两个实参——名和姓,但现在它要求提供名、中间名和姓。新增的中间名参数是必不可少的,这导致get_formatted_name() 的行为不符合预期。就这里而言,最佳的选择是让中间名变为可选的。这样做后,使用类似于Janis Joplin的姓名进行测试时,测试就会通过了,同时这个函数还能接受中间名。下面来修改get_formatted_name() ,将中间名设置为可选的,然后再次运行这个测试用例。如果通过了,我们接着确认这个函数能够妥善地处理中间名。

要将中间名设置为可选的,可在函数定义中将形参middle 移到形参列表末尾,并将其默认值指定为一个空字符串。我们还要添加一个if 测试,以便根据是否提供了中间名相应地创建姓名:

def get_formatted_name(first,last,middle =''):
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()

现在再运行下测试代码试试:


11.1.5 添加新测试

确定get_formatted_name() 又能正确地处理简单的姓名后,我们再编写一个测试,用于测试包含中间名的姓名。为此,我们在NamesTestCase 类中再添加一个方法:

import unittest
from name_function import get_formatted_name

#测试name_function.py
class NameTestCase(unittest.TestCase):
    def test_first_last_name(self):
        formatted_name = get_formatted_name('janis','joplin')
        self.assertEqual(formatted_name,'Janis Joplin')
    def test_first_last_middle_name(self):
        formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name,'Wolfgang Amadeus Mozart')

unittest.main()

运行结果:



太好了!现在我们知道,这个函数又能正确地处理像Janis Joplin这样的姓名了,我们还深信它也能够正确地处理像WolfgangAmadeus Mozart这样的姓名。

11.2 测试类

在本章前半部分,你编写了针对单个函数的测试,下面来编写针对类的测试。很多程序中都会用到类,因此能够证明你的类能够正确地工作会大有裨益。如果针对类的测试通过了,你就能确信对类所做的改进没有意外地破坏其原有的行为。

11.2.1 各种断言方法

Python在unittest.TestCase 类中提供了很多断言方法。前面说过,断言方法检查你认为应该满足的条件是否确实满足。如果该条件确实满足,你对程序行为的假设就得到了确认,你就可以确信其中没有错误。如果你认为应该满足的条件实际上并不满足,Python将引发异常。

表11-1描述了6个常用的断言方法。使用这些方法可核实返回的值等于或不等于预期的值、返回的值为True 或False 、返回的值在列表中或不在列表中。你只能在继承unittest.TestCase 的类中使用这些方法,下面来看看如何在测试类时使用其中的一个。

assertEqual(a, b) 核实a == b
assertNotEqual(a, b) 核实a != b
assertTrue(x) 核实x 为True
assertFalse(x) 核实x 为False
assertIn(item , list) 核实 item 在 list 中
assertNotIn(item , list) 核实 item 不在 list 中

11.2.2 一个要测试的类

类的测试与函数的测试相似——你所做的大部分工作都是测试类中方法的行为,但存在一些不同之处,下面来编写一个类进行测试。来看一个帮助管理匿名调查的类:

#收集匿名调查问卷的答案
class AnonymousSurvey():

    def __init__(self,question):
        self.question = question
        self.responses = []

    def show_question(self):
        print(self.question)

    def store_response(self, new_response):
        self.responses.append(new_response)

    def show_results(self):
        print("Survey results:")
        for response in self.responses:
            print('- ' + response)

为证明AnonymousSurvey 类能够正确地工作,我们来编写一个使用它的程序:

from survey import AnonymousSurvey

#定义一个问题,并创建一个表示调查的AnonymousSurvey对象
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)

#显示问题并存储答案
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
    response = input("Language:")
    if response == 'q':
        break
    my_survey.store_response(response)

#显示调查结果
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()

运行结果:



AnonymousSurvey 类可用于进行简单的匿名调查。假设我们将它放在了模块survey 中,并想进行改进:让每位用户都可输入多个答案;编写一个方法,它只列出不同的答案,并指出每个答案出现了多少次;再编写一个类,用于管理非匿名调查。

进行上述修改存在风险,可能会影响AnonymousSurvey 类的当前行为。例如,允许每位用户输入多个答案时,可能不小心修改了处理单个答案的方式。要确认在开发这个模块时没有破坏既有行为,可以编写针对这个类的测试。

11.2.3 测试AnonymousSurvey类

下面来编写一个测试,对AnonymousSurvey 类的行为的一个方面进行验证:如果用户面对调查问题时只提供了一个答案,这个答案也能被妥善地存储。为此,我们将在这个答案被存储后,使用方法assertIn() 来核实它包含在答案列表中:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):

    #测试单个答案会被妥善地存储
    def test_store_single_response(self):
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('English')

        self.assertIn('English',my_survey.responses)

unittest.main()

运行结果



这很好,但只能收集一个答案的调查用途不大。下面来核实用户提供三个答案时,它们也将被妥善地存储。为此,我们在TestAnonymousSurvey 中再添加一个方法:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):

    #测试单个答案会被妥善地存储
    def test_store_single_response(self):
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('English')

        self.assertIn('English',my_survey.responses)

    #测试三个答案会被妥善地存储
    def test_store_three_response(self):
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English', 'Spanish', 'Mandarin']
        for response in responses:
            my_survey.store_response(response)
        for response in responses:
            self.assertIn(response,my_survey.responses)


unittest.main()

运行结果:


11.2.4 方法setUp()

在前面的test_survey.py中,我们在每个测试方法中都创建了一个AnonymousSurvey 实例,并在每个方法中都创建了答案。unittest.TestCase 类包含方法setUp() ,让我
们只需创建这些对象一次,并在每个测试方法中使用它们。如果你在TestCase 类中包含了方法setUp() ,Python将先运行它,再运行各个以test_打头的方法。这样,在你编写的每个测试方法中都可使用在方法setUp() 中创建的对象了。

下面使用setUp() 来创建一个调查对象和一组答案,供方法test_store_single_response() 和test_store_three_responses() 使用:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):

    #创建一个调查对象和一组答案,供使用的测试方法使用
    def setUp(self):
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['English', 'Spanish', 'Mandarin']

    #测试单个答案会被妥善地存储
    def test_store_single_response(self):
        
        self.my_survey.store_response(self.responses[0])

        self.assertIn('English',self.my_survey.responses)

    #测试三个答案会被妥善地存储
    def test_store_three_response(self):
        for response in self.responses:
            self.my_survey.store_response(response)
        for response in self.responses:
            self.assertIn(response,self.my_survey.responses)


unittest.main()

运行结果:



测试自己编写的类时,方法setUp() 让测试方法编写起来更容易:可在setUp() 方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例。相比于在每个测试方法中都创建实例并设置其属性,这要容易得多。

11.3 小结

在本章中,你学习了:

  • 如何使用模块unittest 中的工具来为函数和类编写测试
  • 如何编写继承unittest.TestCase 的类
  • 如何编写测试方法,以核实函数和类的行为符合预期
  • 如何使用方法setUp() 来根据类高效地创建实例并设置其属性,以便在类的所有测试方法中都可使用它们

明天要回顾一下练习题,巩固一下基础部分所学知识~就到这里吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343