Week12-unittest单元测试

week12

1 核心工作原理

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

  • TestCase(测试用例): 所有测试用例的基类,它是软件测试中最基本的组成单元。一个test case就是一个测试用例,是一个完整的测试流程,包括测试前环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。测试用例是一个完整的测试单元,可以对某一问题进行验证。
  • TestSuite(测试套件):多个测试用例TestCase集合就是TestSuite,TestSuite也可以嵌套TestSuite。
  • TestLoder:是用来加载 TestCase到TestSuite中,其中有几个loadTestsFrom_()方法,就是从各个地方寻找TestCase,创建他们的实例,然后add到TestSuite中,再返回一个TestSuite实例。
  • TextTestRunner:是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法。
  • TextTestResult:测试结果会保存到TextTestResult实例中,包括运行了多少用例,成功与失败多少等信息。
  • TestFixture(测试夹具):又叫测试脚手,测试代码的运行环境,指测试准备前和执行后要做的工作,包括setUp和tearDown方法。

其他与unittest类似的单元测试库:nose/pytest

命令行

从命令行中可以运行单元测试的模块,类,甚至单独的测试方法 。

python -m unittest test_module1 test_module2 #同时测试多个module
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method

显示更详细的测试结果的说明使用 -v flag:

python -m unittest -v test_module

查看所有的命令行选项使用命令:

python -m unittest -h

2 测试流程

  1. 写好TestCase:一个class继承unittest.TestCase,就是一个测试用例,其中有多个以test开头的方法,那么每一个这样的,在load的时候会生成一个TestCase实例。如果一个class中有四个test开头的方法,最后load到suite中时则有四个测试用例;(执行顺序按照test方法的ASCII码顺序)
  2. 由TestLoder加载TestCase到TestSuite;
  3. 然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中。

说明:

a:通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者可以直接通过TextTestRunner来执行用例

b:Runner执行时,默认将结果输出到控制台,我们可以设置其输出到文件,在文件中查看结果,也可以通过HTMLTestRunner将结果输出到HTML

3 unittest实例

3.1 准备待测方法

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

3.2 为以上方法写测试用例

test_mathfunc.py

import unittest
from mathfunc import *

class TestMathFunc(unittest.TestCase):
    """Test mathfunc.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, divide(5, 2))

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

执行结果:

这里有个插曲如何从unittest run变成普通的run?

如果当前是unittest in xxx.py的时候,菜单栏里选Run,然后选Edit Configurations,然后会发现有Python和Python tests两个部分,把你要运行的文件配置从Python tests删掉加在Python里就可以了。这是因为你最开始直接run的时候没选,所以默认按照单元测试run的,这个保存到本地配置里了,所以你下次每次运行都会按照单元测试跑。

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

----------------------------------------------------------------------
Ran 4 tests in 0.004s

FAILED (failures=1)

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

  • 在第一行给出了每一个用例执行的结果的标识,成功是 .,失败是 F,出错是 E,跳过是 S。从上面也可以看出,测试的执行跟方法的顺序没有关系,test_divide写在了第4个,但是却是第2个执行的。
  • 每个测试方法均以 test 开头,否则是不被unittest识别的。
  • 在unittest.main()中加 verbosity 参数可以控制输出的错误报告的详细程度,默认是 1,如果设为 0,则不输出每一用例的执行结果,即没有上面的结果中的第1行;如果设为 2,则输出详细的执行结果。
if __name__ == '__main__':
    unittest.main(verbosity=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 "C:/Users/zhouxy/PycharmProjects/untitled/utest/test_mathfunc.py", line 25, in test_divide
    self.assertEqual(2, divide(5, 2))
AssertionError: 2 != 2.5

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

FAILED (failures=1)

4 组织TestSuite

        a:确定测试用例的顺序,哪个先执行哪个后执行?

        b:如果测试文件有多个,怎么进行组织?

TestLoder加载TestCase有几种方法,在文件夹中我们再新建一个文件,test_suite.py:

import unittest
from test_mathfunc import TestMathFunc
#构建测试集
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 "C:\Users\zhouxy\PycharmProjects\untitled\utest\test_mathfunc.py", line 25, in test_divide
    self.assertEqual(2, divide(5, 2))
AssertionError: 2 != 2.5

----------------------------------------------------------------------
Ran 3 tests in 0.002s

FAILED (failures=1)

上面用了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 用来从clases和modules创建test suites,通常也需要创建一个该类的实例,注意,用TestLoader的方法是无法对case进行排序的(一个module执行的顺序是根据测试用例的名称),同时,suite中也可以套suite。

discover加载测试用例

找到指定目录下所有测试模块,并可递归查到子目录下的测试木块,只有匹配到的文件名才会被加载。如果启动的不是顶层目录,那么顶层目录必然单独指定。

discover方法有三个参数:

 - case_dir:这个是待执行用例的目录;

 - pattern:这个是匹配脚本名称的规则,test*.py意思是匹配test开头的所有脚本;

 - top_level_dir:这个是顶层目录的名称,一般默认等于None。

discover加载到的用例是一个list集合,需要重新写入到一个list对象testcase里,这样就可以用unittest里面的TextTestRunner这里类的run方法去执行

import unittest
import os

#测试用例路径
test_dir = os.path.join(os.getcwd(),'_test')
discover = unittest.defaultTestLoader.discover(test_dir,pattern="test*.py",top_level_dir=None)

if __name__ == "__main__":
    runner = unittest.TextTestRunner()
    runner.run(discover)

5 将结果输出到文件中

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

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

6 test fixture之setUp() tearDown()

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

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

from mathfunc import *
import unittest

class TestMathFunc(unittest.TestCase):
    """Test mathfunc.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)"""
        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, divide(5, 2))

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

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

执行结果:

do something before test.Prepare environment
add
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
divide
do something after test.Clean up

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

class TestMathFunc(unittest.TestCase):
    """Test mathfunc.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.
add
minus
divide
This tearDownClass() method only called once too.

7 跳过某个case

unittest提供了几种方法:

7.1 skip装饰器

    @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, divide(5, 2))

执行结果:

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) ... skipped "I don't want to run this case"

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

OK (skipped=1)

divide()方法被skip了。

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

7.2 TestCase.skipTest()方法

    def test_divide(self):
        """Test method divide(a, b)"""
        print('divide')
        self.assertEqual(2, divide(6, 3))
        self.skipTest('skip this exmple')
        self.assertEqual(2, divide(5, 2))

执行结果:

This setUpClass() method only called once.
add
minus
divide
This tearDownClass() method only called once too.

8 用HTMLTestRunner输出HTML报告

HTMLTestRunner是一个第三方的unittest HTML报告库,首先我们下载HTMLTestRunner.py,并放到当前目录下或者环境变量。

import unittest
from test_mathfunc import TestMathFunc
from HTMLTestRunner import HTMLTestRunner
import time

if __name__ == '__main__':
    suite = unittest.TestSuite()
    tests = [TestMathFunc('test_add'),TestMathFunc('test_minus'),TestMathFunc('test_divide')]
    suite.addTests(tests)
    now = time.strftime('%Y%m%d%H%M%S')
    with open(now+'HTMLReport.html','wb') as f:
        runner = HTMLTestRunner(stream=f,
                                         title='计算器测试',
                                         description = '测试报告',
                                         verbosity=2)
        runner.run(suite)

在同目录下生产测试报告文件:20180718193134HTMLReport.html

9 断言

 

总结

  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报告。
posted @ 2018-07-18 10:18  小律爷  阅读(330)  评论(0编辑  收藏  举报