UnitTest


unittest核心工作原理

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

下面我们分别来解释这四个概念的意思,先来看一张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。

流程

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

一个例子

我们先来准备一些待测方法:

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)

有几点需要说明的:

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

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

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

组织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)

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

初始化与清除

test fixture之setUp() tearDown()

用例方法

# -*- 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用来清理环境,已备之后的测试。

用例类

如果想要在所有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."
...

用例模块

模块级别指的是文件级别。Python 中文件即模块,所以你可以理解为每个文件只运行一次 ,不管你的这个测试文件中有多少的测试类和测试方法。

注意,测试模块(也就是测试文件)一定要包含test字样。虽然不是必须,但是会影响一些工具的默认测试模块的查找功能。

实现模块级别的固件,是使用 setUpModule() 和 tearDownModule() 两个函数(注意是函数,不是方法,写在类外面),函数的位置不重要,只要是在当前测试文件中实现这两个方法即可。

import unittest

def setUpModule():  # => 初始化固件
    print('\n********执行模块级别的初始化方法setUpModule**********')

def tearDownModule():  # => 清理固件
    print('\n********执行模块级别的初始化方法tearDownModule**********')


class TestAddOne(unittest.TestCase):
    pass

针对模块的测试固件每个文件只运行一次。

跳过某个case

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

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

skip装饰器一共有三个

unittest.skip(reason) skip无条件跳过

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

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

用HTMLTestRunner输出HTML报告

导入HTMLTestRunner模块

不能通过pip安装的方式导入,需要网上下载HTMLTestRunner.py,下载后将其放至python安装目录下的Lib下即可直接使用;
但网上“http://tungwaiyip.info/software/HTMLTestRunner.html”不兼容python3,故可在网上直接搜索支持python3的版本,或是直接做以下修改:

第94行,将import StringIO修改成import io
第539行,将self.outputBuffer = StringIO.StringIO()修改成self.outputBuffer= io.StringIO()
第631行,将print >> sys.stderr, ‘\nTime Elapsed: %s‘ %(self.stopTime-self.startTime)修改成print(sys.stderr, ‘\nTimeElapsed: %s‘ % (self.stopTime-self.startTime))
第642行,将if not rmap.has_key(cls):修改成if notcls in rmap:
第766行,将uo = o.decode(‘latin-1‘)修改成uo = e
第775行,将ue = e.decode(‘latin-1‘)修改成ue = e

使用方法

  • import HTMLTestRunner:导入HTMLTestRunner
  • HTMLTestRunner.HTMLTestRunner(stream=sys.stdout, verbosity=1, title=None, description=None)
    stream:输出报告的地址;
    verbosity:报告内容的详细程度,2表示很详细;
    title:报告的标题;
    description:报告的详细描述;
import unittest
import HTMLTestRunner

if __name__ == "__main__":
    # 测试用例保存的目录
    case_dirs = r"E:\PycharmProjects\pythonStudy\ModuleStudy\unittest_module\testCases"
    # 加载测试用例
    discover = unittest.defaultTestLoader.discover(case_dirs, "test_*.py")
    # 运行测试用例同时保存测试报告
    test_report_path = r"E:\PycharmProjects\pythonStudy\ModuleStudy\unittest_module\testCases\report.html"
    with open(test_report_path, "wb") as report_file:
        runner = HTMLTestRunner.HTMLTestRunner(stream=report_file, title="自动化测试报告", description="XX应用功能测试")
        runner.run(discover)

总结一下

  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报告。

文章作者: 姜楠
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 姜楠 !
  目录