Python:使用unittest模块测试代码


本博客将介绍如何使用unittest模块来快速方便地测试代码。

1. 测试函数

name_function.py中编写以下函数:

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

name.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(),让它可以处理中间名。这样做时,我们要确保不破坏这个函数可以处理只有名和姓的姓名的方式。为此,我们可以在每次修改后都进行测试,但这样太麻烦了。索性Python提供了一种自动测试函数输出的高效方式。

1.1 单元测试和测试用例

Python标准库中的unittest提供了代码测试工具。

  • 单元测试 用于核实函数的某个方面没有问题
  • 测试用例 是一组单元测试,这些单元测试一起核实函数在各种情况下的行为都符合要求
  • 全覆盖式测试用例 包含一整套单元测试,涵盖了各种可能的函数使用方式

1.2 可通过的测试

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

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

import unittest
from name_function import get_formatted_name

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

我们创建了一个名为NamesTestCase的类(名字可以任意),用于包含一系列针对get_formatted_name()的单元测试。这个类必须继承unittest.TestCase类。

我们运行test_name_function.py时,所有以test_ 打头的方法都将自动执行。

assertEqual()是unittest类最有用的功能之一:断言方法。断言方法用来核实得到的结果是否和预期的一致。

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

运行这个文件后,得到的输出如下:

.
----------------------------------------------------------
Ran 1 test in 0.000s

OK

第一行的**.**表明有一个测试通过了。接下来的一行指出python运行了一个测试,消耗的时间不到0.001s。最后的OK表明该测试用例中的所有单元测试都通过了。

1.3 不能通过的测试

修改name_function.py,使其能够处理中间名:

def get_formatted_name(first, middle, last):
    """生成整洁的姓名"""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()

这个版本可以处理有中间名的情况,但不能处理只有名和姓的情况。我们再次运行test_name_function.py,输出如下:

E #表示测试用例中有一个单元测试导致了错误
=========================================================
ERROR:test_first_last_name(__main__.NamesTestCase)
---------------------------------------------------------
Traceback(most recent call last):
    ......
TypeError:......

---------------------------------------------------------
Ran 1 test in 0.000s

FAILED(errors=1) #指出整个测试用例都没有通过

1.4 测试未通过时怎么办

修改导致测试不能通过的代码

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

1.5 添加新测试

import unittest
from name_function import get_formatted_name

class NamesTest(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')

    def test_f_l_m_name(self):
        formatted_name = get_formatted_name('janis', 				'joplin', 'wang')
        self.assertEqual(formatted_name,'Janis Wang 				Joplin')

unittest.main()

输出如下:

..
----------------------------------------------------------
Ran 2 tests in 0.000s

OK

2. 测试类

2.1 各种断言方法

方法 用途
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中

只能在继承unittest.TestCase的类中使用这些方法。上面只列出了6个常用的断言方法。

2.2 一个要测试的类

类的测试与函数的测试相似,大部分工作都是测试类中方法的行为,但存在一些不同之处。下面在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_results(self):
        """显示收集到的所有答卷"""
        print("Survey results:")
        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_results()

现在想进行改进:让每位用户都可输入多个答案;编写一个方法,它只列出不同的答案,并指出每个答案出现了多少次;再编写一个类,用于管理非匿名调查。

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

2.3 测试AnonymousSurvey类

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

这种做法的效果很好,但这些测试有些重复的地方(每个测试方法中创建了一个AnonymousSurvey实例)。下面使用unittest的另一项功能来提高它们的效率。

2.4 方法setUp()

unittest.TestCase类包含方法setUp(),让我们只需创建这些对象一次,并在每个测试方法中使用它们。Python将会先运行setUp(),然后再运行其他前缀为test_ 的方法。

下面使用setUp()来创建一个调查对象和一组答案:

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

注意:运行测试用例时,每完成一个单元测试,Python都打印一个字符,通过打印,引发错误打印E,导致断言失败打印F。


文章作者: 恰醋
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 恰醋 !
评论
  目录