python 单元测试 -- unittest

@(python)

单元测试是对程序中的单个子程序、函数、过程进行的测试,面向白盒测试。
单元测试测试覆盖常用子程序的输入组合,边界条件和异常处理,尽可能保证单元测试代码简洁,避免单测本身代码有 bug 影响对测试对象的测试结果。

python 提供单元测试框架 unittest,

简单编写一个模块 calculator.py ,作为单元测试对象

#!/usr/bin/env python
# coding=utf-8

def my_print(str):
    pass
    #print(str)

class Calculator():
    __version__ = 4
    def __init__(self, a, b):
        my_print("cal init")
        self.a = int(a)
        self.b = int(b)

    def __del__(self):
        my_print("cal del")

    def add(self):
        return self.a + self.b

    def sub(self):
        return self.a - self.b

    def mul(self):
        return self.a * self.b

    def div(self):
        return self.a / self.b

编写测试用例 (test case)

如上, 我们为该模块编写对应的单元测试,取名 testCalculator.py :

#!/usr/bin/env python
# coding=utf-8
import unittest
from calculator import Calculator

class CalculatorTest(unittest.TestCase):

    def test_add_0(self):
        cal = Calculator(8, 4)
        result = cal.add()
        self.assertEqual(result, 12)

    def test_add_1(self):
        cal = Calculator(8, 4)
        result = cal.add()
        self.assertNotEqual(result, 12)
        
    def will_not_callme(self):
        print("lalalla")
        
if __name__ == "__main__":
    unittest.main()

简单地编写了两个对模块方法 add() 的测试用例。编写单元测试,我们需要对应测试的对象实现一个类,继承 unittest.TestCase
测试类 CalculatorTest 中的测试用例都是以 test_, 其他方法在执行脚本的时候框架不会直接调用执行。
对应目标模块的各个方法编写测试用例,使用断言判断结果,注意使用的断言是 unittest.TestCase内置的,这样才能保证不会由于某个用例断言失败而直接退出执行。

执行 运行结果如下,可以看到,没有通过的例子断言了错误的行号,可以快速定位问题。

$  python testCalculator.py -v
test_add_0 (__main__.CalculatorTest) ... ok
test_add_1 (__main__.CalculatorTest) ... FAIL

======================================================================
FAIL: test_add_1 (__main__.CalculatorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "aa.py", line 16, in test_add_1
    self.assertNotEqual(result, 12)
AssertionError: 12 == 12

----------------------------------------------------------------------
Ran 2 tests in 0.002s

FAILED (failures=1)

内置的断言

Method Checks that New in
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 3.1
assertIsNot(a, b) a is not b 3.1
assertIsNone(x) x is None 3.1
assertIsNotNone(x) x is not None 3.1
assertIn(a, b) a in b 3.1
assertNotIn(a, b) a not in b 3.1
assertIsInstance(a, b) isinstance(a, b) 3.2
assertNotIsInstance(a, b) not isinstance(a, b) 3.2

测试初始化和清理(test fixture)

看到上面的例子,每次写一个测试用例都要重新定义一个测试实例 cal, 显得很重复,但是直接在测试类初始化函数定义的话又怕用例之间相互干扰。还有就是,有些测试,需要测试前构建测试场景,测试结束后清理。
类似以上的问题,unittest 提供了几个方法实现。

  • setUp() : 执行每个测试用例前都会调用,执行准备
  • tearDown() : 执行完每个用例后都会调用,执行清理

对应上面两个方法,下面两个在测试类函数开始和结束调用

  • setUpClass()
  • tearDownClass()

测试套件 (test suit)

测试套件是多个测试用例的集合
结合上面内容,看个相对完整的测试套件 :

#!/usr/bin/env python
# coding=utf-8

import sys
import unittest
from calculator import Calculator

class CalculatorTest(unittest.TestCase):

    def setUp(self):
        self.cal = Calculator(8, 4)
        #assert 1 == 2, "test if setUp error"

    def tearDown(self):
        self.cal = None

    def test_add(self):
        '''
        des : test add
        '''
        result = self.cal.add()
        self.assertEqual(result, 12)
        # if use python builtin assert , it will stop run
        # assert result == 12, "add error"

    def test_sub(self):
        result = self.cal.sub()
        self.assertEqual(result, 4)

    def test_mul(self):
        result = self.cal.mul()
        self.assertEqual(result, 32)

    def test_div(self):
        result = self.cal.div()
        self.assertEqual(result, 2)

    @unittest.skip('just skip')
    def test_div_1(self):
        result = self.cal.div()
        self.assertEqual(result, 3)

    @unittest.skipIf(Calculator.__version__ < 5, 'not support this library')
    def test_div_2(self):
        result = self.cal.div()
        self.assertEqual(result, 2)

    @unittest.skipIf(Calculator.__version__ < 2, 'not support this library')
    def test_div_3(self):
        result = self.cal.div()
        self.assertEqual(result, 2)

    @unittest.skipUnless(sys.platform.startswith('win'),  'windows')
    def test_div_4(self):
        result = self.cal.div()
        self.assertEqual(result, 2)

    @unittest.skipUnless(sys.platform.startswith('linux'),  'linux')
    def test_div_5(self):
        result = self.cal.div()
        self.assertEqual(result, 2)

def suite1():
    ## 执行测试用例构建条件
    suite = unittest.TestSuite()
    suite.addTest(CalculatorTest("test_add"))
    suite.addTest(CalculatorTest("test_sub"))
    return suite

def suite2():
    # 指定前缀构建测试套件
    suite = unittest.makeSuite(CalculatorTest, 'test')
    return suite

if __name__ == "__main__":
    ## 运行用例方法1
    #my_print("Test suit : run some testcase")
    #runner = unittest.TextTestRunner()
    #runner.run(suite1())

    ## 运行用例方法2
    #my_print("Test suit : run all testcase")
    #runner.run(suite2())

    ## 运行用例方法3
    # python ./testCalculator.py -v
    # python ./testCalculator.py -v testCalculator.test_add
    my_print("Run all testcase dircetly")
    unittest.main()

关注的点:

  • 测试用例指定条件,在不符合条件的情况下跳过不执行(见最后几个带修饰器的用例,对于跨平台什么实用)
  • 所有测试用例执行顺序与其在类中的定义顺序没有关系,不能依靠这个先后关系;并且不同用例之间最好不要相互依赖。

如上,运行所有测试用例,或者指定某个测试用例运行。

$ python testCalculator.py -v
test_add (__main__.CalculatorTest) ... ok
test_div (__main__.CalculatorTest) ... ok
test_div_1 (__main__.CalculatorTest) ... skipped 'just skip'
test_div_2 (__main__.CalculatorTest) ... skipped 'not support this library'
test_div_3 (__main__.CalculatorTest) ... ok
test_div_4 (__main__.CalculatorTest) ... skipped 'windows'
test_div_5 (__main__.CalculatorTest) ... ok
test_mul (__main__.CalculatorTest) ... ok
test_sub (__main__.CalculatorTest) ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK (skipped=3)

$ python testCalculator.py -v CalculatorTest.test_add
test_add (__main__.CalculatorTest) ... ok

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

OK

多个测试套件运行

$ python -m unittest discover 
### 结合一下参数, python自动匹配运行所有符合条件的测试用例
-v, --verbose
Verbose output
 
-s, --start-directory directory
Directory to start discovery (. default)
 
-p, --pattern pattern
Pattern to match test files (test*.py default)
 
-t, --top-level-directory directory
Top level directory of project (defaults to start directory)

或者直接新建一个文件,import 所有测试类

使用 mock

前面提到,单元测试测试最小单元,但是有时候测试,遇到测试模块需要依赖于其他模块,一个是不确定依赖的模块是否有问题;另一个是依赖模块容易构造获取,没有实现。比如网络消息分析包,需要接收网络字节之类的。
这种情况下,就需要对依赖的模块进行 mock,虚拟一个依赖模块供我们测试。

如上面的例子,假如原理的 add() 还没有实现

def  add(self):
    pass

测试用例类似如下, 对其进行mock

from unittest import mock

    def test_add(self):
        result = self.cal.add()
        self.assertNotEqual(result, 12)
        self.cal.add = mock.Mock(return_value=12)
        result = self.cal.add()
        self.assertEqual(result, 12)

大概的意思,详细根据实际需求了解使用
参考 python mock

--

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • 单元测试 什么是单元 单元测试(unit testing),是指对软件中的最小可测试单元(一个模块、一个函数或者一...
    PPMac阅读 6,452评论 0 19
  • unittest作为一个python中的基本模块,是其他框架和工具的基础,官方文档神马的最实用了:https://...
    cheneydc阅读 8,101评论 0 3
  • 本文试图总结编写单元测试的流程,以及自己在写单元测试时踩到的一些坑。如有遗漏,纯属必然,欢迎补充。 目录概览: 编...
    苏尚君阅读 3,401评论 0 4
  • Startup 单元测试的核心价值在于两点: 更加精确地定义某段代码的作用,从而使代码的耦合性更低 避免程序员写出...
    wuwenxiang阅读 10,070评论 1 27
  • 单元测试不是一个小工程,需要多用些时间才能做好,不要希望通过这个文章就能掌握单元测试,这只是一个入门,需要自己动手...
    勇不言弃92阅读 7,767评论 9 60