Python 单元测试

测试代码

编写函数或类时,可以为其编写测试单元,通过测试,可以确定代码面对各种输入都能按照要求那样工作,在添加新代码时也可以对其进行测试,确保不会破坏既有程序。

1、测试函数

首先编写一个程序 name_functions.py,里面包含一个函数 get_formatted_name(),用以处理名和姓,得到全名full_name:

# name_functions.py
def get_formatted_name(first, last):
    """获得一个全名"""
    full_name = first + ' ' + last
    return full_name.title()

再编写一个程序 name.py:

# name.py
from name_functions import get_formatted_name

print("Enter 'q' at any time to quit")
while True:
    first = input('\nFirst name: ')
    if first == 'q':
        break
    last = input('\nLast name: ')
    if last == 'q':
        break
    
    formatted_name = get_formatted_name(first, last)
    print('\Neatly formatted name: ' + formatted_name + '.')

上述程序只能处理名和姓,当有中间名的时候,我们需要修改 get_formatted_name(),使其能处理中间名(middle),又不破坏只有名和姓的方式,为此,我们每次都要在修改 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 NameTestCase(unittest.TestCase):
    """测试name_function.py"""
    
    def test_first_last_name(self):
        """能处理如Janis Joplin 这样的姓名吗?"""
        formatted_name = get_formatted_name('janis', 'joplin')3)       self.assertEqual(formatted_name, 'Janis Joplin')
        
unittest.main()
----------------------------
Ran 1 tests in o.001s     # 测试通过

OK

首先,我们导入了模块 unittest 和要测试的函数,接下来我们创建了一个名为NameTestCase 的类(类名随意,最好包含Test),用于包含一系列针对函数的单元测试,这个类继承unittest.TestCase 类,这样python才知道如何运行你编写的测试。

在(3)处,我们使用了 unittest 类最有用的功能之一:一个断言方法,该方法可以用来判断是否与预期一致,有2个参数,第1个为“测试结果”,第2个为“预期结果”,在这里我们是期望 formatted_name 的值为 Janis Joplin。如果相等就好,否则就输出错误信息。

1.3、未通过的测试

上述程序测试的是只有名和姓的情况,如果我在函数 get_formatted_name()中再指定一个中间名(middle),那么再运行刚才的测试,就会出错:

# name_function.py
def get_formatted_name(first, middle, last):
    """获得全名"""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()

再运行测试程序 test_name_function.py:

# test_name_function.py
Ran 1 test in 0.004s

FAILED (errors=1)     # 测试未通过

Error
Traceback (most recent call last):
  File "C:\Users\hj\AppData\Local\Programs\Python\Python36-32\lib\unittest\case.py", line 59, in testPartExecutor
    yield
  File "C:\Users\hj\AppData\Local\Programs\Python\Python36-32\lib\unittest\case.py", line 605, in run
    testMethod()
  File "C:\Users\hj\PycharmProjects\package\test_get_formatted_name.py", line 7, in test_get_formatted_name
    formatted_name = get_formatted_name('tom', 'jerry')
TypeError: get_formatted_name() missing 1 required positional argument: 'middle'   # 提示缺少一个实参

1.4、测试未通过怎么办

测试未通过,都会提示错误信息,根据信息修改原程序,而非测试程序;

在上述示例中,测试未通过提示我们确实一个实参(middle),那么我们修改 函数get_formatted_name(),将middle 设置为可选参数:

# get_formatted_name.py
def get_formatted_name(first, last, middle=''):
    """获得全名"""
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()

运行测试程序 test_name_function.py:

# test_name_function.py
Ran 1 test in 0.001s

OK

1.5、添加新测试

再编写测试程序,确定其能处理包含中间名的姓名,为此我们在NameTestCase 类中添加个方法:

# test_name_function.py
from unittest import TestCase
from name_function import get_formatted_name


class NameTestCase(TestCase):
    def test_first_last_name(self):
        """能处理如Janis Joplin 这样的姓名吗?"""
        formatted_name = get_formatted_name('tom', 'jerry')
        self.assertEqual(formatted_name, 'Tom Jerry')

    def test_middle(self):     # 添加新方法用以处理如Tom Li Jerry 这样的名字
        """能处理如Tom Li Jerry 这样的名字?"""
        formatted_name = get_formatted_name('tom', 'jerry', 'li')
        self.assertEqual(formatted_name, 'Tom Li Jerry')
        
unittest.main()
-----------------------------
Ran 2 tests in 0.001s      # 测试通过,说明程序能够处理.......

OK      

踩坑提醒

上述例子通过测试,在 python 自带 IDLE 中,能够正常运行,而使用 pycharm 进行测试函数时,修改原程序函数 get_formatted_name(),再运行测试程序,发现测试结果没有变化,上网查阅了资料得以确定是编辑器的原因,诸如pycharm 类似的编辑器在运行测试程序时,会优先运行本身自带的 unittest 组件,导致修改原函数结果没有变化(绕的我自己都糊涂了),为此在使用 pycharm 测试函数时,我们可以如下操作:(可参考:http://blog.csdn.net/u010816480/article/details/72821535)

  • 鼠标选择要测试的函数,ctrl+shift+T,在弹出的窗口选择 creat new test (自动创建测试程序)
  • 创建测试程序只是一个框架,为此需要我们补充完成
  • 测试时,鼠标选择测试的类,右键选择 Run unittest xxxx 即可

测试代码如下:

# test_name_function.py
from unittest import TestCase   # 直接导入unittest 模块的TestCase 类,而不是只导入模块
from name_function import get_formatted_name

class NameTestCase(TestCase): 
	def test_first_last_name(self): 
		formatted_name = get_formatted_name('tom', 'jerry') 
		self.assertEqual(formatted_name, 'Tom Jerry') 

2、测试类

2.1、各种断言方法

python 在 unittest.TestCase 类中提供了很多断言方法,使用这些方法可核实返回的值等于或不等于预期的值、返回的值为 True 或 False、返回的值是否在列表中:

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

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 result: ')
        for response in self.responses:
            print('-' + response)

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

# language_survey.py
from language_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 survey 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: q

Thank you to survey who participated in the survey!
Survey result: 
-english

将AnonymousSurvey 类存放在 模块 survey.py 中,并想进一步改进,让每位用户都可以输入多个答案,编写一个方法,它列出不同答案,并指出每个答案出现了多少次,再编写一个类,用于管理非匿名调查,经过上述修改可能存在风险,可能会影响 AnonymousSurvey 类的当前行为,为此我们可以编写针对这个类的测试。

2.3、测试 AnonymousSurvey 类

# test_anonymousSurvey.py         
from unittest import TestCase          # 导入测试模块的类
from survey import AnonymousSurvey     # 导入测试类

class TestAnonymousSurvey(TestCase):
    """针对 AnonymousSurvey 类的测试"""
    
    def test_store_response(self):         # 测试单个答案
        """测试单个答案会被妥善地存储"""
        question = 'What language did you first learn to speak?'
        my_survey = AnonymousSurvey(question)   # 创建实例,传入实参 question
        my_survey.store_response('English')   # 调用store_response方法,将答案存储到属性responses(列表)中

        self.assertIn('English', my_survey.responses)  # 使用断言方法,判断是否在属性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)       # 调用store_response方法,将答案存储到属性responses(列表)中

        for responses in  responses:
            self.assertIn(response, my_survey.responses)  # 使用断言方法,判断是否在属性responses中
-------------------------------------


Ran 2 tests in 0.002s       # 测试通过

OK

2.4、方法 setUp()

在之前的 test_anonymousSurvey.py 中,每个测试方法,我们都创建了一个 AnonymousSurvey 实例对象,并在每个方法中都创建了一个答案,平白多了几行代码;

而unittest.TestCase 类中包含了方法 setUp(),我们只需在方法 setUp()创建一次实例对象,下面的每个测试方法都将能使用它们,python 也是优先运行它,为此省去了多余的代码:

# test_anonymousSurvey.py         
from unittest import TestCase          # 导入测试模块的类
from survey import AnonymousSurvey     # 导入测试类

class TestAnonymousSurvey(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])  # 调用store_response,并将答案存储到属性responses 中
        self.assertIn(self.response[0], self.my_surver.responses)  # 断言方法,判断是否在属性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)
           
  -------------------------------

Ran 2 tests in 0.002s

OK
        
    

注意

运行测试时,每完成一个单元测试,python 都会打印一个字符:通过打印一个句点,错误打印一个E,测试导致断言失败打印一个F。

在项目中包含初步测试,将使程序更趋完美,在用户报告bug 之前,把问题考虑在内,在项目早期,不要试图去编写全覆盖的测试用例,除非有充分的理由。

练习

编写一个名为 Employee 的类,其方法 init 接受名、姓和年薪,并将他们存储到属性中,编写一个名为 give_raise()的方法,它默认将年薪增加 5000 美元,但也能接受其他的年薪增加量。

为 Employee 编写一个测试用例,包含两个测试方法:test_default_raise()和 test_give_custom_raise(),使用方法 setUp(),以免在每个测试方法都创建新的雇员实例,运行这个测试用例,确认两个测试都通过了。

# name_employee.py
class Employee():
    """编写一个类接受雇员姓名和年薪"""
    
    def __init__(self, first, last, salary ):
        """初始化雇员姓名和年薪"""
        self.first_name = first
        self.last_name = last
        self.salary = salary
        
    def give_raise(self, raise=5000):
        """设置年薪增加量"""
        self.salary += raise
# test_employee.py
from unittest import TestCase
from name_employee import Employee

class TestEmployee(TestCase):
    """测试 Employee 类 """
    def setUp(self):
        """给 Employee 类创建实例化对象,以供测试使用"""
        self.someone = Employee('li', 'la', 65000)      
        
    def test_give_default(self):
        """测试默认年薪增加量为 5000 美元"""
        self.someone.give_raise()
        self.assertEqual(self.someone.salary, 70000)
        
    def test_give_custom_raise(self):
        """测试能接受其他年薪增加量"""
        self.someone.give_raise(6000)
        self.assertEqual(self.someone.salary, 71000)
-------------------------------------------------------------------------


Ran 2 tests in 0.057s

OK   

你可能感兴趣的:(Python,学习笔记,单元测试,python,测试工具)