Python的单元测试工具——unittest小结

简介

unittest是Python的内建模块,是Python单元测试的事实标准,也叫PyUnit。使用unittest之前,先了解如下几个概念:

  • test case:测试用例,可以通过创建unitest.TestCase类的子类创建一个测试用例。
  • test fixture:包含执行测试用例前的测试准备工作、测试用例执行后的清理工作(分别对应TestCase中的setUp()tearDown()方法),测试准备和测试清理的目的是保证每个测试用例执行前后的系统状态一致。
  • test suite:测试套,是测试用例、测试套或者两者的集合,用来将有关联的测试项打包。
  • test runner:负责执行测试并将结果展示给用户,可以展示图形或文字形式(unittest.TextTestRunner)的结果,或者返回一个错误码标识测试用例的执行结果。test runner提供了一个方法run(),接受一个unittest.TestSuiteunittest.TestCase实例作为参数,执行对应测试项目后返回测试结果unittest.TestResult对象。

基本使用方法

定义测试用例的方法如下:

#unit.py
import unittest

class TestStringMethods(unittest.TestCase):
    def test_upper(self):
        self.assertEqual('Loo'.upper(), 'LOO')

    def test_isupper(self):
        self.assertTrue('LOO'.isupper())
        self.assertFalse('Loo'.isupper())

    def test_split(self):
        s = 'Mars Loo'
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()

执行脚本:

$ python unit.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

每一个测试项目的函数定义以test开头命名,这样test runner就知道哪些函数是应该被执行的。上面的例子展示了验证测试结果常用的三种方法:

  • assertEqual(a, b):比较a == b
  • assertTrue(exp)assertFalse(exp):验证bool(exp)为True或者False
  • assertRaises(Exception):验证Exception被抛出。

之所以不使用Python内建的assert抛出异常,是因为test runner需要根据这些封装后的方法抛出的异常做测试结果统计。

unittest.main()方法会在当前模块寻找所有unittest.TestCase的子类,并执行它们中的所有测试项目。使用-v参数可以看到更详细的测试执行过程:

$ python unit.py -v
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

也可以修改最后两行成如下代码:

suite = unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)
unittest.TextTestRunner(verbosity=2).run(suite)

测试结果如下:

$ python unit.py
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

从命令行运行unittest

$ python -m unittest unit          #直接运行模块unit中的测试用例
$ python -m unittest unit.TestStringMethods          #运行模块中的某个类
$ python -m unittest unit.TestStringMethods.test_upper          #运行某个单独的测试方法

混合运行测试模块、类以及测试方法也是可以的。

如果要查看unittest模块命令行的更多参数信息,使用-h参数:

$ python -m unittest -h
  • -b参数:只在测试用例fail或者error时显示它的stdout和stderr,否则不会显示。

  • -f参数:如果有一个测试用例fail或者出现error,立即停止测试。

  • -c参数:捕捉Control-C信号,并显示测试结果。

自动发现测试用例

unittest能够自动发现测试用例。为了让测试用例能够被自动发现,测试文件需要是在项目目录中可以import的module或者package,比如如下目录结构:

unittest
├── test_a
│   ├── __init__.py
│   └── test_a.py
└── test_b.py

在unittest目录中运行如下命令,即可运行test_a这个package和test_b这个module中的测试项目:

$ python -m unittest discover
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

unittest discovery默认会搜索名字命名符合test*的module或package,可以添加更多的参数:

  • -v:详细输出。
  • -s:开始自动搜索的目录,默认是.;这个参数也可以指向一个package名,而不是目录,例如unittest.test_a。
  • -p:文件匹配的模式,默认是test*.py
  • -t:项目顶级目录,默认与开始自动搜索的目录相同。

比如:

$ python -m unittest discover -s test_a
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

测试用例发现通过import模块或package执行测试,例如foo/bar/baz.py会被import为foo.bar.baz

测试代码的组织

测试用例一定要是自包含的,即测试用例既可以独立运行,也可以和其他测试用例混合执行,测试用例执行前后不能影响系统状态。

建议将被测试代码和测试代码分离,比如一个模块module.py对应的单元测试的文件是test_module.py,这样方便维护。

最简单的测试用例定义,是一个unittest.TestCase的子类只包含一个测试步骤,这个时候只需要定义一个runTest方法,比如:

# unit.py
import unittest

class MyTestCase(unittest.TestCase):
    def runTest(self):
        self.assertEqual(1, 2, 'not equal')

执行测试后结果如下:

$ python -m unittest -v unit
runTest (unit.MyTestCase) ... FAIL

======================================================================
FAIL: runTest (unit.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "unit.py", line 5, in runTest
    self.assertEqual(1, 2, 'not equal')
AssertionError: not equal

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

FAILED (failures=1)

如果assert*方法检查失败,会抛出一个异常,unittest会将其算作失败(failure)。任何其他异常都被unittest算作错误(error),比如:

#unit.py
import unittest

class MyTestCase(unittest.TestCase):
    def runTest(self):
        self.assertEqual(notexist, 2, 'not exist')

执行测试结果如下:

$ python -m unittest -v unit
runTest (unit.MyTestCase) ... ERROR

======================================================================
ERROR: runTest (unit.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "unit.py", line 5, in runTest
    self.assertEqual(notexist, 2, 'not exist')
NameError: global name 'notexist' is not defined

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

即failure通常是实际结果与预期结果不符,error通常是因为测试代码有bug导致。
如果很多测试项目的初始化准备工作类似,可以为他们定义同一个setUp方法,比如:

import unittest

class BaseTestCase(unittest.TestCase):
    def setUp(self):
        self._value = 12

class TestCase1(BaseTestCase):
    def runTest(self):
        self.assertEqual(self._value, 12, 'default value error')

class TestCase2(BaseTestCase):
    def runTest(self):
        self._value = 13
        self.assertEqual(self._value, 13, 'change value fail')

如果基类BaseTestCasesetUp方法中抛出异常,unittest不会继续执行子类中的runTest方法。
如果想在测试项目执行结果后进行现场清理,可以定义tearDown()方法:

import unittest

class B(unittest.TestCase):
    def setUp(self):
        self._value = 1
    def test_b(self):
        self.assertEqual(self._value, 1)
    def tearDown(self):
        del self._value

setUp()tearDown()方法的执行过程是:针对每一个测试项目,先执行setUp()方法,如果成功,那么继续执行测试函数,最后不管测试函数是否执行成功,都执行tearDown()方法;如果setUp()方法失败,则认为这个测试项目失败,不会执行测试函数也不执行tearDown()方法。

工作中很多测试项目依赖相同的测试夹具(setUptearDown),unittest支持像这样定义测试用例:

import unittest

class TestCase1(unittest.TestCase):
    def setUp(self):
        self._value = 12

    def test_default(self):
        self.assertEqual(self._value, 12, 'default value error')

    def test_change(self):
        self._value = 13
        self.assertEqual(self._value, 13, 'change value fail')

如果要执行指定的测试用例的话,可以使用TestSuite这个类,包含使用方法名作为参数声明一个测试用例实例,比如:

import unittest

class TestCase1(unittest.TestCase):
    def setUp(self):
        self._value = 12

    def test_default(self):
        self.assertEqual(self._value, 12, 'default value error')

    def test_change(self):
        self._value = 13
        self.assertEqual(self._value, 13, 'change value fail')

test_suite = unittest.TestSuite()
test_suite.addTest(TestCase1('test_default'))

test_runner = unittest.TextTestRunner()
test_runner.run(test_suite)

测试套也可以是测试套的集合,比如:

import unittest

class TestCase1(unittest.TestCase):
    def setUp(self):
        self._value = 12

    def test_default(self):
        self.assertEqual(self._value, 12, 'default value error')

    def test_change(self):
        self._value = 13
        self.assertEqual(self._value, 13, 'change value fail')

test_suite1 = unittest.TestSuite()
test_suite1.addTest(TestCase1('test_default'))

test_suite2 = unittest.TestSuite()
test_suite2.addTest(TestCase1('test_change'))

test_suite = unittest.TestSuite([test_suite1, test_suite2])

test_runner = unittest.TextTestRunner()
test_runner.run(test_suite)

如果想执行测试类中的部分测试用例,可以采用如下方式:

def suite():
    tests = ['test_default', 'test_change']
    return unittest.TestSuite(map(TestCase1, tests))

test_runner = unittest.TextTestRunner()
test_runner.run(suite())

因为将一个测试用例类下面的所有测试步骤都执行一遍的情况非常普遍,unittest提
供了TestLoader类,它的loadTestsFromTestCase()方法会在一个TestCase类中寻找所有以test开头的函数定义,并将他们添加到测试套中,这些函数会按照其名字的字符串排序顺序执行,比如:

import unittest

class TestCase1(unittest.TestCase):
    def setUp(self):
        self._value = 12

    def test_default(self):
        self.assertEqual(self._value, 12, 'default value error')

    def test_change(self):
        self._value = 13
        self.assertEqual(self._value, 13, 'change value fail')

test_suite = unittest.TestLoader().loadTestsFromTestCase(TestCase1)

unittest.TextTestRunner(verbosity=2).run(test_suite)

忽略测试用例及假设用例失败

有些情况下需要忽略执行某些测试用例或者测试类,这个时候可以使用unittest.skip装饰器及其变种。需要特别注意的是,可以通过skip某个测试类的setUp()方法而跳过整个测试类的执行,比如:

import unittest, sys

version = (0, 1)

class HowToSkip(unittest.TestCase):
    @unittest.skip('demonstrating skipping')
    def test_nothing(self):
        self.fail('will never be ran')

    @unittest.skipIf(version < (1, 3),
            'not supported version')
    def test_format(self):
        print 'your version is >= (1, 3)'

    @unittest.skipUnless(sys.platform.startswith('win'),
            'requires windows')
    def test_winndows_support(self):
        print 'support windows'

@unittest.skip('class can also be skipped')
class Skipped(unittest.TestCase):
    def test_skip(self):
        pass

class SkippedBySetUp(unittest.TestCase):
    @unittest.skip('Skipped by setUp method')
    def setUp(self):
        pass

    def test_dummy1(self):
        print 'dummy1'

    def test_dummy2(self):
        print 'dummy2'

测试结果如下:

$ python -m unittest -v unit
test_format (unit4.HowToSkip) ... skipped 'not supported version'
test_nothing (unit4.HowToSkip) ... skipped 'demonstrating skipping'
test_winndows_support (unit4.HowToSkip) ... skipped 'requires windows'
test_skip (unit4.Skipped) ... skipped 'class can also be skipped'
test_dummy1 (unit4.SkippedBySetUp) ... skipped 'Skipped by setUp method'
test_dummy2 (unit4.SkippedBySetUp) ... skipped 'Skipped by setUp method'

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK (skipped=6)

特别地,被忽略的测试用例将不会执行他们的setUp()、tearDown()方法,被忽略的测试类将不会执行他们的setUpClass()、tearDownClass()方法(关于setUpClass()和tearDownClass()的详细介绍,在下一篇博客中)。

有的时候,明知道某些测试用例会失败,这时可以使用unittest.expectedFailure装饰器,被期望失败的测试用例不会加到测试结果的failure统计中,而是加到expected failure统计中,比如:

import unittest

class ExpectedFailure(unittest.TestCase):
    @unittest.expectedFailure
    def test_fail(self):
        self.assertEqual(1, 2, 'not equal')

测试结果如下:

$ python -m unittest -v unit
test_fail (unit.ExpectedFailure) ... expected failure

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK (expected failures=1)

如果被expectedFailure的测试用例成功了,会被加到unexpected success的计数中。
综上所述,unittest执行测试用例结束后,有6种结束状态:ok、failure、error、expected failure、skipped、unexpected success。实际工作中发送自动化测试报告时,需要注意分别这些状态的含义。

用Python搭建自动化测试框架,我们需要组织用例以及测试执行,这里博主推荐Python的标准库——unittest。

unittest是xUnit系列框架中的一员,如果你了解xUnit的其他成员,那你用unittest来应该是很轻松的,它们的工作方式都差不多。

unittest核心工作原理

unittest中最核心的四个概念是:test case, test suite, test runner, test fixture

下面我们分别来解释这四个概念的意思,先来看一张unittest的静态类图(下面的类图以及解释均来源于网络,原文链接):

unittest类图
  • 一个TestCase的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
  • 而多个测试用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。
  • TestLoader是用来加载TestCase到TestSuite中的,其中有几个loadTestsFrom__()方法,就是从各个地方寻找TestCase,创建它们的实例,然后add到TestSuite中,再返回一个TestSuite实例。
  • TextTestRunner是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法。
    测试的结果会保存到TextTestResult实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息。
  • 而对一个测试用例环境的搭建和销毁,是一个fixture。

一个class继承了unittest.TestCase,便是一个测试用例,但如果其中有多个以 test 开头的方法,那么每有一个这样的方法,在load的时候便会生成一个TestCase实例,如:一个class中有四个test_xxx方法,最后在load到suite中时也有四个测试用例。

到这里整个流程就清楚了:

写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,我们通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者我们可以直接通过TextTestRunner来执行用例。这里加个说明,在Runner执行时,默认将执行结果输出到控制台,我们可以设置其输出到文件,在文件中查看结果(你可能听说过HTMLTestRunner,是的,通过它可以将结果输出到HTML中,生成漂亮的报告,它跟TextTestRunner是一样的,从名字就能看出来,这个我们后面再说)。

unittest实例

下面我们通过一些实例来更好地认识一下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

# -*- coding: utf-8 -*-

import unittest
from mathfunc import *

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    def test_add(self):
        """Test method add(a, b)"""
        self.assertEqual(3, add(1, 2))
        self.assertNotEqual(3, add(2, 2))

    def test_minus(self):
        """Test method minus(a, b)"""
        self.assertEqual(1, minus(3, 2))

    def test_multi(self):
        """Test method multi(a, b)"""
        self.assertEqual(6, multi(2, 3))

    def test_divide(self):
        """Test method divide(a, b)"""
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))

if __name__ == '__main__':
    unittest.main()

执行结果:

.F..
======================================================================
FAIL: test_divide (__main__.TestMathFunc)
Test method divide(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/py/test_mathfunc.py", line 26, in test_divide
    self.assertEqual(2.5, divide(5, 2))
AssertionError: 2.5 != 2

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=1)

能够看到一共运行了4个测试,失败了1个,并且给出了失败原因,2.5 != 2 也就是说我们的divide方法是有问题的。

这就是一个简单的测试,有几点需要说明的:

  1. 在第一行给出了每一个用例执行的结果的标识,成功是 .,失败是 F,出错是 E,跳过是 S。从上面也可以看出,测试的执行跟方法的顺序没有关系,test_divide写在了第4个,但是却是第2个执行的。

  2. 每个测试方法均以 test 开头,否则是不被unittest识别的。

  3. 在unittest.main()中加 verbosity 参数可以控制输出的错误报告的详细程度,默认是 1,如果设为 0,则不输出每一用例的执行结果,即没有上面的结果中的第1行;如果设为 2,则输出详细的执行结果,如下:

test_add (__main__.TestMathFunc)
Test method add(a, b) ... ok
test_divide (__main__.TestMathFunc)
Test method divide(a, b) ... FAIL
test_minus (__main__.TestMathFunc)
Test method minus(a, b) ... ok
test_multi (__main__.TestMathFunc)
Test method multi(a, b) ... ok

======================================================================
FAIL: test_divide (__main__.TestMathFunc)
Test method divide(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/py/test_mathfunc.py", line 26, in test_divide
    self.assertEqual(2.5, divide(5, 2))
AssertionError: 2.5 != 2

----------------------------------------------------------------------
Ran 4 tests in 0.002s

FAILED (failures=1)

可以看到,每一个用例的详细执行情况以及用例名,用例描述均被输出了出来(在测试方法下加代码示例中的”“”Doc String”“”,在用例执行时,会将该字符串作为此用例的描述,加合适的注释能够使输出的测试报告更加便于阅读

组织TestSuite

上面的代码示例了如何编写一个简单的测试,但有两个问题,我们怎么控制用例执行的顺序呢?(这里的示例中的几个测试方法并没有一定关系,但之后你写的用例可能会有先后关系,需要先执行方法A,再执行方法B),我们就要用到TestSuite了。我们添加到TestSuite中的case是会按照添加的顺序执行的

问题二是我们现在只有一个测试文件,我们直接执行该文件即可,但如果有多个测试文件,怎么进行组织,总不能一个个文件执行吧,答案也在TestSuite中。

下面来个例子:

在文件夹中我们再新建一个文件,test_suite.py

# -*- coding: utf-8 -*-

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)
Test method add(a, b) ... ok
test_minus (test_mathfunc.TestMathFunc)
Test method minus(a, b) ... ok
test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b) ... FAIL

======================================================================
FAIL: test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\py\test_mathfunc.py", line 26, in test_divide
    self.assertEqual(2.5, divide(5, 2))
AssertionError: 2.5 != 2

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

可以看到,执行情况跟我们预料的一样:执行了三个case,并且顺序是按照我们添加进suite的顺序执行的。

上面用了TestSuite的 addTests() 方法,并直接传入了TestCase列表,我们还可以:

# 直接用addTest方法添加单个TestCase
suite.addTest(TestMathFunc("test_multi"))

# 用addTests + TestLoader
# loadTestsFromName(),传入'模块名.TestCase名'
suite.addTests(unittest.TestLoader().loadTestsFromName('test_mathfunc.TestMathFunc'))
suite.addTests(unittest.TestLoader().loadTestsFromNames(['test_mathfunc.TestMathFunc']))  # loadTestsFromNames(),类似,传入列表

# loadTestsFromTestCase(),传入TestCase
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))

注意,用TestLoader的方法是无法对case进行排序的,同时,suite中也可以套suite。

将结果输出到文件中

用例组织好了,但结果只能输出到控制台,这样没有办法查看之前的执行记录,我们想将结果输出到文件。很简单,看示例:

修改test_suite.py

# -*- coding: utf-8 -*-

import unittest
from test_mathfunc import TestMathFunc

if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))

    with open('UnittestTextReport.txt', 'a') as f:
        runner = unittest.TextTestRunner(stream=f, verbosity=2)
        runner.run(suite)

执行此文件,可以看到,在同目录下生成了UnittestTextReport.txt,所有的执行报告均输出到了此文件中,这下我们便有了txt格式的测试报告了。

test fixture之setUp() tearDown()

上面整个测试基本跑了下来,但可能会遇到点特殊的情况:如果我的测试需要在每次执行之前准备环境,或者在每次执行完之后需要进行一些清理怎么办?比如执行前需要连接数据库,执行完成之后需要还原数据、断开连接。总不能每个测试方法中都添加准备环境、清理环境的代码吧。

这就要涉及到我们之前说过的test fixture了,修改test_mathfunc.py

# -*- coding: utf-8 -*-

import unittest
from mathfunc import *

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    def setUp(self):
        print "do something before test.Prepare environment."

    def tearDown(self):
        print "do something after test.Clean up."

    def test_add(self):
        """Test method add(a, b)"""
        print "add"
        self.assertEqual(3, add(1, 2))
        self.assertNotEqual(3, add(2, 2))

    def test_minus(self):
        """Test method minus(a, b)"""
        print "minus"
        self.assertEqual(1, minus(3, 2))

    def test_multi(self):
        """Test method multi(a, b)"""
        print "multi"
        self.assertEqual(6, multi(2, 3))

    def test_divide(self):
        """Test method divide(a, b)"""
        print "divide"
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))

我们添加了 setUp()tearDown() 两个方法(其实是重写了TestCase的这两个方法),这两个方法在每个测试方法执行前以及执行后执行一次,setUp用来为测试准备环境,tearDown用来清理环境,已备之后的测试。

我们再执行一次:

test_add (test_mathfunc.TestMathFunc)
Test method add(a, b) ... ok
test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b) ... FAIL
test_minus (test_mathfunc.TestMathFunc)
Test method minus(a, b) ... ok
test_multi (test_mathfunc.TestMathFunc)
Test method multi(a, b) ... ok

======================================================================
FAIL: test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\py\test_mathfunc.py", line 36, in test_divide
    self.assertEqual(2.5, divide(5, 2))
AssertionError: 2.5 != 2

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=1)
do something before test.Prepare environment.
add
do something after test.Clean up.
do something before test.Prepare environment.
divide
do something after test.Clean up.
do something before test.Prepare environment.
minus
do something after test.Clean up.
do something before test.Prepare environment.
multi
do something after test.Clean up.

可以看到setUp和tearDown在每次执行case前后都执行了一次。

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

...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    @classmethod
    def setUpClass(cls):
        print "This setUpClass() method only called once."

    @classmethod
    def tearDownClass(cls):
        print "This tearDownClass() method only called once too."

...

执行结果如下:

...
This setUpClass() method only called once.
do something before test.Prepare environment.
add
do something after test.Clean up.
...
do something before test.Prepare environment.
multi
do something after test.Clean up.
This tearDownClass() method only called once too.

可以看到setUpClass以及tearDownClass均只执行了一次。

跳过某个case

如果我们临时想要跳过某个case不执行怎么办?unittest也提供了几种方法:

  1. skip装饰器
...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    ...

    @unittest.skip("I don't want to run this case.")
    def test_divide(self):
        """Test method divide(a, b)"""
        print "divide"
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))

执行:

...
test_add (test_mathfunc.TestMathFunc)
Test method add(a, b) ... ok
test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b) ... skipped "I don't want to run this case."
test_minus (test_mathfunc.TestMathFunc)
Test method minus(a, b) ... ok
test_multi (test_mathfunc.TestMathFunc)
Test method multi(a, b) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK (skipped=1)

可以看到总的test数量还是4个,但divide()方法被skip了。

skip装饰器一共有三个 unittest.skip(reason)unittest.skipIf(condition, reason)unittest.skipUnless(condition, reason),skip无条件跳过,skipIf当condition为True时跳过,skipUnless当condition为False时跳过。

  1. TestCase.skipTest()方法
...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    ...

    def test_divide(self):
        """Test method divide(a, b)"""
        self.skipTest('Do not run this.')
        print "divide"
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))

输出:

...
test_add (test_mathfunc.TestMathFunc)
Test method add(a, b) ... ok
test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b) ... skipped 'Do not run this.'
test_minus (test_mathfunc.TestMathFunc)
Test method minus(a, b) ... ok
test_multi (test_mathfunc.TestMathFunc)
Test method multi(a, b) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK (skipped=1)

效果跟上面的装饰器一样,跳过了divide方法。

进阶——用HTMLTestRunner输出漂亮的HTML报告

我们能够输出txt格式的文本执行报告了,但是文本报告太过简陋,是不是想要更加高大上的HTML报告?但unittest自己可没有带HTML报告,我们只能求助于外部的库了。

HTMLTestRunner是一个第三方的unittest HTML报告库,首先我们下载HTMLTestRunner.py,并放到当前目录下,或者你的’C:\Python27\Lib’下,就可以导入运行了。

下载地址:

官方原版:http://tungwaiyip.info/software/HTMLTestRunner.html

灰蓝修改版:HTMLTestRunner.py(已调整格式,中文显示)

修改我们的 test_suite.py

# -*- coding: utf-8 -*-

import unittest
from test_mathfunc import TestMathFunc
from HTMLTestRunner import HTMLTestRunner

if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))

    with open('HTMLReport.html', 'w') as f:
        runner = HTMLTestRunner(stream=f,
                                title='MathFunc Test Report',
                                description='generated by HTMLTestRunner.',
                                verbosity=2
                                )
        runner.run(suite)

这样,在执行时,在控制台我们能够看到执行情况,如下:

ok test_add (test_mathfunc.TestMathFunc)
F  test_divide (test_mathfunc.TestMathFunc)
ok test_minus (test_mathfunc.TestMathFunc)
ok test_multi (test_mathfunc.TestMathFunc)

Time Elapsed: 0:00:00.001000

并且输出了HTML测试报告,HTMLReport.html,如图:

html report

这下漂亮的HTML报告也有了。其实你能发现,HTMLTestRunner的执行方法跟TextTestRunner很相似,你可以跟我上面的示例对比一下,就是把类图中的runner换成了HTMLTestRunner,并将TestResult用HTML的形式展现出来,如果你研究够深,可以写自己的runner,生成更复杂更漂亮的报告。

总结一下

  1. unittest是Python自带的单元测试框架,我们可以用其来作为我们自动化测试框架的用例组织执行框架。
  2. unittest的流程:写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,我们通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者我们可以直接通过TextTestRunner来执行用例。
  3. 一个class继承unittest.TestCase即是一个TestCase,其中以 test 开头的方法在load时被加载为一个真正的TestCase。
  4. verbosity参数可以控制执行结果的输出,0 是简单报告、1 是一般报告、2 是详细报告。
  5. 可以通过addTest和addTests向suite中添加case或suite,可以用TestLoader的loadTestsFrom__()方法。
  6. setUp()tearDown()setUpClass()以及 tearDownClass()可以在用例执行前布置环境,以及在用例执行后清理环境
  7. 我们可以通过skip,skipIf,skipUnless装饰器跳过某个case,或者用TestCase.skipTest方法。
  8. 参数中加stream,可以将报告输出到文件:可以用TextTestRunner输出txt报告,以及可以用HTMLTestRunner输出html报告。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容