一篇文章了解_unittest
1. 基本概念
2018年10月7日 星期日
11:39
unittest是python自带的单元测试框架,有时候又被称为”PyUnit”,是python版本的JUint实现。
该框架的作者是 Kent Beck和Erich Gamma,感谢祖师爷赏饭吃。
在学习使用unittest库之前,我们需要了解一下unittest库的一些重要概念:
- test fixture: 代表了用例执行前的准备工作和用例执行之后的清理工作。比如在用例执行前创建临时文件和文件夹,又或者启动1个server进程等;
- test case: 测试用例,这个相信大家都不陌生。是测试的最小单位,一般检查一组输入的响应(输出)是否符合预期。unittest模块提供了TestCase类来帮助我们创建测试用例;
- test suite: 经常被翻译成”测试套件”,也有人称为”测试套”,是测试用例或测试套件的集合,一般用来把需要一起执行的用例组合到一起;
- test runner: 用来执行测试用例并输出测试结果的组件。可以是图形界面或命令行界面;
总之
- test fixture的功能可以理解成是初始化和清理测试数据及环境
- test case是测试用例
- test suite是用例集合
- test runner的作用是运行用例并返回结果
2. 基本用法
2018年10月7日 星期日
11:40
我们通过最简单的例子来看一下unittest的基本用法,下面的代码测试了3个python字符串方法,基本上满足了大部分情况下的测试需求
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(),
['hello', 'world'])
# check that s.split fails when the separator is not a
string
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main()
解释一下关键点
- 可以通过继承unittest.TestCase类来定义我们自己的测试用例,1个测试用例类下面可以有多个测试方法(test)或者叫做测试点
- 记住这个套路:测试用例中方法名以test开头的方法才是测试方法,比如上面的例子里定义了3个以test开头的方法,分别是test_upper,test_isupper和test_split。非测试方法是不会被test runner执行的
- 断言是测试用例的核心。我们使用assertEqual()来判断预期结果,用assertTrue()和assertFalse来做是非判断,以及用assertRaises()来判断预期的异常是否有被抛出。这些unittest提供的以assert开头的方法就是断言,一般情况下,每个测试方法里都必须有断言
- 最后, unittest.main提供了最简单的运行用例的方式。当我们从命令行运行上面的代码时,我们可以看到如下的输出
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
除了使用unittest.main,还有其他的方式可以运行测试用例,比如把最后2行替换为
suite =
unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)
unittest.TextTestRunner(verbosity=2).run(suite)
运行用例,结果将会如下所示
test_isupper
(__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
3. 实例: 测试弱密码
2018年10月7日 星期日
11:41
背景
考虑这样一个测试弱密码的实例,这个我们在pytest相关教程中也有过描述。
我们需要判断用户的密码中包含简单密码,规则是这样的,密码必须至少6位,满足6位的话判断用户的密码不是password123或者password之类的弱密码。
对于如下的测试数据,我们要如何使用unittest来进行相关测试呢?
[
{"name":"jack","password":"Iloverose"},
{"name":"rose","password":"Ilovejack"},
{"name":"tom","password":"password123"}
]
Test fixture
前文我们也说过,text fixture的主要功能是初始化测试数据或环境以及清理测试数据或环境。
考虑上面的例子,对我们而已,在用例执行之前初始化上面的测试数据是有必要的,我们可以把上面的数据用python的数据结构来表示,比较合适的数据结构是python的字典。这样做有如下的好处
- 统一初始化一些需要在多个用例之间共享的数据
- 可以在初始化的时候做一些数据的处理工作,比如过滤一些无效数据等
Test fixture最简单的实现方式是通过自定义下面的2个方法:
- TestCase.setUp方法在每个测试方法运行之前都会运行一次,适合为每个用例都初始化一遍数据
- TestCase.tearDown方法在每个测试方法运行之后都会运行一次,适合为每个用例都清理一遍数据
代码
新建名为test_password_1.py的文本文件,输入如下内容
import unittest
class PasswordTeseCase(unittest.TestCase):
def setUp(self):
print('set up')
self.test_data = [
dict(name='jack', password='Iloverose'),
dict(name='rose', password='Ilovejack'),
dict(name='tom', password='password123')
]
def test_week_password(self):
for data in
self.test_data:
passwd = data['password']
self.assertTrue(len(passwd) >= 6)
msg = "user %s has a weak password" %(data['name'])
self.assertTrue(passwd != 'password', msg)
self.assertTrue(passwd != 'password123', msg)
def test_dummy(self):
pass
if __name__ == '__main__':
unittest.main()
运行
在命令行里输入 python test_password_1.py来运行用例,结果如下
$ python
test_password_1.py
set up
.set up
F
======================================================================
FAIL: test_week_password (__main__.PasswordTeseCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_password_1.py", line 21, in test_week_password
self.assertTrue(passwd != 'password123', msg)
AssertionError: False is not true : user tom has a weak password
----------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
解释一下
- setUp方法运行了2次,所以打印出了2次’set up’,这是因为上面的用例中有2个测试方法(2个方法名以test开头的方法),setUp会在每个测试方法执行之前执行1次
- 由于用户tom的密码是弱密码password123,所以上面的用例运行失败了,打印出1个”F”,有几个F就代表有几个测试用例失败
- 测试方法运行失败时测试结果里会打印出失败方法的方法名,因此好的测试方法名可以方便我们快速找出失败用例
- 为了让错误信息更加容易理解,我们经常会自定义断言出错提示消息,比如msg = "user %s has a weak password"
%(data['name'])。一旦断言失败,我们一眼就能看出是哪个用户的密码强度不够
亲自动手试一试
假设我们增加1条测试数据,如下所示
[
{"name":"jack","password":"Iloverose"},
{"name":"rose","password":"Ilovejack"},
{"name":"tom","password":"password123"},
{"name":"jerry","password":"password"}
]
再运行上面的用例,观察一下测试结果是否会有不同?如果没有不同,那是为什么?
4. 实例: 读取测试数据并测试弱密码
2018年10月7日 星期日
11:42
背景
接上一节的弱密码例子,我们的用例尽管运行的不错,但还是有点问题。
假如我们需要增加一些测试数据,那么我们就必须去修改setUp方法,在test_data列表中增加数据,频繁修改代码以适应一些不变的测试场景,这是没有必要的开销,可以想办法去优化。
我们可以把测试数据保存在文件里,通过读取文件的方式,每次动态从测试用例读取数据,这样数据的改变并不会影响用例,用例逻辑相对稳定,维护成本得到一定的降低。
设计测试数据
我们可以把测试数据保存成json格式,json格式的数据在各个语言间有较好的通用性,比较适合复用。
新建user_data.json文件,内容如下
[
{"name":"jack","password":"Iloverose"},
{"name":"rose","password":"Ilovejack"},
{"name":"tom","password":"password123"}
]
使用python的json库解析上面的json文件,可以得到如上节中test_data一致的数据。
代码
新建test_password_2.py,内容如下
import unittest
import json
class PasswordWithJsonTeseCase(unittest.TestCase):
data_file_path = './user_data.json'
def setUp(self):
print('set up')
self.test_data =
json.loads(open(self.data_file_path).read())
def test_week_password(self):
for data in
self.test_data:
passwd = data['password']
self.assertTrue(len(passwd) >= 6)
msg = "user %s has a weak password" %(data['name'])
self.assertTrue(passwd != 'password', msg)
self.assertTrue(passwd != 'password123', msg)
def test_dummy(self):
pass
if __name__ == '__main__':
unittest.main()
跟上一节相比,最大的不同点是现在test_data通过解析json文件的方式来赋值self.test_data
= json.loads(open(self.data_file_path).read())。
执行测试文件,结果应该与上一节一致。
发现问题
上面的代码有2个测试方法:test_week_password和test_dummy。由于setUp会在每个测试方法执行之前执行一次,那么setUp方法会执行2次,相应的json文件也会读取2次。如果测试方法多的话,那么反复读取json文件对性能来说是一个巨大的挑战。
优化
对于上面的测试数据读取场景,我们可以在所有测试方法执行前读取一次数据,毕竟测试数据在所有测试方法执行过程中是保持不变的。
setUpClass()和tearDownClass()
- setUpClass方法在每个测试用例类执行之前会执行一次,接收该class作为唯一的参数,并且必须使用装饰器classmethod()
- tearDownClass:
在所有测试方法执行完之后被调用1次,调用方式跟上面的方法类似
重构
下面我们重构代码以达到只读取1次测试数据的目的,新建文件`test_password_3.py,内容如下
import unittest
import json
class WeakPasswordTeseCase(unittest.TestCase):
@classmethod
def setUpClass(kls):
data_file_path = './user_data.json'
print('before all test methods')
with open(data_file_path) as
f:
kls.test_data =
json.loads(f.read())
def test_week_password(self):
for data in
self.test_data:
passwd = data['password']
self.assertTrue(len(passwd) >= 6)
msg = "user %s has a weak password" %(data['name'])
self.assertTrue(passwd != 'password', msg)
self.assertTrue(passwd != 'password123', msg)
def test_dummy(self):
pass
if __name__ == '__main__':
unittest.main()
有几点需要提及一下
- 使用open 方法的with模式可以在读取文件后自动关闭文件
- 在setUpClass方法中可以直接设置变量,比如kls.test_data = json.loads(f.read()),在其他测试方法中可以被访问
5. 实例: 找出所有是弱密码的用户
2018年10月7日 星期日
11:43
背景
当我们的测试数据是下面这些的时候,我们的用例是有问题的。
[
{"name":"jack","password":"Iloverose"},
{"name":"rose","password":"Ilovejack"},
{"name":"tom","password":"password123"},
{"name":"jerry","password":"password"}
]
我们的用例只能找出tom是弱密码的用户,jerry这个用户会成为漏网之鱼。
为什么
这是因为在unittest中,一旦某个测试方法中的断言失败,后续的断言都不会被执行。
还原一下上面的例子,当用例在断言tom失败后,for循环就退出了,测试方法也执行完毕了,后面jerry这条数据就不会被断言。
怎么办
我们可以重构一下我们的用例,让整个用例只断言1次,断言失败以后就把弱密码的用户打印出来。
代码
修改user_data.json文件,加入一些测试数据,修改后的user_data.json文件应该是
[
{"name":"jack","password":"Iloverose"},
{"name":"rose","password":"Ilovejack"},
{"name":"tom","password":"password123"},
{"name":"jerry","password":"password"},
{"name":"fred","password":"123456"},
{"name":"elma","password":"654321"}
]
新建test_password_4.py文件,内容如下
import unittest
import json
class WeakPasswordTeseCase1(unittest.TestCase):
@classmethod
def setUpClass(kls):
data_file_path = './user_data.json'
print('before all test methods')
with open(data_file_path) as
f:
kls.test_data = json.loads(f.read())
def test_week_password(self):
res = True
msg = []
for data in
self.test_data:
passwd = data['password']
tmp_res = True
tmp_res = tmp_res and
(len(passwd) >= 6)
tmp_res = tmp_res and
(passwd != 'password')
tmp_res = tmp_res and
(passwd != 'password123')
if not tmp_res:
msg.append("user %s has a weak password %s" %(data['name'], data['password']))
res = res and tmp_res
self.assertTrue(res, "\n".join(msg))
def test_dummy(self):
pass
if __name__ == '__main__':
unittest.main()
运行及结果
在命令行中运行python
test_password_4.py,结果如下
$ python
test_password_4.py
before all test methods
.F
======================================================================
FAIL: test_week_password (__main__.WeakPasswordTeseCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_password_4.py", line 27, in test_week_password
self.assertTrue(res, "\n".join(msg))
AssertionError: user tom has a weak password password123
user jerry has a weak password password
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
我们能学到什么
- 断言一旦失败之后测试方法就会结束运行,所以一般来说1个测试方法推荐只有1个断言
- 如果一个测试方法里面必须要有多个断言,那么要确保前面的断言失败之后,后面的断言就算不运行也不会影响测试的范围和结果
- for循环中的断言一旦失败,for循环就退出了
- 上面演示的测试用例写法其实具备了一定的数据驱动测试的思想
6. 命令行接口
2018年10月7日 星期日
11:44
背景
unittest支持命令行接口,我们可以在命令行里指定运行具体的测试用例。
实例
在test_password_1.py中定义了PasswordTeseCase用例,我们可以从命令行中指定只运行该用例。
$
python -m unittest test_password_1.PasswordTeseCase
set up
.set up
F
======================================================================
FAIL: test_week_password (test_password_1.PasswordTeseCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/easonhan/code/testclass.net/src/pyunit/test_password_1.py", line 21, in
test_week_password
self.assertTrue(passwd != 'password123', msg)
AssertionError: False is not true : user tom has a weak password
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
还可以使用-v参数来获得更详细的输出
$
python -m unittest test_password_1.PasswordTeseCase -v
test_dummy (test_password_1.PasswordTeseCase) ... set up
ok
test_week_password (test_password_1.PasswordTeseCase) ... set up
FAIL
======================================================================
FAIL: test_week_password (test_password_1.PasswordTeseCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/easonhan/code/testclass.net/src/pyunit/test_password_1.py", line 21, in
test_week_password
self.assertTrue(passwd != 'password123', msg)
AssertionError: False is not true : user tom has a weak password
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
也可以在命令行中一次指定多个测试用例类,具体的大家可以自己尝试一下。
7. 各种断言方法
2018年10月7日 星期日
11:45
背景
unittest支持各种断言方法。
断言列表
方法 |
检查点 |
assertEqual(a, |
a == b |
assertNotEqual(a, |
a != b |
assertTrue(x) |
bool(x) |
assertFalse(x) |
bool(x) |
assertIs(a, |
a is b |
assertIsNot(a, |
a is not |
assertIsNone(x) |
x is |
assertIsNotNone(x) |
x is not |
assertIn(a, |
a in b |
assertNotIn(a, |
a not in |
assertIsInstance(a, |
isinstance(a, |
assertNotIsInstance(a, |
not |
assertRaises(exc, |
fun(*args, |
assertRaisesRegexp(exc, |
fun(*args, |
assertAlmostEqual(a, |
round(a-b, |
assertNotAlmostEqual(a, |
round(a-b, |
assertGreater(a, |
a > b |
assertGreaterEqual(a, |
a >= |
assertLess(a, |
a < b |
assertLessEqual(a, |
a <= |
assertRegexpMatches(s, |
r.search(s) |
assertNotRegexpMatches(s, |
not |
assertItemsEqual(a, |
sorted(a) |
assertDictContainsSubset(a, |
a里面所有的键值对都在b中存在 |
自己动手
大家可以自己动手亲自尝试使用这些断言方法。
8. 断言异常
2018年10月7日 星期日
11:45
背景
我们有时候需要断言一些方法会抛出异常,这些异常需要符合我们的预期。
代码
新建test_exception.py文件,内容如下
import unittest
class DivZeroTestCase(unittest.TestCase):
def test_should_raise_exception(self):
with self.assertRaises(ZeroDivisionError):
1 / 0
if __name__ == '__main__':
unittest.main()
运行及结果
$ python
test_exception.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
我们能学到什么
- 上面的例子断言了当0作为除数的时候会抛出ZeroDivisionError
- 断言异常是有套路的,使用with语句加assertRaises,assertRaises的参数中传入预期的异常(这些异常可能需要先import进来),在with的子句中放上会抛出异常的语句或表达式
一篇文章了解_unittest的更多相关文章
- 一篇文章让Oracle程序猿学会MySql【未完待续】
一篇文章让Oracle DB学会MySql[未完待续] 随笔前言: 本篇文章是针对已经能够熟练使用Oracle数据库的DB所写的快速学会MySql,为什么敢这么说,是因为本人认为Oracle在功能性方 ...
- 由一篇文章引发的思考——多线程处理大数组
今天领导给我们发了一篇文章文章,让我们学习一下. 文章链接:TAM - Threaded Array Manipulator 这是codeproject上的一篇文章,花了一番时间阅读了一下.文章主要是 ...
- 一篇文章告诉你为何GitHub估值能达20亿美元
软件开发平台GitHub今日宣布,已获得硅谷多家知名风投2.5亿美元融资,这也让其融资总额达到了3.5亿美元,此轮融资对GitHub的估值约为20亿美元. GitHub有何特别之处? GitHub创立 ...
- DEDECMS教程:上/下一篇文章标题长度的截取方法
对dedecms了解的朋友们,想必对如何获取上一篇.下一篇文章的标签也是非常熟悉.dedecms获取上一篇.下一篇文章的标签分别为:{dede:prenext get='pre'/}.{dede:pr ...
- Android:学习AIDL,这一篇文章就够了(下)
前言 上一篇博文介绍了关于AIDL是什么,为什么我们需要AIDL,AIDL的语法以及如何使用AIDL等方面的知识,这一篇博文将顺着上一篇的思路往下走,接着介绍关于AIDL的一些更加深入的知识.强烈建议 ...
- Android:学习AIDL,这一篇文章就够了(上)
前言 在决定用这个标题之前甚是忐忑,主要是担心自己对AIDL的理解不够深入,到时候大家看了之后说——你这是什么玩意儿,就这么点东西就敢说够了?简直是坐井观天不知所谓——那样就很尴尬了.不过又转念一想, ...
- 一篇文章一张思维导图看懂Android学习最佳路线
一篇文章一张思维导图看懂Android学习最佳路线 先上一张android开发知识点学习路线图思维导图 Android学习路线从4个阶段来对Android的学习过程做一个全面的分析:Android初级 ...
- (转) TensorFlow深度学习,一篇文章就够了
TensorFlow深度学习,一篇文章就够了 2016/09/22 · IT技术 · TensorFlow, 深度学习 分享到:6 原文出处: 我爱计算机 (@tobe迪豪 ) 作者: 陈迪 ...
- php下删除一篇文章生成的多个静态页面
php自定义函数之删除一篇文章生成的多个静态页面,可能有多页的文章,都是需要考虑到的. 复制代码代码如下: //– 删除一篇文章生成的多个静态页面 //– 生成的文章名为 5.html 5_2.ht ...
随机推荐
- RabbitMQ消息队列总结
AMQP[高级消息队列协议] 是一个异步消息传递所使用的应用层协议规范(是线路层协议)AMQP 客户端能够无视消息的来源任意发送和接受信息 队列的使用场景: 1.与业务的主要逻辑无关,但又需要执行,就 ...
- JavaSE学习笔记01注释、标识符与基本类型
1. HelloWorld 编写代码 public class Hello{ public static void main(String[] args){ System.out.println(&q ...
- mac 搭建 Robot Framework
前提介绍,我的mac上python2和python3是都要有的,然后大家可以看看我其他的文章,这些文章虽然很多都是连接,是别人的博客或者资料,但都是自己试过没有问题的,只是比较懒然后就没有自己写. r ...
- 渗透测试之GoogleHack
GoogleHack 1,介绍: 使用google等搜索引擎对某些特定的网络主机漏洞(一般是服务器上的脚本漏洞)进行搜索,都能达到快速找到1漏洞的目的,然而,google相对百度讲,没有广告,搜索引擎 ...
- 国内npm镜像源设置
淘宝npm镜像 搜索地址:http://npm.taobao.org/ registry地址:http://registry.npm.taobao.org/ cnpmjs镜像 搜索地址:http:// ...
- java 常用快捷键及命令积累
ctl + shift + o--->导入所需包,删掉没有被引用的包 ctl + / --->添加多行注释 ctl + \--->删除多行注释
- 读书摘要观后感与总结:《Glibc内存管理:ptmalloc2源代码分析》
更新中 在Linux平台下做漏洞利用的时候,针对于Heap部分总是有些不求甚解,下面开个博文来记录下<Glibc内存管理:ptmalloc2源代码分析>这本书的读后感和收获,一些简单的点将 ...
- java数据结构-07栈
一.什么是栈 栈是一种线性结构,栈的特点就是先进后出(FILO):就像弹夹装子弹一样,最先压进去的在最底下,最后才被射出. 二.相关接口设计 三.栈的实现 栈可以用之前的数组.链表等设计,这里我使 ...
- 【源码】spring生命周期
一.spring生命周期 1. 实例化Bean 对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用crea ...
- 【总结】HTTP
一.HTTP 1.http HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol),HTTP 是一个在计算机世界里专门在两点之间传输文字.图片.音频.视频等超文本数 ...