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,
b)

a == b

assertNotEqual(a,
b)

a != b

assertTrue(x)

bool(x)
is True

assertFalse(x)

bool(x)
is False

assertIs(a,
b)

a is b

assertIsNot(a,
b)

a is not
b

assertIsNone(x)

x is
None

assertIsNotNone(x)

x is not
None

assertIn(a,
b)

a in b

assertNotIn(a,
b)

a not in
b

assertIsInstance(a,
b)

isinstance(a,
b)

assertNotIsInstance(a,
b)

not
isinstance(a, b)

assertRaises(exc,
fun, *args, **kwds)

fun(*args,
**kwds) raises exc

assertRaisesRegexp(exc,
r, fun, *args, **kwds)

fun(*args,
**kwds) raises exc and the message matches regex r

assertAlmostEqual(a,
b)

round(a-b,
7) == 0

assertNotAlmostEqual(a,
b)

round(a-b,
7) != 0

assertGreater(a,
b)

a > b
2.7

assertGreaterEqual(a,
b)

a >=
b

assertLess(a,
b)

a < b

assertLessEqual(a,
b)

a <=
b

assertRegexpMatches(s,
r)

r.search(s)

assertNotRegexpMatches(s,
r)

not
r.search(s)

assertItemsEqual(a,
b)

sorted(a)
== sorted(b) 也支持unhashable对象

assertDictContainsSubset(a,
b)

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的更多相关文章

  1. 一篇文章让Oracle程序猿学会MySql【未完待续】

    一篇文章让Oracle DB学会MySql[未完待续] 随笔前言: 本篇文章是针对已经能够熟练使用Oracle数据库的DB所写的快速学会MySql,为什么敢这么说,是因为本人认为Oracle在功能性方 ...

  2. 由一篇文章引发的思考——多线程处理大数组

    今天领导给我们发了一篇文章文章,让我们学习一下. 文章链接:TAM - Threaded Array Manipulator 这是codeproject上的一篇文章,花了一番时间阅读了一下.文章主要是 ...

  3. 一篇文章告诉你为何GitHub估值能达20亿美元

    软件开发平台GitHub今日宣布,已获得硅谷多家知名风投2.5亿美元融资,这也让其融资总额达到了3.5亿美元,此轮融资对GitHub的估值约为20亿美元. GitHub有何特别之处? GitHub创立 ...

  4. DEDECMS教程:上/下一篇文章标题长度的截取方法

    对dedecms了解的朋友们,想必对如何获取上一篇.下一篇文章的标签也是非常熟悉.dedecms获取上一篇.下一篇文章的标签分别为:{dede:prenext get='pre'/}.{dede:pr ...

  5. Android:学习AIDL,这一篇文章就够了(下)

    前言 上一篇博文介绍了关于AIDL是什么,为什么我们需要AIDL,AIDL的语法以及如何使用AIDL等方面的知识,这一篇博文将顺着上一篇的思路往下走,接着介绍关于AIDL的一些更加深入的知识.强烈建议 ...

  6. Android:学习AIDL,这一篇文章就够了(上)

    前言 在决定用这个标题之前甚是忐忑,主要是担心自己对AIDL的理解不够深入,到时候大家看了之后说——你这是什么玩意儿,就这么点东西就敢说够了?简直是坐井观天不知所谓——那样就很尴尬了.不过又转念一想, ...

  7. 一篇文章一张思维导图看懂Android学习最佳路线

    一篇文章一张思维导图看懂Android学习最佳路线 先上一张android开发知识点学习路线图思维导图 Android学习路线从4个阶段来对Android的学习过程做一个全面的分析:Android初级 ...

  8. (转) TensorFlow深度学习,一篇文章就够了

    TensorFlow深度学习,一篇文章就够了 2016/09/22 · IT技术 · TensorFlow, 深度学习 分享到:6   原文出处: 我爱计算机 (@tobe迪豪 )    作者: 陈迪 ...

  9. php下删除一篇文章生成的多个静态页面

    php自定义函数之删除一篇文章生成的多个静态页面,可能有多页的文章,都是需要考虑到的. 复制代码代码如下: //– 删除一篇文章生成的多个静态页面  //– 生成的文章名为 5.html 5_2.ht ...

随机推荐

  1. RabbitMQ消息队列总结

    AMQP[高级消息队列协议] 是一个异步消息传递所使用的应用层协议规范(是线路层协议)AMQP 客户端能够无视消息的来源任意发送和接受信息 队列的使用场景: 1.与业务的主要逻辑无关,但又需要执行,就 ...

  2. JavaSE学习笔记01注释、标识符与基本类型

    1. HelloWorld 编写代码 public class Hello{ public static void main(String[] args){ System.out.println(&q ...

  3. mac 搭建 Robot Framework

    前提介绍,我的mac上python2和python3是都要有的,然后大家可以看看我其他的文章,这些文章虽然很多都是连接,是别人的博客或者资料,但都是自己试过没有问题的,只是比较懒然后就没有自己写. r ...

  4. 渗透测试之GoogleHack

    GoogleHack 1,介绍: 使用google等搜索引擎对某些特定的网络主机漏洞(一般是服务器上的脚本漏洞)进行搜索,都能达到快速找到1漏洞的目的,然而,google相对百度讲,没有广告,搜索引擎 ...

  5. 国内npm镜像源设置

    淘宝npm镜像 搜索地址:http://npm.taobao.org/ registry地址:http://registry.npm.taobao.org/ cnpmjs镜像 搜索地址:http:// ...

  6. java 常用快捷键及命令积累

    ctl + shift + o--->导入所需包,删掉没有被引用的包 ctl + / --->添加多行注释 ctl + \--->删除多行注释

  7. 读书摘要观后感与总结:《Glibc内存管理:ptmalloc2源代码分析》

    更新中 在Linux平台下做漏洞利用的时候,针对于Heap部分总是有些不求甚解,下面开个博文来记录下<Glibc内存管理:ptmalloc2源代码分析>这本书的读后感和收获,一些简单的点将 ...

  8. java数据结构-07栈

    一.什么是栈 栈是一种线性结构,栈的特点就是先进后出(FILO):就像弹夹装子弹一样,最先压进去的在最底下,最后才被射出.  二.相关接口设计  三.栈的实现 栈可以用之前的数组.链表等设计,这里我使 ...

  9. 【源码】spring生命周期

    一.spring生命周期 1. 实例化Bean 对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用crea ...

  10. 【总结】HTTP

    一.HTTP 1.http HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol),HTTP 是一个在计算机世界里专门在两点之间传输文字.图片.音频.视频等超文本数 ...