Python 单元测试(unittest)的使用小结


Posted in Python onNovember 14, 2018

测试目录

项目的整体结构可以参考“软件目录开发规范”,这里单说测试目录。一般都是在项目里单独创建一个测试目录,目录名就是“tests”。

关于目录的位置,一种建议是,在项目名(假设项目名是Foo)的一级子目录下创建二级子目录 “Foo/foo/tests” 。但是这样可能是因为用起来不方便,有很多是按下面的做法。不过下面的示例我还是用这个方法来创建测试目录。
还可以把测试目录向上移一层,作为一级子目录,直接创建在项目之下 “Foo/tests”。参考django、scrapy、flask都是这样的做法。

测试函数

标题的意思是对函数(def)进行测试,相对于测试类(class)。

学习测试,得有要测试的代码。下面是一个简单的函数,接收城市名和国家名,返回一个格式为“City, Country“这样的字符串:

# UnitTest/unit_test/utils/city_functions.py
def get_city_info(city, country):
  city_info = "%s, %s" % (city, country)
  return city_info.title()

接下来就对上面的这个函数进行测试。

手动测试

现在来写一个使用这个函数的程序:

# UnitTest/unit_test/test/cities.py
try:
  from unit_test.utils.city_functions import get_city_info
except ModuleNotFoundError:
  import sys
  sys.path.append('../..')
  from unit_test.utils.city_functions import get_city_info

print("Enter 'q' at any time to quit.")
while True:
  city = input("city name: ")
  if city == 'q':
    break
  country = input("country name: ")
  if country == 'q':
    break
  fullname = get_city_info(city, country)
  print("\tcity info:", fullname)

然后运行的结果:

Enter 'q' at any time to quit.
city name: shanghai
country name: china
    city info: Shanghai, China
city name: q

Process finished with exit code 0

上面这样是手动测试,还是得有一种自动测试函数输出的高效方式。如果能够对get_fullname()进行自动测试,就能始终确信,给这个函数提供测试过的姓名后,它能返回正确的结果。尤其是在对函数进行修改的前后。

模块导入路径的问题

PyCharm会自动把项目目录加到环境变量里去,在PyCharm里执行都没问题。但是如果不用PyCharm而是单独运行,这个目录结构应该会有点问题,会找不到需要测试的函数。简单点就是把测试用例和被测试的函数放到同一个目录里,然后改一下 from import 就可以正常运行了。或者自己手动添加环境变量,就像例子里那样。

单元测试-unittest

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

创建测试用例

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

下面是一个只包含一个方法的测试用例:

# UnitTest/unit_test/test/test_city_functions.py
import unittest
try:
  from unit_test.utils.city_functions import get_city_info
except ModuleNotFoundError:
  import sys
  sys.path.append('../..')
  from unit_test.utils.city_functions import get_city_info

class CitiesTestCase(unittest.TestCase):
  """测试city_functions.py"""
  def test_city_country(self):
    city_info = get_city_info('shanghai', 'china')
    self.assertEqual(city_info, 'Shanghai, China')

  def test_New_York(self):
    city_info = get_city_info('new york', 'America')
    self.assertEqual(city_info, 'New York, America')

if __name__ == '__main__':
  unittest.main()

命名的规则和建议:

  • 类名,可以任意起名,但是最好看起来和测试有关并包含Test字样。
  • 方法名,名字必须以“test_”开头,所有以“test_”开头的方法,都会自动运行

在测试的方法的最后,使用了unittest类最有用的功能之一:一个断言方法。来检查得到的结果和我们预期的结果是否一致。

输出的效果

最后一行 unittest.main() 让Python运行这个文件中的测试。执行程序后得到如下的输出:

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

OK

运行测试用例时,每完成一个单元测试,Python都打印一个字符:

  • 测试通过时打印一个句点;
  • 测试引发错误时打印一个E;
  • 测试导致断言失败时打印一个F。

这就是你运行测试用例时,在输出的第一行中看到的句点和字符数量各不相同的原因。如果测试用例包含很多单元测试,需要运行很长时间,就可通过观察这些结果来获悉有多少个测试通过了。

PyCharm对单元测试做了自己的优化,输出看不到上面的点,而是有更加漂亮的展示方式。

测试不通过

现在看下测试不通过的效果。这里不修改测试用例,而是对get_city_info()函数做一个update,现在还要显示城市的人口数量:

def get_city_info(city, country, population):
  city_info = "%s, %s - 人口: %d" % (city, country, population)
  return city_info.title()

这次再执行测试用例,输出如下:

E
======================================================================
ERROR: test_city_country (__main__.CitiesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_city_functions.py", line 17, in test_city_country
    city_info = get_city_info('shanghai', 'china')
TypeError: get_city_info() missing 1 required positional argument: 'population'

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

这里看的是E而不是之前的点,表示有一个错误。

测试未通过的处理

这里不要去修改之前的测试用例。假设update之前的函数已经在项目内使用起来了,现在测试不通过,表示之前调用这个函数的代码都有问题。如果不想改项目里其它的代码,这里先尝试修改get_city_info()函数,让它能够通过测试,也可以加上新的功能:

# UnitTest/unit_test/utils/city_functions.py
def get_city_info(city, country, population=None):
  if population:
    city_info = "%s, %s - 人口: %d" % (city, country, population)
  else:
    city_info = "%s, %s" % (city, country)
  return city_info.title()

现在的各个版本的update才是兼容旧版本的代码,这次测试用例就可以通过了。

添加新测试

之前的测试用例只能验证就的功能,现在添加了新功能,是否没问题,还得通过测试来进行验证:

# UnitTest/unit_test/test/test_city_functions.py
class CitiesTestCase(unittest.TestCase):
  """测试city_functions.py"""
  def test_city_country(self):
    city_info = get_city_info('shanghai', 'china')
    self.assertEqual(city_info, 'Shanghai, China')

  def test_New_York_population(self):
    city_info = get_city_info('new york', 'America', 8537673)
    self.assertEqual(city_info, 'New York, America - 人口: 8537673')

现在新功能的测试用例也用了,并且2个测试都能通过。以后如果还需要对get_city_info()函数进行修改,只要再运行测试就可以知道新的代码是否会对原有的项目有影响。

断言方法

模块在unittest.TestCase类中提供了很多断言方法,之前已经用一个了。下面是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中

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

测试类

前面的内容只是对函数进行测试。很多时候都会用到类,因为还需要能够证明类也可以正常的运行。类的测试与函数的测试相似,其中大部分工作都是测试类中方法的行为,但存在一些不同之处。

准备要测试的类

先编写一个类来进行测试,这个类里存储了一个课程名,以及学习该课程的学员:

# UnitTest/unit_test/course.py
class CourseManage(object):

  def __init__(self, course):
    self.course = course
    self.students = []

  def show_course(self):
    print("课程:", self.course)

  def add_student(self, name):
    self.students.append(name)

  def show_students(self):
    print("所有学员:")
    for student in self.students:
      print('-', student)

为证明CourseManage类工作正常,再编写一个使用它的程序:

from unit_test.course import CourseManage

course = CourseManage("Python")
course.show_course()
print("准备录入学员...")
print("Enter 'q' at any time to quit.\n")
while True:
  resp = input("Student's Name: ")
  if resp == 'q':
    break
  if resp:
    course.add_student(resp.title())
print("\n录入完毕...")
course.show_students()

这段程序定义了一门课程,并使用课程名创建了一个CourseManage对象。接下来主要就是调用对象的add_student()方法来录入学员名字。输入完毕后,按q能退出。最后会打印所有的学员。
所有的输入和输出如下:

课程: Python
准备录入学员...
Enter 'q' at any time to quit.

Student's Name: oliver queen
Student's Name: barry allen
Student's Name: kara
Student's Name: sara lance
Student's Name: q

录入完毕...
所有学员:
- Oliver Queen
- Barry Allen
- Kara
- Sara Lance

Process finished with exit code 0

编写类的测试用例

下面来编写一个测试,对CourseManage类的行为的一个方面进行验证。如果用户输入了某个学员的名字,这个名字可以被存储在self.students的列表里。所以,需要做的是在学员被录入后,使用assertIn()这个断言方法:

# UnitTest/unit_test/test/test_course.py
import unittest
from unit_test.course import CourseManage

class TestCourseManage(unittest.TestCase):

  def test_add_student(self):
    course = CourseManage("Python")
    name = 'snart'
    course.add_student(name.title())
    self.assertIn('Snart', course.students)

if __name__ == '__main__':
  unittest.main()

上面的方法只验证了录入一个学员的情况,而大多数情况下都是有很多学员的。所以,还要添加一个方法,验证录入多个学员是否正常:

class TestCourseManage(unittest.TestCase):

  def test_add_student(self):
    course = CourseManage("Python")
    name = 'snart'
    course.add_student(name.title())
    self.assertIn('Snart', course.students)

  def test_add_students(self):
    course = CourseManage("Python")
    name_list = ['oliver queen', 'barry allen', 'kara', 'sara lance']
    for name in name_list:
      course.add_student(name.title())
    for name in name_list:
      self.assertIn(name.title(), course.students)

setUp() 方法

在上面的例子里,每个测试方法中都创建了一个实例。但是还有一种需求是,我希望只创建一个实例,但是要在多个方法里对这个实例进行操作来反复验证。在unittest.TestCase类包含方法setUp(),就可以只实例化一次,并可以在每个测试方法中使用。如果在TestCase类中包含了方法setUp(),Python会先运行它,再运行各个以test_打头的方法。
简单点说,setUp()方法就是在父类里预留的一个钩子,会在其他测试方法运行前先运行:

import unittest
from unit_test.course import CourseManage

class TestCourseManage(unittest.TestCase):

  def setUp(self):
    self.course = CourseManage("Python")
    self.name_list = ['oliver queen', 'barry allen', 'kara', 'sara lance']

  def test_add_student(self):
    name = 'snart'
    self.course.add_student(name.title())
    self.assertIn('Snart', self.course.students)

  def test_add_students(self):
    for name in self.name_list:
      self.course.add_student(name.title())
    for name in self.name_list:
      self.assertIn(name.title(), self.course.students)

if __name__ == '__main__':
  unittest.main()

测试自己编写的类时,使用setUp()方法会让测试方法编写起来更容易,下面是建议的做法:

在setUp()方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例。相比于在每个测试方法中都创建实例并设置其属性,这要容易得多。

小结

如果你在项目中包含了初步测试,其他程序员将更敬佩你,他们将能够更得心应手地尝试使用你编写的代码,也更愿意与你合作开发项目。如果你要跟其他程序员开发的项目共享代码,就必须证明你编写的代码通过了既有测试,通常还需要为你添加的新行为编写测试。

请通过多开展测试来熟悉代码测试过程。对于自己编写的函数和类,请编写针对其重要行为的测试,但在项目早期,不要试图去编写全覆盖的测试用例,除非有充分的理由这样做。

pytest

这篇讲的是Python内置的单元测试模块。作为初学者先用着熟悉起来就很不错了。

pytest是Python最流程的单测框架之一。具体可以上GitHub参考下那些开源项目的单元测试,很多用的是这个。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python中使用socket发送HTTP请求数据接收不完整问题解决方法
Feb 04 Python
python使用socket连接远程服务器的方法
Apr 29 Python
浅谈Python中带_的变量或函数命名
Dec 04 Python
centos 安装python3.6环境并配置虚拟环境的详细教程
Feb 22 Python
python使用代理ip访问网站的实例
May 07 Python
Python 中的range(),以及列表切片方法
Jul 02 Python
解决pip install xxx报错SyntaxError: invalid syntax的问题
Nov 30 Python
Python 多线程不加锁分块读取文件的方法
Dec 11 Python
浅谈Python类中的self到底是干啥的
Nov 11 Python
python DataFrame转dict字典过程详解
Dec 26 Python
python邮件中附加文字、html、图片、附件实现方法
Jan 04 Python
详解python字符串驻留技术
May 21 Python
python for循环输入一个矩阵的实例
Nov 14 #Python
python获取中文字符串长度的方法
Nov 14 #Python
对python插入数据库和生成插入sql的示例讲解
Nov 14 #Python
python正向最大匹配分词和逆向最大匹配分词的实例
Nov 14 #Python
对python中的乘法dot和对应分量相乘multiply详解
Nov 14 #Python
在python中实现对list求和及求积
Nov 14 #Python
python 统计一个列表当中的每一个元素出现了多少次的方法
Nov 14 #Python
You might like
随机头像PHP版
2006/10/09 PHP
SSI指令
2006/11/25 PHP
教你如何解密 “ PHP 神盾解密工具 ”
2014/06/20 PHP
php将一维数组转换为每3个连续值组成的二维数组
2016/05/06 PHP
php ajax数据传输和响应方法
2018/08/21 PHP
PHP超级全局变量【$GLOBALS,$_SERVER,$_REQUEST等】用法实例分析
2019/12/11 PHP
javascript中IE浏览器不支持NEW DATE()带参数的解决方法
2012/03/01 Javascript
根据当前时间在jsp页面上显示上午或下午
2014/08/18 Javascript
JavaScript中消除闭包的一般方法介绍
2015/03/16 Javascript
基于JS实现textarea中获取动态剩余字数的方法
2016/05/25 Javascript
纯js仿淘宝京东商品放大镜功能
2017/03/02 Javascript
JS开发中基本数据类型具体有哪几种
2017/10/19 Javascript
JavaScript实现QQ列表展开收缩扩展功能
2017/10/30 Javascript
layui表格数据重载
2019/07/27 Javascript
对Layer弹窗使用及返回数据接收的实例详解
2019/09/26 Javascript
Vue的data、computed、watch源码浅谈
2020/04/04 Javascript
小程序实现背景音乐播放和暂停
2020/06/19 Javascript
用webAPI实现图片放大镜效果
2020/11/23 Javascript
[01:52]PWL S2开团时刻第四期——DOTA2成语故事
2020/12/03 DOTA
python实现连接mongodb的方法
2015/05/08 Python
Python定时执行之Timer用法示例
2015/05/27 Python
Python编程判断这天是这一年第几天的方法示例
2017/04/18 Python
简单谈谈python中的语句和语法
2017/08/10 Python
wxPython的安装图文教程(Windows)
2017/12/28 Python
Python比较配置文件的方法实例详解
2019/06/06 Python
Python多进程multiprocessing、进程池用法实例分析
2020/03/24 Python
HTML5之tabindex属性全面解析
2016/07/07 HTML / CSS
amazeui树节点自动展开折叠面板并选中第一个树节点的实现
2020/08/24 HTML / CSS
美国男士西装打折店:Jos. A. Bank
2017/11/13 全球购物
JD Sports比利时官网:英国领先的运动鞋和运动服饰零售商
2018/10/10 全球购物
美国电子产品主要品牌的授权在线零售商:DataVision
2019/03/23 全球购物
阿尔卡特(中国)的面试题目
2014/08/20 面试题
党的群众路线教育实践活动批评与自我批评范文
2014/10/16 职场文书
2015年乡镇妇联工作总结
2015/05/19 职场文书
高中地理教学反思
2016/02/19 职场文书
利用python实时刷新基金估值(摸鱼小工具)
2021/09/15 Python