Python编程从入门到实践——第十一章

第十一章——测试代码

在本章中, 你将学习如何使用Python模块unittest中的工具来测试代码。

11.1 测试函数

name_function.py

def get_formatted_name(first,last):
    """Generate a neatly formatted full name."""
    full_name = first + ' '+ last
    return full_name.title()

names.py

from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")

while True:
    first = input("Please input your first name: ")
    if first == 'q':
        break   
    last = input("Please input your last name:")
    if last == 'q':
        break
    
    name = get_formatted_name(first,last)
    print(name)    
    
        

假设我们要修改get_formatted_name(),使其还能够处理中间名。确保不破坏这个函数处理只有名和姓的姓名的方式,为此,只能每次改动进行测试,但过于繁琐。
Python提供了一种自动测试函数输出的高效方式,Python标准库中的模块unittest提供了代码测试工具。

11.1.1 单元测试和测试用例

单元测试用于核实函数的某个方面没有问题(具有测试功能的模块)

测试用例是一组单元测试(进行测试的数据)

全覆盖式测试用例:包含一整套单元测试,涵盖了各种可能的函数使用方式。(包含各种函数使用方式的测试实例)

11.1.2 可通过的测试

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

import unittest
from name_function import get_formatted_name

class NameTest(unittest.TestCase):
    """测试name_function.py"""
    
    def test_first_last_name(self):
        """能够正确处理Janis Joplin这样的姓名吗? """
        formatted_name = get_formatted_name('janis','joplin')
        self.assertEqual(formatted_name,'Janis Joplin')

unittest.main()

我们使用了unittest类最有用的功能之一:一个断言方法。断言方法用来核实得到的结果是否与期望的结果一致。

 self.assertEqual(formatted_name,'Janis Joplin')

代码行unittest.main()让Python运行这个文件中的测试。

unittest.main()

运行结果如下:

第1行的句点表明有一个测试通过了。接下来的一行指出Python运行了一个测试,消耗的时
不到0.001秒。最后的OK表明该测试用例中的所有单元测试都通过了

11.1.3 不能通过的测试

下面是函数get_formatted_name()的新版本,它要求通过一个实参指定中间名:

def get_formatted_name(first,middle,last):
    """Generate a neatly formatted full name."""
    full_name = first + ' ' + middle + '' + last
    return full_name.title()

对其进行测试,运行程序test_name_function.py时,输出如下:

有一个字母E,它指出测试用例中有一个单元测试导致了错误。

接下来,我们看到NamesTestCase中的test_first_last_name()导致了错误。测试用例包含众多单元测试时,我们可以在这里看到哪个测试发生错误。

下面我们看到了一个标准的traceback,它指出函数调用get_formatted_name('janis', 'joplin')有问题,因为它缺少一个必不可少的位置实参。

最后,还看到了一条消息,它指出整个测试用例都未通过,因为运行该测试用例时发生了一个错误。这条消息位于输出末尾,让你一眼就能看到。

11.1.4 测试未通过时怎么办

测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚对函数所做的修改,找出导致函数行为不符合预期的修改。

在这个示例中, get_formatted_name()以前只需要两个实参——名和姓,但现在它要求提供
名、中间名和姓,最佳的选择是让中间名变为可选的。

def get_formatted_name(first,last,middle=''):
    """Generate a neatly formatted full name."""
    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

class NameTest(unittest.TestCase):
    """测试name_function.py"""
    --snip--

    def test_first_last_middle_name(self):
        """能够正确处理像Wolfgang Amadeus Mozart这样的姓名吗? """
        formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name,'Wolfgang Amadeus Mozart')

unittest.main()

11.1动手试一试

11-1 城市和国家:编写一个函数,它接受两个形参:一个城市名和一个国家名。这个函数返回一个格式为 City, Country 的字符串,如 Santiago, Chile。将这个函数存储在一个名city_functions.py 的模块中。

city_functions.py

def City_Country(city,country):
    city = input('Please input your city: ')
    country = input('Please input your country: ')
    msg =  city + ", " + country
    return msg
    

创建一个名为 test_cities.py 的程序,对刚编写的函数进行测试(别忘了,你需要导
入模块 unittest 以及要测试的函数)。编写一个名为 test_city_country()的方法,核实使用类似于'santiago'和'chile'这样的值来调用前述函数时,得到的字符串是正确的。运行 test_cities.py,确认测试 test_city_country()通过了。

test_cities.py

import unittest

from city_functions import City_Country

class CityTestCase(unittest.TestCase):
    """测试CityCountry.py"""
    def test_city_country(self):
        msg = City_Country('Santiago','Chile')
        self.assertEqual(msg,'Santiago, Chile')

unittest.main()
    

11-2 人口数量:修改前面的函数,使其包含第三个必不可少的形参 population,并
返回一个格式为 City, Country – population xxx 的字符串,如 Santiago, Chile –
population 5000000。运行 test_cities.py,确认测试 test_city_country()未通过。
修改上述函数,将形参 population 设置为可选的。再次运行 test_cities.py,确认测
试 test_city_country()又通过了。

再编写一个名为 test_city_country_population()的测试,核实可以使用类似于'santiago'、 'chile'和'population=5000000'这样的值来调用这个函数。再次运行test_cities.py,确认测试 test_city_country_population()通过了。

city_function.py

def City_Country(city, country, population=''):

    if population:
        msg = city + ", " + country + "-population=" + str(population)
    else:
        msg = city + ", " + country
    return msg

test_cities.py

import unittest

from city_functions import City_Country


class CityTestCase(unittest.TestCase):
    """测试CityCountry.py"""

    def test_city_country(self):
        msg = City_Country('Santiago', 'Chile')
        self.assertEqual(msg, 'Santiago, Chile')

    def test_city_country_population(self):
        msg = City_Country('Santiago', 'Chile', 5000000)
        self.assertEqual(msg, 'Santiago, Chile-population=5000000')


unittest.main()

11.2 测试类

这一部分主要用来学习测试类

11.2.1 各种断言方法

你只能在继承unittest.TestCase的类中使用这些方法。

11.2.2 一个要测试的类

下面来编写一个类进行测试,来看一个帮助管理匿名调查的类:
补充:首先明确的是self只有在类的方法中才会有,独立的函数或方法是不必带有self的。self在定义类的方法时是必须有的,虽然在调用时不必传入相应的参数。

survey.py

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_result(self):
        """显示收到的所有调查问卷"""
        print('Survey result:')
        for response in self.responses:
            print("-" + response)

language_survey.py

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

定义一个问题,并创建一个AnonyousSurvey对象,显示问题并提示,想要提示就输入'q',进入循环,用户输入第一个学习的语言是什么,添加到response数组里,不断循环,输入'q'退出循环,调用显示结果的函数,将结果打印在屏幕上。

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

11.2.3 测试 AnonymousSurvey 类

编写一个测试,对AnonymousSurvey类的行为的一个方面进行验证,用户只提供一个答案,这个答案能被妥善处理。将在这个答案被存储后,使用方法assertIn()来核实它包含在答案列表中:
test_survey.py

import unittest

from survey import AnonymousSurvey


class TestAnonymousSurvey(unittest.TestCase):
    """用来测试AnonymousSurvey类"""
    
    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()

要测试类的行为,需要创建其实例。使用问题"What language did you first learn to speak?"创建了一个名为my_survey的实例,然后使用方法store_response()存储了单个答案English。检查English是否包含在列表my_survey.responses中,以核实这个答案是否被妥善地存储了。

下面来核实用户提供三个答案时,它们也将被妥善地存储。
test_survey.py

import unittest

from survey import AnonymousSurvey


class TestAnonymousSurvey(unittest.TestCase):
    """用来测试AnonymousSurvey类"""

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

再次运行test_survey.py时,两个测试(针对单个答案的测试和针对三个答案的测试)都通过了。
前述做法的效果很好,但这些测试有些重复的地方。下面使用unittest的另一项功能来提高它们的效率。

11.2.4 方法 setUp()

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

import unittest

from survey import AnonymousSurvey


class TestAnonymousSurvey(unittest.TestCase):
    """用来测试AnonymousSurvey类"""
    
    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(self.responses[0], self.my_survey.responses)

    def test_store_three_response(self):
        """测试三个答案也能够被妥善存储"""
        responses = self.responses
        for response in responses:
            self.my_survey.store_response(response)

        for response in responses:
            self.assertIn(response, self.my_survey.responses)


unittest.main()

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

注意

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

11.2动手试一试

11-3 雇员

编写一个名为 Employee 的类,其方法__init__()接受名、姓和年薪,并将它们都存储在属性中。编写一个名为 give_raise()的方法,它默认将年薪增加 5000美元,但也能够接受其他的年薪增加量。为 Employee 编写一个测试用例,其中包含两个测试方法: test_give_default_raise()和 test_give_custom_raise()。使用方法 setUp(),以免在每个测试方法中都创建新的雇员实例。运行这个测试用例,确认两个测试都通过了。
employee.py

class Emoployee():
    """用来显示一个员工的姓名和工资情况"""
    
    def __init__(self, first_name, last_name, salary):
        """初始化员工姓名和工资"""
        self.first_name = first_name
        self.last_name = last_name
        self.salary = salary
    
    def give_raise(self, raise_salary=5000):
        """增加年薪,默认增加5000"""
        self.total_salary = raise_salary + self.salary

注意函数中新出现的变量一定要带self.呀,要不然在测试类里面没有被办法调用。

test_employee.py

import unittest

from employee import Emoployee


class TestEmployee(unittest.TestCase):
    """测试employ类"""
    
    def setUp(self):
        """创建类实例"""
        self.my_employee = Emoployee('li', 'zeyu', 15000)
            
    def test_give_default_raise(self):
        """默认涨薪"""
        self.my_employee.give_raise()
        self.assertEqual(self.my_employee.total_salary, 20000)           
    
    def test_give_custom_raise(self):
        """定制化涨薪"""
        self.my_employee.give_raise(2000)
        self.assertEqual(self.my_employee.total_salary, 17000) 
    

unittest.main()

11.3 小结

1.测试函数:导入官方测试类unittest,调用测试函数,给形参赋值,使用assertEqual进行对比

2.测试类,导入官方测试类unittest,实例化类对象,调用对于函数,对于对照列表是否有对于的值assertIn()

3.使用方法setUp()来根据类高效地创建实例并设置其属性,在后面的函数可以调用它

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值