阅读《Python编程从入门到实践》Day14

第十一章

编写函数或类时,还可为其编写测试。通过测试,可确定代码面对各种输入都能够按要求的那样工作。

1、测试函数

下面是要用于测试的函数,它接受名和姓并返回整洁的姓名,存储在文件name_function.py中:

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

函数将名和姓合并成姓名,在名和姓之间加上一个空格,并将它们的首字母都大写,再返回结果。为核实函数是否像期望的那样工作,下面编写一个使用这个函数的程序:

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 + ".")

测试如下:

Enter 'q' at any time to quit.

Please give me a first name: janis
Please give me a last name: joplin
    Neatly formatted name: Janis Joplin.

Please give me a first name: bob
Please give me a last name: dylan
    Neatly formatted name: Bob Dylan.

Please give me a first name: q

从测试的结果可以知道,合并得到的姓名是正确的。如果现在需要添加处理中间名的功能,就需要在保证不破坏原来功能的基础上,添加新的功能,然后再进行测试。这样显得就太繁琐了,不过Python您提供了一种自动测试函数输出的高效方式,可对相应的函数进行自动测试。

(1)单元测试和测试用例

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

(2)可通过的测试

要为函数编写测试用例,可先导入模块unittest以及要测试的函数,再创建一个unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。下面是只包含一个方法的测试用例:

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    def test_first_last_name(self):
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

unittest.main()
# 输出:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

首先,需要导入模块unittest和要测试的函数。然后创建一个类,用于包含一系列针对被测试函数的单元测试,这个类必须继承unittest.TestCase类,这样Python才知道如何运行你编写的测试。
这个类只包含了一个方法,用于核实姓名能否被正确地格式化。在运行上述文件时,所有以test_大头的方法都将自动运行。
这里使用了unittest类中最有用的功能之一:一个断言方法。断言方法用来核实得到的结果是否与期望的结果一致。self.assertEqual()方法就是将第一个参数和第二个参数进行比较。
在输出的结果中,第一行的句点表明有一个测试通过了。接下来的一行指出Python运行了一个测试,消耗的时间不到0.001秒。最后的OK表明该测试用例中的所有单元测试都通过了。

(3)不能通过的测试

我们故意只在函数中添加可以处理中间名的形参middle,而没有修改测试用例中的实参。运行结果:

E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_name_function.py", line 8, in test_first_last_name
    formatted_name = get_formatted_name('janis', 'joplin')
TypeError: get_formatted_name() missing 1 required positional argument: 'last'

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)

由于测试没有通过,返回了很多信息。第一行输出只有一个字母E,它指出测试用例中有一个单元测试导致了错误。然后可以看到类中的函数导致了错误。当测试用例包含很多单元测试时,准确知道那个测试没通过至关重要。在往下,我们看到一个标准的traceback,它准确指出函数调用中出现了问题,因为它缺少了一个必不可少的位置实参。最后显示运行了一个单元测试,并指出整个测试用例都没有通过。

(4)测试未通过时怎么办

测试未通过说明你编写的新代码有错,此时,不要修改测试,而应修复导致测试不能通过的代码:检查刚对函数所做的修改,找出导致函数行为不符合预期的修改。
对于上述未能通过的测试,我们知道是新增的中间名参数导致的,所以可以让中间名变为可选的,即添加默认值,然后再适当地添加if判断语句,就可以让测试通过了。

(5)添加新测试

下面再编写一个测试,用于测试包含中间名的姓名。

import unittest
from name_function import get_formatted_name

class NamesTestCase(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()
# 输出:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

这个新添加方法的方法名必须以test_开头,这样它才会在我们运行整个测试文件时自动运行。在TestCase类中使用很长的方法名是可以的;这些方法名必须具有描述性的,这才能让你明白测试未通过时的输出;这些方法由Python自动调用,你根本不用编写调用它们的代码。

2、测试类

(1)unittest Module中常用的6个断言方法
方法 用途
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中

Python在unittest.TestCase类中提供了很多断言方法。如果你认为应该满足的条件实际上并不满足,Python将引发异常。

(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)

要创建这个类的实例,只需提供一个问题即可。下面编写一个使用它的程序:

from survey import 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()
# 输出:
What language did you first learn to speak?
Enter 'q' at any time to quit.

Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q

Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
- Mandarin
(3)测试AnonymousSurvey类

下面编写一个测试:如果用户面对调查问题时只提供一个答案,这个答案也能被妥善地存储。

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()
# 测试输出:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

这里的测试与测试函数时类似,这里的第一个方法验证调查问题的单个答案被存储后,会包含在调查结果列表中。要测试类的行为,需要创建其实例。由输出知道,测试顺利通过了。
下面来核实用户提供三个答案时,它们也将被妥善地存储。

    def test_store_three_responses(self):
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English', 'Spanish', 'Mandarih']
        for response in responses:
            my_survey.store_response(response)
            
        for response in responses:
            self.assertIn(response, my_survey.responses)
# 测试输出:
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

在测试类中添加上述的方法,有结果可知,测试也顺利通过了。但是这些测试有些重复的地方。下面使用unittest的另一项功能来提高它们的效率。

(4)方法setUp()

在前面的示例中,我们在每个测试的方法中都创建了一个AnonymousSurvey实例,并在每个方法中都创建了答案。unittest.TestCase类中包含了方法setUp(),Python将先运行它,再运行各个以test_开头的方法。这样,在你编写的每个测试方法中都可使用在方法setUp()中创建的对象。
下面使用setUp()来创建一个调查对象和一组答案,供两个测试方法使用:

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', 'Mandarih']
    
    def test_store_single_response(self):
        self.my_survey.store_response(self.responses[0])
        self.assertIn(self.responses[0], self.my_survey.responses)

    def test_store_three_responses(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()做了两件事情:创建一个调查对象;创建一个答案列表。存储这两样东西的变量名包含前缀self(即存储在属性中),因此可在这个类的任何地方使用。
测试自己编写的类时,方法setUp()让测试方法编写起来更容易:可在setUp()方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例。

注意:运行测试用例时,每完成一个单元测试,Python都打印一个字符:测试通过时打印一个句点;测试引发错误时打印一个E;测试导致断言失败时打印一个F。这就是你运行测试用例时,在输出的第一行中看到的句点和字符数量各不相同的原因。如果测试用例包含很多单元测试,需要运行很长时间,就可通过观察这些结果来获悉有多少个测试通过了。

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

推荐阅读更多精彩内容

  • 编写函数或类时,还可为其编写测试。通过测试,可确定代码面对各种输入都能够按要求的那样工作。在程序中添加新代码时,你...
    Darren_Lin阅读 5,236评论 1 5
  • 洞见SELENIUM自动化测试 写在最前面:目前自动化测试并不属于新鲜的事物,或者说自动化测试的各种方法论已经层出...
    厲铆兄阅读 6,706评论 3 47
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 感赏女儿下午提醒我不要再购买韩货,萨徳系统让女儿愤愤不平,家国情怀,政治抱负酿造了我家小愤青,大刀阔斧地分析了一...
    利利lili阅读 188评论 0 5
  • 今天是“双十二”,跟风上淘宝逛逛,结果一不留神没克制住,买了两双童鞋,两个儿童水杯,两只橡木高脚凳,和一瓶护肤水。...
    成乐阅读 125评论 0 1