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实现数值积分的Simpson方法实例分析
Jun 05 Python
python计算圆周率pi的方法
Jul 11 Python
在类Unix系统上开始Python3编程入门
Aug 20 Python
一个月入门Python爬虫学习,轻松爬取大规模数据
Jan 03 Python
Tensorflow实现AlexNet卷积神经网络及运算时间评测
May 24 Python
Python 新建文件夹与复制文件夹内所有内容的方法
Oct 27 Python
Python利用sqlacodegen自动生成ORM实体类示例
Jun 04 Python
Python 异常的捕获、异常的传递与主动抛出异常操作示例
Sep 23 Python
PyQt5+Pycharm安装和配置图文教程详解
Mar 24 Python
python中实现栈的三种方法
Dec 19 Python
利用python进行数据加载
Jun 20 Python
python高温预警数据获取实例
Jul 23 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
DIY实用性框形天线
2021/03/02 无线电
提升PHP执行速度全攻略(上)
2006/10/09 PHP
PHP编程过程中需要了解的this,self,parent的区别
2009/12/30 PHP
window+nginx+php环境配置 附配置搭配说明
2010/12/29 PHP
PHP实现自动登入google play下载app report的方法
2014/09/23 PHP
PHP explode()函数用法讲解
2019/02/15 PHP
jQuery中文入门指南,翻译加实例,jQuery的起点教程
2007/02/09 Javascript
一实用的实现table排序的Javascript类库
2007/09/12 Javascript
Exitjs获取DataView中图片文件名
2009/11/26 Javascript
js实现的黑背景灰色二级导航菜单效果代码
2015/08/24 Javascript
jquery实现具有嵌套功能的选项卡
2016/02/12 Javascript
JS用斜率判断鼠标进入DIV四个方向的方法
2016/11/07 Javascript
Bootstrap select下拉联动(jQuery cxselect)
2017/01/04 Javascript
JavaScript编写的网页小游戏,很给力
2017/08/18 Javascript
vue、react等单页面项目应该这样子部署到服务器
2018/01/03 Javascript
React Native 真机断点调试+跨域资源加载出错问题的解决方法
2018/01/18 Javascript
原生JS实现的轮播图功能详解
2018/08/06 Javascript
js实现点击展开隐藏效果(实例代码)
2018/09/28 Javascript
微信小程序间使用navigator跳转传值问题实例分析
2020/03/27 Javascript
python之virtualenv的简单使用方法(必看篇)
2017/11/25 Python
django使用html模板减少代码代码解析
2017/12/12 Python
修复 Django migration 时遇到的问题解决
2018/06/14 Python
什么是python的函数体
2020/06/19 Python
浅谈keras 模型用于预测时的注意事项
2020/06/27 Python
浅析关于Keras的安装(pycharm)和初步理解
2020/10/23 Python
使用Python+Appuim 清理微信的方法
2021/01/26 Python
澳大利亚在线百货商店:Real Smart
2017/08/13 全球购物
.NET面试问题集
2015/12/08 面试题
团组织关系介绍信
2014/01/12 职场文书
乌鸦喝水教学反思
2014/02/07 职场文书
《小池塘》教学反思
2014/02/28 职场文书
餐厅总厨求职信
2014/03/04 职场文书
2015年班主任个人工作总结
2015/03/31 职场文书
幼儿园秋季开学通知
2015/07/16 职场文书
Django项目配置Memcached和Redis, 缓存选择哪个更有优势
2021/04/06 Python
PostgreSQL出现死锁该如何解决
2022/05/30 PostgreSQL