引言

本文主要介绍了如下内容

单元测试的定义;

python中如何写基础的unittest单元测试;

详解unittest中的基础知识点:断言、测试固件、suite、如何控制用例执行顺序、如何把测试结果输出到文件;

详解unittest中的高级知识点:@unittest.skip、@unittest.expectedFailure、failfast、参数化;


什么是单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

比如对于函数abs(),我们可以编写的测试用例为:

(1)输入正数,比如1、1.2、0.99,期待返回值与输入相同

(2)输入复数,比如-1、-1.2、-0.99,期待返回值与输入相反

(3)输入0,期待返回0

(4)输入非数值类型,比如None、[]、{}、期待抛出TypeError

把上面这些测试用例放到一个测试模块里,就是一个完整的单元测试。

unittest工作原理

unittest中最核心的四部分是:TestCase,TestSuite,TestRunner,TestFixture

(1)一个TestCase的实例就是一个测试用例。测试用例就是指一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。

(2)而多个测试用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。

(3)TestLoader是用来加载TestCase到TestSuite中的。

(4)TextTestRunner是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法

(5)测试的结果会保存到TextTestResult实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息。

综上,整个流程就是首先要写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,整个过程集成在unittest.main模块中。核心类图关系如下:

 

上手unittest读这篇文章就够了_unittest

unittest的基本使用方法

1)import unittest

2)定义一个继承自unittest.TestCase的测试用例类

3)定义setUp和tearDown,在每个测试用例前后做一些辅助工作。

4)定义测试用例,名字以test开头

5)一个测试用例应该只测试一个方面,测试目的和测试内容应很明确。主要是调用assertEqual、assertRaises等断言方法判断程序执行结果和预期值是否相符。

6)调用unittest.main()启动测试

下面举个实例,来看看unittest如何测试一个简单的函数

测一个简单的加减乘除接口

mathfunc.py文件代码如下:

def add(a, b):

    return a + b

def minus(a, b):

    return a - b

def multi(a, b):

    return a * b

def divide(a, b):

    return a / b

 test_mathfunc.py文件代码如下:

import unittest

 from mathfunc import *

class TestMathFunc (unittest.TestCase):

    def test_add(self):

        self.assertEqual (3, add (1, 2))

    def test_minus(self):

        self.assertEqual (1, minus (3, 2))

    def test_multi(self):

        self.assertEqual (6, multi (3, 2))

    def test_divide(self):

        self.assertEqual (2, divide (6, 2))

if __name__ == '__main__':

    unittest.main ()

 

在pycharm中运行该用例:

 

上手unittest读这篇文章就够了_ide_02

输出:

 

上手unittest读这篇文章就够了_ide_03

 

可以看到一共运行了4个测试,失败了1个,并且给出了失败原因,2 != 3.0,也就是说我们的divide方法是有问题的。上图的左侧显示了用例的运行状态,右侧显示了具体的用例运行信息。

备注:在unitest.main()中加verbosity参数可以控制输出的错误报告的详细程度,用命令行的方式运行脚本会发现区别。

0 (静默模式): 你只能获得总的测试用例数和总的结果 ;

1 (默认模式): 非常类似静默模式 只是在每个成功的用例前面有个“.” 每个失败的用例前面有个 “F” ;

2 (详细模式):测试结果会显示每个测试用例的所有相关的信息;

设置unitest.main(verbosity=2),在cmd中运行 python test_mathfunc.py,运行结果如下:

test_add (__main__.TestMathFunc) ... ok

test_divide (__main__.TestMathFunc) ... FAIL

test_minus (__main__.TestMathFunc) ... ok

test_multi (__main__.TestMathFunc) ... ok

 

======================================================================

FAIL: test_divide (__main__.TestMathFunc)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "unittest_demo.py", line 32, in test_divide

    self.assertEqual (2, divide (6, 2))

AssertionError: 2 != 3.0

 

----------------------------------------------------------------------

Ran 4 tests in 0.003s

 

断言

在测试用例中,执行完测试用例后,最后一步是判断测试结果是pass还是fail,自动化测试脚本里面一般把这种生成测试结果的方法称为断言(assert)。

Python中的断言类型丰富,主要包括:

基础断言

 

上手unittest读这篇文章就够了_unittest_04

exceptions, warnings, 日志信息断言

 

上手unittest读这篇文章就够了_ide_05

特殊断言

 

上手unittest读这篇文章就够了_ide_06

集合断言

 

上手unittest读这篇文章就够了_单元测试_07

组织TestSuite

上面的测试用例在执行的时候没有按照顺序执行,如果想要让用例按照你设置的顺序执行就用到了TestSuite。我们添加到TestSuite中的case是会按照添加的顺序执行的。现在我们只有一个测试文件,如果有多个测试文件,也可以用TestSuite组织起来。继续上面加减乘除的例子,现在再新建一个文件,test_suite.py,代码如下:

    import unittest

    from test_mathfunc import TestMathFunc

    if __name__ == '__main__':

        suite = unittest.TestSuite()

        tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")]

        suite.addTests(tests)

        runner = unittest.TextTestRunner(verbosity=2)

        runner.run(suite)

执行结果如下:

    test_add (test_mathfunc.TestMathFunc) ... ok

    test_minus (test_mathfunc.TestMathFunc) ... ok

    test_divide (test_mathfunc.TestMathFunc) ... FAIL

 

    ======================================================================

    FAIL: test_divide (test_mathfunc.TestMathFunc)

    ----------------------------------------------------------------------

    Traceback (most recent call last):

      File "D:\pythonWorkspace\HTMLTest\test_mathfunc.py", line 20, in test_divide

        self.assertEqual(2, divide(6, 2))

    AssertionError: 2 != 3.0

 

    ----------------------------------------------------------------------

    Ran 3 tests in 0.000s

 

FAILED (failures=1)

当测试用例达到上百条时,上述加载用例的方法效率就变得很差了,unittest提供了discover方法来解决这一问题,该方法可以自动识别测试用例,具体用法如下:

discover(start_dir,pattern='test*.py',top_level_dir= None)

找到指定目录下所有测试模块,并可递归查到子目录下的测试模块,只有匹配到文件名时才加载

start_dir:要测试的模块名或测试用例目录

pattern='case*.py':表示用例文件名的匹配原则。此处匹配以“case”开头的.py 类型的文件,* 表示任意多个字符

top_level_dir= None 测试模块的顶层目录,如果没有顶层目录,默认为None

实例代码:

test_dir='./' #表示当前目录

discover=unittest.defaultTestLoader.discover(test_dir,pattern='case*.py')

runner = unittest.TextTestRunner()

runner.run(discover)

suite相关方法

loadTestsFromTestCase(testCaseClass)

testCaseClass必须是TestCase的子类(或孙类也行)

loadTestsFromModule(module, pattern=None)

test case所在的module

loadTestsFromName(name, module=None)

name是一个string,string需要是是这种格式的“module.class.method”

loadTestsFromNames(name, module=None)

names是一个list,用法与上同

discover(start_dir, pattern=’test*.py’, top_level_dir=None)

从python文件中获取test cases

用例顺序执行

unittest在执行用例(test_xxx)时,并不是按从上到下的顺序执行,有特定的顺序。unittest框架默认根据ACSII码的顺序加载测试用例,数字与字母的顺序为:0~9,A~Z,a~z。

对于类来说,class TestAxx 会优先于class TestBxx被执行。

对于方法来说,test_aaa()方法会有优先于test_bbb()被执行。

所以我们设计用例时,如果各个用例有执行先后的顺序关系,可以参考如下:

def test_1_***

def test_2_***

def test_n_***

将结果输出到文件

现在我们的测试结果只能输出到控制台,现在我们想将结果输出到文件中以便后续可以查看。

将test_suite.py进行一点修改,代码如下: 

    import unittest

    from test_mathfunc import TestMathFunc

    if __name__ == '__main__':

        suite = unittest.TestSuite()

        tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")]

        suite.addTests(tests)

        with open('UnittestTextReport.txt', 'a') as  f:

            runner = unittest.TextTestRunner(stream=f, verbosity=2)

            runner.run(suite)

 

运行该文件,就会发现目录下生成了'UnittestTextReport.txt,所有的执行报告均输出到了此文件中。

测试固件——testfixture

当遇到要启动一个数据库这种情况时,只想在开始时连接上数据库,在结束时关闭连接。那么可以使用setUp和tearDown函数。

    class TestDict(unittest.TestCase):

        def setUp(self):

            print ('setUp...')

        def tearDown(self):

            print ('tearDown...')

 

这两个方法在每个测试方法执行前以及执行后执行一次,setUp用来为测试准备环境,tearDown用来清理环境,以备后续的测试。

如果想要在所有case执行之前准备一次环境,并在所有case执行结束之后再清理环境,我们可以用setUpClass()与tearDownClass(),代码格式如下:

    class TestMathFunc(unittest.TestCase):

        @classmethod

        def setUpClass(cls):

            createConnection()

        @classmethod

        def tearDownClass(cls):

          closeConnection()

同理,unittest也提供了模块的fixture

def setUpModule():

    createConnection()

def tearDownModule():

    closeConnection()

 

跳过某个case——@unittest.skip

unittest提供了几种方法可以跳过case

(1)skip装饰器

代码如下

    import unittest

    from mathfunc import *

    class TestMathFunc(unittest.TestCase):

 

         .....

 

        @unittest.skip("i don't want to run this case.")

        def test_minus(self):

            self.assertEqual(1, minus(3, 2))

输出:

    test_add (test_mathfunc.TestMathFunc) ... ok

    test_minus (test_mathfunc.TestMathFunc) ... skipped "i don't want to run this case."

    test_divide (test_mathfunc.TestMathFunc) ... FAIL

 

    ======================================================================

    FAIL: test_divide (test_mathfunc.TestMathFunc)

    ----------------------------------------------------------------------

    Traceback (most recent call last):

      File "D:\pythonWorkspace\HTMLTest\test_mathfunc.py", line 28, in test_divide

        self.assertEqual(2, divide(6, 2))

    AssertionError: : 2 != 3.0

 

    ----------------------------------------------------------------------

    Ran 3 tests in 0.000s

 

    FAILED (failures=1, skipped=1)

 

skip装饰器一共有三个

unittest,skip(reason):无条件跳过

unittest.skipIf(condition, reason):当condition为True时跳过

unittest.skipUnless(condition, reason):当condition为False时跳过

(2)TestCase.skipTest()方法

    class TestMathFunc(unittest.TestCase):

    ...

    def test_minus(self):

            self.skipTest('do not run this.')

            self.assertEqual(1, minus(3, 2))

输出:

    test_add (test_mathfunc.TestMathFunc) ... ok

    test_minus (test_mathfunc.TestMathFunc) ... skipped 'do not run this.'

    test_divide (test_mathfunc.TestMathFunc) ... FAIL

 

    ======================================================================

    FAIL: test_divide (test_mathfunc.TestMathFunc)

    ----------------------------------------------------------------------

    Traceback (most recent call last):

      File "D:\pythonWorkspace\HTMLTest\test_mathfunc.py", line 20, in test_divide

        self.assertEqual(2, divide(6, 2))

    AssertionError: 2 != 3.0

 

    ----------------------------------------------------------------------

    Ran 3 tests in 0.000s

 

FAILED (failures=1, skipped=1)

预期失败—— @unittest.expectedFailure

@unittest.expectedFailure标记用例为预期是失败的,如果用例执行失败,认为是success;如果用例执行成功,则认为是failure。

例如代码:

import unittest

class ExpectedFailureTestCase(unittest.TestCase):   

@unittest.expectedFailure   

def test_fail1(self):       

      self.assertEqual(1, 0,)   

@unittest.expectedFailure   

def test_fail2(self):       

      self.assertEqual (1, 1)

@unittest.expectedFailure

def test_fail3(self):       

    self.assertEqual (1, 1)

短路测试—— failfast

failfast是TestResult的一个属性,缺省为False。 如果failfast为True,一旦测试集中有测试案例failed或发生error立即终止当前整个测试执行,跳过剩下所有测试用例。

实现“短路测试”,设置failfast为True,具体操作如下:

unittest.main(failfast=True)

unittest.TextTestRunner(failfast=True)

实例如下:

import unittest

class TestMathFunc (unittest.TestCase):

    def test_add(self):

        self.assertEqual (3, add (1, 2))

    def test_minus(self):

        self.assertEqual (1, minus (3, 2))

    def test_multi(self):

        self.assertEqual (6, multi (3, 2))

    def test_divide(self):

        self.assertEqual (2, divide (6, 2))

if __name__ == '__main__':

    unittest.main (failfast=True)

使用命令行的方式运行代码,结果如下:

test_add (__main__.TestMathFunc) ... ok

test_divide (__main__.TestMathFunc) ... FAIL

 

======================================================================

FAIL: test_divide (__main__.TestMathFunc)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "unittest_demo.py", line 32, in test_divide

    self.assertEqual (2, divide (6, 2))

AssertionError: 2 != 3.0

 

----------------------------------------------------------------------

Ran 2 tests in 0.001s

 

FAILED (failures=1)

可以看到用例test_divide失败后,用例test_minus和test_multi没有继续运行

用例的参数化

unittest从Python3.4版本开始支持参数化,使用方法subTest()

具体代码如下

class ParaTest(unittest.TestCase):

   def test_1(self):

        for i in range(0, 4):

            with self.subTest(i=i):

                self.assertEqual(i % 2, 0)

输出结果如下:

上手unittest读这篇文章就够了_python_08

详细的错误信息如下:

 

上手unittest读这篇文章就够了_python_09