Python自动化测试系列[v1.0.0][单元测试框架Unittest详解附源码]

[单元测试的含义]

Unittest单元测试框架的设计灵感来源于Junit(Java语言的单元测试框架),它与其他语言的单元测试框架风格相类似,支持自动化测试、为测试共享setUp和shutDown、它可以在将测试用例聚合到一起形成一个集合一起执行的同时在测试报告中展示独立的测试结果。
为了达到此目的,unittest支持几个重要的面向对象式的概念:

  • test fixture:一个test fixture所做的事情是执行单个或多个测试用例时的准备工作和执行结束后的一些相关清理工作,这包括:创建临时或代理数据库、目录或开始服务器进程
  • test case:一个test case是一个独立的测试单元,针对于一组特定的输入得到的特殊相应的验证,通过继承unittest提供的基类TestCase, 然后可以创建新的测试用例
  • test suite:一个test suite是一组测试用例的集合,也可以是一组test suite的集合,也可以两者混合的集合,test suite就是用来聚合你想要一起执行的测试用例的
  • test runner:一个test runner是一个协调测试执行并向用户提供执行结果的组建,它可以使用图形界面、文本界面或返回一个特殊值标识测试执行的结果

实例代码

下面我们看Python官方给的一个简单的小例子,来测试三个字符串

import unittest

class TestStringMethods(unittest.TestCase):  # 测试类继承了unittest.TestCase类,因此在该类里可以创建新的测试用例

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')  # ‘foo’是一个字符串,upper()方法会将这个字符串转成大写,assertEqual是unittest提供的一个断言方法,用来比较逗号前后两个值是否相等

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())  # assertTrue也是unittest提供的断言方法,用来判断括号内的内容是真是假,如果是真则断言成功,否则为失败,'FOO'是个字符串并且是大写,调用isupper()方法,返回结果
        self.assertFalse('Foo'.isupper())  # assertFalse则正好相反,如果括号内返回为假则断言成功,否则为失败

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

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

代码解析

在第三个用例里,定义了一个字符串,s='hello world', 然后进行了断言,断言的条件里调用了一个split()方法,实际上初学者看到这会比较懵,s.split()到底返回的是什么,它是否等于逗号后边['hello', 'world']?学这个东西就是要多试试,那么接下来我们进行一系列尝试来认识一下split()

启动命令行CMD,进入python环境

  • 尝试1:定义一个字符串s = 'hello world', 然后print(s)回车,结果应该是hello world
  • 尝试2:如果我们敲入print(s.split())然后回车呢呢,结果应该是['hello', 'world'], 到此我们就得到了答案,第三条用例里的断言self.assertEqual(s.split(), ['hello', 'world'])是成功
  • 尝试3:如果我们print(s.split(2))会是什么结果? 如果我们print(s.split('o'))又会是什么结果?
with self.assertRaises(TypeError):
	s.split(2)

在第三个用例里,我们也看到了两行代码,split()函数我们已经知道它能干什么了,那么with是什么?assertRaises在这又在干什么?
with,我们总提python之美,那么这就是美丽之处,with语句提供一个有效的处理异常和完成清理工作的机制,它让代码更简练,有点懵没关系,换一个方式说,如果不用with,要达到同等效果的的话要用什么呢?try…except…finally,这是另一个课题了并不是此处的重点,读者朋友可以先忽略它不要打断学习unittest的思路
assertRaises是什么呢?unittest 模块提供了用于测试函数是否在给定无效输入时引发特定异常的方法,这个方法就是assertRaises,我们在回去看代码,s.split(2), 很明显我们前边已经尝试过执行参数为2的情况,报了异常,也就是2并不是split函数的合法参数,我们传给2这个参数,它理应报异常,那么好with self.assertRaises(TypeError): 在干什么,它想要的就是看看split()在得到了非法参数的时候是否报一个TypeError,此处读者可能要整理一下思路,仔细阅读此段内容

最后的unittest.main(),有了它我们就有了一个简单的方式执行这个脚本,unittest.main()提供了命令行界面运行脚本的方式
假设我们上边的脚本保存在文件testingunit.py里并将它保存在桌面,然后我们启动命令行,输入“python C:\Users\davieyang\Desktop\testingunit.py” 看看是个什么结果?

C:\Users\用户名>python C:\Users\davieyang\Desktop\testingunit.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

我们能看到4个…,它的意义我们共4个用例,点表示测试通过,那么如果点表示测试通过,什么表示测试失败呢? 是“F”,如果测试遇到异常呢? 是“E”,如果我们执行“python C:\Users\davieyang\Desktop\testingunit.py -v”又是什么结果?

C:\Users\用户名>python C:\Users\davieyang\Desktop\testingunit.py -v
test_isupper (__main__.TestStringMethods) ... ok
test_list (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok

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

OK

[subTest]

代码实例

# coding:utf-8
import unittest


class NumbersTest(unittest.TestCase):

    def test_even(self):
        """
        使用subTest上下文管理器,区分细小的变化
        取模运算,返回除法的余数,但是参数是0到5的整数,没必要单独写方法
        """
        for i in range(0, 6):
            with self.subTest(i=i):
                self.assertEqual(i % 2, 0)


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

执行这段代码的结果会是:

SubTest failure: Traceback (most recent call last):
  File "C:\Python37\lib\unittest\case.py", line 59, in testPartExecutor
    yield
  File "C:\Python37\lib\unittest\case.py", line 533, in subTest
    yield
  File "D:\Programs\Python\Demo\unittest4\subtestDemo.py", line 14, in test_even
    self.assertEqual(i % 2, 0)
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 39, in _patched_equals
    raise native_error
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals
    old(self, first, second, msg)
  File "C:\Python37\lib\unittest\case.py", line 839, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\Python37\lib\unittest\case.py", line 832, in _baseAssertEqual
    raise self.failureException(msg)
AssertionError: 1 != 0

SubTest failure: Traceback (most recent call last):
  File "C:\Python37\lib\unittest\case.py", line 59, in testPartExecutor
    yield
  File "C:\Python37\lib\unittest\case.py", line 533, in subTest
    yield
  File "D:\Programs\Python\Demo\unittest4\subtestDemo.py", line 14, in test_even
    self.assertEqual(i % 2, 0)
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 39, in _patched_equals
    raise native_error
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals
    old(self, first, second, msg)
  File "C:\Python37\lib\unittest\case.py", line 839, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\Python37\lib\unittest\case.py", line 832, in _baseAssertEqual
    raise self.failureException(msg)
AssertionError: 1 != 0

SubTest failure: Traceback (most recent call last):
  File "C:\Python37\lib\unittest\case.py", line 59, in testPartExecutor
    yield
  File "C:\Python37\lib\unittest\case.py", line 533, in subTest
    yield
  File "D:\Programs\Python\Demo\unittest4\subtestDemo.py", line 14, in test_even
    self.assertEqual(i % 2, 0)
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 39, in _patched_equals
    raise native_error
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals
    old(self, first, second, msg)
  File "C:\Python37\lib\unittest\case.py", line 839, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\Python37\lib\unittest\case.py", line 832, in _baseAssertEqual
    raise self.failureException(msg)
AssertionError: 1 != 0


One or more subtests failed
Failed subtests list: (i=1), (i=3), (i=5)


Ran 1 test in 0.020s

FAILED (failures=3)

Process finished with exit code 1

而如果我们不使用subTest(), 只是写个简单的循环去断言,当程序执行到第一个断言失败时就会终止了,后边可能还有断言能够成功的也就不会被执行了

# coding:utf-8
import unittest


class NumbersTest(unittest.TestCase):

    def test_even(self):
        for i in range(0, 6):
            # with self.subTest(i=i):
            print("当前参数是:%d" % i)
            self.assertEqual(i % 2, 0)


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

执行结果会是:

当前参数是:0
当前参数是:1


Ran 1 test in 0.010s

FAILED (failures=1)


0 != 1

Expected :1
Actual   :0
 <Click to see difference>

Traceback (most recent call last):
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals
    old(self, first, second, msg)
  File "C:\Python37\lib\unittest\case.py", line 839, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\Python37\lib\unittest\case.py", line 832, in _baseAssertEqual
    raise self.failureException(msg)
AssertionError: 1 != 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Python37\lib\unittest\case.py", line 59, in testPartExecutor
    yield
  File "C:\Python37\lib\unittest\case.py", line 615, in run
    testMethod()
  File "D:\Programs\Python\Demo\unittest4\subtestDemo.py", line 15, in test_even
    self.assertEqual(i % 2, 0)


Process finished with exit code 1

[Test Fixture]

import unittest
class TestStringMethods(unittest.TestCase): 

import unittest 导入unittest模块,使得我们可以使用它,class TestStringMethods(unittest.TestCase): 新创建的测试类继承了unittest.TestCase,使得我们可以是使用TestCase里的特性

那么它有哪些主要特性呢?

  • 测试用例,当我们的测试类继承了unittest.TestCase,若以“def test_xxx(self):”这样的命名方式(test开头)在测试类中定义函数时,它就会被unittest认为是一条测试方法;然而就像我们做手动测试用例的时候,总有一些原则在,那么在写自动化测试用例时有哪些主要的原则呢?

    • 每一个测试用例必须是完全独立的,从而能够单独执行,也可以组团执行
    • 每一个测试用例必须有断言,从而在测试失败的情况下断言异常和一条解释性的语句(AssertionError)将会抛出,此时unittest将会将这条用例标识为失败,其他的异常类型将会被认为是错误(error)
    • 在设计测试用例时要尽可能考虑后续维护的问题,我们要尽可能的减少修改测试代码,从而能够满足快速的迭代测试
  • setUp():这个函数也继承自unittest.TestCase,它的作用是用来完成每一个测试方法执行前的准备工作,如果setUp()方法执行的时候出现异常,那么unittest框架认为测试出现了错误,测试方法是不会被执行的

  • tearDown(): 同样继承自unittest.TestCase,它的作用是每一个测试方法执行完后的清理工作,如果setUp()执行成功,那么测试方法执行成功还是失败,tearDown()方法都会被执行

  • setUpClass(): 同样继承自unittest.TestCase,它的作用是完成在所有测试方法执行前(包括setUp()),单元测试的前期准备工作,必须用@classmethod修饰,整个测试类只执行一次

  • tearDownClass(): 同样继承自unittest.TestCase,它的作用是完成在所有测试方法执行后(包括tearDown()),单元测试的清理工作,必须用@classmethod修饰,整个测试类只执行一次

  • 还有一种特例,最简单的测试用例只需要通过覆盖runTest()方法来执行自定义的测试代码,我们称之为静态方法,测试方法名不能重复,也意味着测试类中只能有一个runTest()方法,很显然这样的方式会导致很多冗余代码

  • 使用了1到5测试特性构建测试用例的,我们称之为动态方法

实例代码

下边将用实例代码详细展示如上概念,待测代码如下

class BubbleSort(object):
    def __init__(self, mylist):
        self.myList = mylist
        self.length = len(mylist)

    def ascending_order(self):
        for i in range(self.length-1):
            for j in range(self.length-1-i):
                if self.myList[j] > self.myList[j + 1]:
                    self.myList[j], self.myList[j+1] = self.myList[j+1], self.myList[j]
        return self.myList

    def descending_order(self):
        for i in range(self.length-1):
            for j in range(self.length-1-i):
                if self.myList[j] < self.myList[j + 1]:
                    self.myList[j], self.myList[j+1] = self.myList[j+1], self.myList[j]
        return self.myList

测试代码如下:

import unittest
from Util.BubbleSort import BubbleSort


class TestBubbleSort(unittest.TestCase):  
    @classmethod
    def setUpClass(cls):
        print("execute setUpClass\n")

    @classmethod
    def tearDownClass(cls):
        print("execute tearDownClass\n")

    def setUp(self):
        self.list1 = [2, 10, 25, 30, 45, 100, 325]
        self.list3 = [325, 10, 25, 45, 30, 100, 2]
        self.list4 = [11, 3, 41, 101, 327, 26, 46]
        self.list2 = [327, 101, 46, 41, 26, 11, 3]

    def tearDown(self):
        print("execute tearDown\n")

    def test_descending_order(self):
        bls = BubbleSort(self.list4)
        self.list5 = bls.descending_order()
        print(self.list5)
        self.assertEqual(self.list5, self.list2)

    def test_ascending_order(self):
        bls = BubbleSort(self.list3)
        self.list6 = bls.ascending_order()
        print(self.list6)
        self.assertEqual(self.list6, self.list1)


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

执行结果应该是

..
execute setUpClass
----------------------------------------------------------------------

Ran 2 tests in 0.001s

[2, 10, 25, 30, 45, 100, 325]
OK
execute tearDown

[327, 101, 46, 41, 26, 11, 3]
execute tearDown

execute tearDownClass


Process finished with exit code 0

[TestSuite]

代码示例

以前的执行方式
# encoding = utf-8
import random
import unittest


class TestRandomFunction(unittest.TestCase):
    def setUp(self):
        self.str = "abcdef!@#$%"

    def tearDown(self):
        pass

    def test_randomchoice(self):
        var = random.choice(self.str)
        self.assertTrue(var in self.str)
        print(var)

    def test_randomsample(self):
        with self.assertRaises(ValueError):
            random.sample(self.str, 100)
        for var in random.sample(self.str, 6):
            self.assertTrue(var in self.str)
            print(var)


class TestRandomShuffleFunction(unittest.TestCase):

    def setUp(self):
        self.list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13]

    def tearDown(self):
        pass

    def test_randomshuffle(self):
        random.shuffle(self.list)
        print(self.list)
        self.list.sort()
        self.assertEqual(self.list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13])


if __name__ == '__main__':
    unittest.main()
方式一

使用unittest.TestLoader,它可以通过传给他的参数获取测试用例的测试方法,然后再组合成TestSuite,最后在将TestSuite传递给TestRunner 完成我们所期望的执行组合

# encoding = utf-8
import random
import unittest


class TestRandomFunction(unittest.TestCase):
    def setUp(self):
        self.str = "abcdef!@#$%"

    def tearDown(self):
        pass

    def test_randomchoice(self):
        var = random.choice(self.str)
        self.assertTrue(var in self.str)
        print(var)

    def test_randomsample(self):
        with self.assertRaises(ValueError):
            random.sample(self.str, 100)
        for var in random.sample(self.str, 6):
            self.assertTrue(var in self.str)
            print(var)


class TestRandomShuffleFunction(unittest.TestCase):

    def setUp(self):
        self.list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13]

    def tearDown(self):
        pass

    def test_randomshuffle(self):
        random.shuffle(self.list)
        print(self.list)
        self.list.sort()
        self.assertEqual(self.list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13])


if __name__ == '__main__':
    # unittest.main()
    testcase1 = unittest.TestLoader().loadTestsFromTestCase(TestRandomFunction)
    testcase2 = unittest.TestLoader().loadTestsFromTestCase(TestRandomShuffleFunction)
    suite = unittest.TestSuite([testcase1, testcase2])
    unittest.TextTestRunner(verbosity=2).run(suite)
方式二

另创建一个.py文件,定义suite方法,使用unittest.TestSuite().addTest(测试类(测试方法))

# encoding = utf-8
import unittest
from unittest3.TestSuiteDemo2 import *


def suite():
    suite = unittest.TestSuite()
    suite.addTest(TestRandomFunction("test_randomchoice"))
    suite.addTest(TestRandomShuffleFunction("test_randomshuffle"))
    return suite


if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    runner.run(suite())
方式三

另创建一个.py文件,使用unittest.TestLoader().discover(“路径”,“匹配文件名”)

# encoding = utf-8
import unittest

if __name__ == '__main__':
    suite = unittest.TestLoader().discover('.', pattern='TestSuiteDemo1.py')
    unittest.TextTestRunner(verbosity=2).run(suite)

random实例

import random
import string

#随机获取0到10之间随机整数N,0 <= N <= 10, 语法randint(a, b),a <= N <= b
random.randint(0,10)
#等同于
random.randrange(0,11) 

#随机获取0到10之间的偶数M, 语法:randrange(start,stop,[step])  
random.randrange(0,11,2)

#随机获取浮点数
random.random() 

#随机后去0到10之间的浮点数O,语法:uniform(a, b)[a <= N <= b for a <= b and b <= N <= a for b < a]
random.uniform(0,10)

#获取字符串随机字符
random.choice('string')

#获取特定数量的随机字符,返回list
random.sample('string',3)

#多个字符中选取特定数量的字符组成新字符串
list1 = ['a','b','c','d','e','f','g','h','i','j', 1, 2, 3]
list2 = [str(i) for i in list1]
str1 = ''.join(random.sample(list2, 5))

#获取随机字符串
random.choice(['apple', 'pear', 'peach', 'orange', 'lemon'])
random.choice('abcdefghijklmn')

#洗牌
string1 = 'a b c d e f g'
string2 = string1.split()
random.shuffle(string2)
print(string2)

类说明

  • TestLoader类:它用于加载测试用例,然后返回一个测试用例集和
  • LoadTestsFromTestCase类:根据传给它的测试类获取其中以test开头的测试方法,然后返回一个测试集合
  • TestSuite类:用于组装测试用例的实例,然后传递给TestRunner进行测试执行
  • TextTestRunner类:用于执行测试用例,Text表示是以文本形式输出测试结果,它会返回一个TestResult实例对象,用于存储测试用例执行过程中的详细信息,可以使用Python的dir()进行查看

参数verbosity说明

  • verbosity =< 0时,输出结果中不提示执行成功的用例数
  • verbosity = 1时,输出结果中仅仅以(.)的形式表示执行成功的用例数,成功几个就是几个点
  • verbosity >= 2时,可以输出每个用例执行的详细信息,特别是在用例较多的情况下,此设置会比较有用

[命令行执行]

命令行模式执行用例

unittest框架支持命令行执行测试模块、测试类甚至单独的测试方法

  • 执行测试模块:python -m unittest test_module1 test_module2 ......也可以采用路径的方式 python -m unittest tests/test_something.py,如果想用一个高级的verbosity的方式执行加上参数-v即可,例如 python -m unittest -v test_module
  • 执行测试类:python -m unittest test_module1.Test_Class
  • 执行测试方法:python -m unittest test_module1.Test_Class.test_method
  • 如果想获取这种命令组合的help,则执行命令 python -m unittest -h将得到如下帮助信息
D:\Programs\Python\Demo\unittest1>python -m unittest -h
usage: python.exe -m unittest [-h] [-v] [-q] [--locals] [-f] [-c] [-b]
                              [-k TESTNAMEPATTERNS]
                              [tests [tests ...]]

positional arguments:
  tests                a list of any number of test modules, classes and test
                       methods.

optional arguments:
  -h, --help           show this help message and exit
  -v, --verbose        Verbose output
  -q, --quiet          Quiet output
  --locals             Show local variables in tracebacks
  -f, --failfast       Stop on first fail or error
  -c, --catch          Catch Ctrl-C and display results so far
  -b, --buffer         Buffer stdout and stderr during tests
  -k TESTNAMEPATTERNS  Only run tests which match the given substring 
例如 -k foo 会去匹配 
foo_tests.SomeTest.test_something, bar_tests.SomeTest.test_foo去执行,但是不会匹配bar_tests.FooTest.test_something

Examples:
  python.exe -m unittest test_module               - run tests from test_module
  python.exe -m unittest module.TestClass          - run tests from module.TestClass
  python.exe -m unittest module.Class.test_method  - run specified test method
  python.exe -m unittest path/to/test_file.py      - run tests from test_file.py

usage: python.exe -m unittest discover [-h] [-v] [-q] [--locals] [-f] [-c]
                                       [-b] [-k TESTNAMEPATTERNS] [-s START]
                                       [-p PATTERN] [-t TOP]

optional arguments:
  -h, --help            show this help message and exit
  -v, --verbose         Verbose output
  -q, --quiet           Quiet output
  --locals              Show local variables in tracebacks
  -f, --failfast        Stop on first fail or error
  -c, --catch           Catch Ctrl-C and display results so far
  -b, --buffer          Buffer stdout and stderr during tests
  -k TESTNAMEPATTERNS   Only run tests which match the given substring
  -s START, --start-directory START
                        Directory to start discovery ('.' default)
  -p PATTERN, --pattern PATTERN
                        Pattern to match tests ('test*.py' default)
  -t TOP, --top-level-directory TOP
                        Top level directory of project (defaults to start
                        directory)

For test discovery all test modules must be importable from the top level
directory of the project.

如果没有传参数,那么将执行Test Discovery, 例如输入命令python -m unittest(它也等价于 python -m unittest discover)并未给他任何模块、类或者方法,那么他将做的事情便是Test Discovery

例如命令:python -m unittest discover -s project_directory -p "*_test.py"本条命令中使用了参数 -s 和 -p ,-s表示从那个目录开始,默认为 (.), -p则表示匹配哪样的文件名,这条命令也等价于python -m unittest discover project_directory "*_test.py"

输入python -m unittest discover -h将的到如何帮助,很清楚的写明了各参数说明

D:\Programs\Python\Demo\unittest1>python -m unittest discover -h
usage: python.exe -m unittest discover [-h] [-v] [-q] [--locals] [-f] [-c]
                                       [-b] [-k TESTNAMEPATTERNS] [-s START]
                                       [-p PATTERN] [-t TOP]

optional arguments:
  -h, --help            show this help message and exit
  -v, --verbose         Verbose output
  -q, --quiet           Quiet output
  --locals              Show local variables in tracebacks
  -f, --failfast        Stop on first fail or error
  -c, --catch           Catch Ctrl-C and display results so far
  -b, --buffer          Buffer stdout and stderr during tests
  -k TESTNAMEPATTERNS   Only run tests which match the given substring
  -s START, --start-directory START
                        Directory to start discovery ('.' default)
  -p PATTERN, --pattern PATTERN
                        Pattern to match tests ('test*.py' default)
  -t TOP, --top-level-directory TOP
                        Top level directory of project (defaults to start
                        directory)

For test discovery all test modules must be importable from the top level
directory of the project.

[TestLoader]

实际上TestLoader还有其他的方法

  • loadTestsFromModule(module, pattern=None)
  • loadTestsFromName(name, module=None) 和 loadTestsFromNames(names, module=None)
  • getTestCaseNames(testCaseClass)返回一个存出测试方法名称的序列
  • discover(start_dir, pattern='test*.py', top_level_dir=None):这个方法我们已经用到好多次了
  • defaultTestLoader:他是一个TestLoader的实例,如果我们不需要定制化的TestLoader,直接使用这个即可还能必变重复创建TestLoader实例

总结

unittest单元测试框架介绍到这里基本上满足了日常的工作需要,然而这并不是unittest的全部,它还有很多的特性,读者可以直接访问Python官网查看unittest的API文档,对于学习来说,官方文档是最好的书籍

[装饰器]

  • 如果有一些测试方法不想执行,如果有些测试方法在某些条件下不执行 该当如何?
  • 如果有些方法未在unittest框架下编写,又想使用unittest框架执行,该当如何?
  • 如果想自定义一个执行顺序该当如何?

代码实例

如果有一些测试方法不想执行,如果有些测试方法在某些条件下不执行 该当如何?

# coding : utf-8
import unittest
import random
import sys


class TestSequenceFunctions(unittest.TestCase):
    a = 1
    b = 2

    def setUp(self):
        self.seq = list(range(10))
        self.list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13]

    @unittest.skip("就跳过了不为什么")
    def test_shuffle(self):
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, list(range(10)))
        self.assertRaises(TypeError, random.shuffle, (1, 2, 3))

    @unittest.skipIf(a != 1, "如果a不等于1就跳过此测试方法")
    def test_choic(self):
        element = random.choice(self.seq)
        self.assertTrue(element in self.seq)

    @unittest.skipUnless(b > 1, "除非b大于1,否则跳过")
    def test_sample(self):
        with self.assertRaises(ValueError):
            random.sample(self.seq, 20)
        for element in random.sample(self.seq, 5):
            self.assertTrue(element in self.seq)

    @unittest.expectedFailure
    def test_randomshuffle(self):
        random.shuffle(self.list)
        print(self.list)
        self.assertEqual(self.list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13])


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

执行结果会是:

Expected failure: Traceback (most recent call last):
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals
    old(self, first, second, msg)
  File "C:\Python37\lib\unittest\case.py", line 839, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\Python37\lib\unittest\case.py", line 1045, in assertListEqual
    self.assertSequenceEqual(list1, list2, msg, seq_type=list)
  File "C:\Python37\lib\unittest\case.py", line 1027, in assertSequenceEqual
    self.fail(msg)
  File "C:\Python37\lib\unittest\case.py", line 680, in fail
    raise self.failureException(msg)
AssertionError: Lists differ: [4, 1, 9, 2, 7, 8, 3, 11, 6, 5, 13] != [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13]

First differing element 0:
4
1

- [4, 1, 9, 2, 7, 8, 3, 11, 6, 5, 13]
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13]

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Python37\lib\unittest\case.py", line 59, in testPartExecutor
    yield
  File "C:\Python37\lib\unittest\case.py", line 615, in run
    testMethod()
  File "D:\Programs\Python\Demo\unittest6\SkipDemo.py", line 38, in test_randomshuffle
    self.assertEqual(self.list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13])
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 38, in _patched_equals
    raise error
teamcity.diff_tools.EqualsAssertionError:  :: [4, 1, 9, 2, 7, 8, 3, 11, 6, 5, 13] != [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13]



Ran 4 tests in 0.010s

OK (skipped=1, expected failures=1)

Skipped: 就跳过了不为什么

代码解析:

unittest为我们提供了多种跳过测试用例的方法,当我们的大量用例在不同场景下可能有些用例并不想执行,例如回归测试、例如新部署的一套环境需要对主功能进行验证、例如有些用例需要具备条件才执行等等场景我们便需要这些跳过用例的方法,当然我们可以将那些不想执行的用例注释掉,但也可以采用如下装饰器给测试方法加上注解

  • @unittest.skip(reason)# 无条件跳过,reason是用来描述为什么跳过它
  • @unittest.skipIf(condition, reason)# 有条件跳过,当condition满足的情况下便跳过此装饰器装饰的用例
  • @unittest.skipUnless(condition, reason)# 有条件跳过,当condition满足的情况下便要执行此装饰器装饰的用例,与上一个相反
  • @unittest.expectedFailure# 用于标记期望执行失败的测试方法,如果该测试方法执行失败,则被认为成功,如果执行成功,则被认为失败
  • 并且当测试模块被装饰器装饰为跳过时,它的setUpModule()tearDownModule()也就不会执行了
  • 同样的当测试类被装饰器装饰为跳过时,它的setUpClass()tearDownClass()也就不会执行了
  • 一样的当测试方法被装饰器装饰为跳过时,它的setUp()tearDown()也就不会执行了

如果想自定义一个执行顺序该当如何?

在前边的文章中介绍了多种执行用例的方式,首先unittest.main()这种方式启动单元测试去执行,各测试方法的执行顺序是按所有方法名的字符串的ASCII码排序后的顺序执行的

如果想自定义顺序执行,我们需要使用TestSuite(), 它的执行顺序是按照我们addTest()的次序进行执行的

# encoding = utf-8
import unittest
from unittest3.TestSuiteDemo2 import *


def suite():
    suite = unittest.TestSuite()
    suite.addTest(TestRandomFunction("test_randomchoice"))
    suite.addTest(TestRandomShuffleFunction("test_randomshuffle"))
    return suite


if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    runner.run(suite())

如果有些方法未在unittest框架下编写,又想使用unittest框架执行,该当如何?

当我们的一些老代码并没建立在unittest的体系中,但是如果想使用unittest去执行它,又不想将所有老代码转换到unittest的时候,unittest为我们提供了unittest.FunctionTestCase(testFunc, setUp=None, tearDown=None, description=None)
假设我们有个测试方法如下

    def test_randomchoice(self):
        var = random.choice(self.str)
        self.assertTrue(var in self.str)
        print(var)

它并没有建立在unittest框架中,只是一个独立的函数,那么我们可以创建等价的测试用例

testcase = unittest.FunctionTestCase(test_randomchoice, setUp=makeSomethingDB, tearDown=deleteSomethingDB)

然而并不建议使用这种方法,如果大量的这种代码出现,将使得测试代码比较混乱难以维护,和重构

[断言]

简介

单元测试里很重要的一个部分就是断言,unittest为我们提供了很多断言方法,断言方法分为三类,一种是用来断言被测试的方法的,另一种是测试是否抛正确异常的,第三种是用来断言日志是否包含应有信息的,方法很多

  • 第一种很好理解,是用来判断我们的被测点是否达到预期用的。
  • 第二种用来判断在某种情况下是否会抛出特定的异常,如果会抛出该特定异常,则会判断为断言成功,如果没抛出这种特定异常则会判断为断言失败。
  • 第三种是用来断言日志是否包含应有信息的
assertEqual(a, b, msg=None)断言 a == b
assertNotEqual(a, b, msg=None)断言  a != b
assertTrue(expr, msg=None)断言  bool(expr) is True
assertFalse(expr, msg=None)断言  bool(expr) is False
assertIs(a, b, msg=None)断言  a is b
assertIsNot(a, b, msg=None)断言  a is not b
assertIsNone(expr, msg=None)断言  expr is None
assertIsNotNone(expr, msg=None)断言  expr is not None
assertIn(a, b, msg=None)断言  a in b
assertNotIn(a, b, msg=None)断言  a not in b
assertIsInstance(obj, cls, msg=None)断言 obj  is cls instance
assertNotIsInstance(obj, cls, msg=None)断言 obj is not cls instance
assertRaises(exc, fun, *args, **kwds)断言  fun(*args, **kwds) 是否抛出正确异常, 否则抛出断言异常
assertRaisesRegex(exc, r, fun, *args, **kwds) 断言  fun(*args, **kwds) 是否抛出正确异常,同时可以用正则r去匹配异常信息exc,否则抛出断言异常
assertWarns(warn, fun, *args, **kwds)断言fun(*args, **kwds) raises warn 
assertWarnsRegex(warn, r, fun, *args, **kwds)断言  fun(*args, **kwds) raises warn and the message matches regex r
assertLogs(logger, level) 断言log: 断言log里是否出现期望的信息,如果出现则通过,如果没出现,则断言失败抛出断言异常
assertAlmostEqual(a, b, msg=None, delta=None) round(a-b, 7) == 0  断言a-b约等于0,小数点后默认保留7位
assertNotAlmostEqual(a, b, msg=None, delta=None) round(a-b, 7) != 0 断言不是约等于的情况
assertGreater(a, b, msg=None) a > b  断言大于
assertGreaterEqual(a, b, msg=None) a >= b  断言大于等于
assertLess(a, b, msg=None, msg=None) a < b  断言小于
assertLessEqual(a, b, msg=None) a <= b 断言小于等于
assertRegex(text, regex, msg=None) r.search(s) 
assertNotRegex(text, regex, msg=None) not r.search(s) 
assertCountEqual(a, b, msg=None) a and b have the same elements in the same number, regardless of their order
assertMultiLineEqual(a, b, msg=None) strings 断言多行字符串
assertSequenceEqual(a, b, msg=None, seq_type=None) sequences 断言序列
assertListEqual(a, b, msg=None) lists 断言List
assertTupleEqual(a, b, msg=None) tuples  断言元组
assertSetEqual(a, b, msg=None) sets or frozensets 断言Set
assertDictEqual(a, b, msg=None) dicts 断言词典

在早期的python版本中,断言函数的写法有些已经被废弃了,如下对应关系所示,在我们使用编译器的时候经常会提示“Deprecated”这个单词,意味着有新的方式取代了当前的实现方法

Method Name Deprecated alias Deprecated alias
assertEqual() failUnlessEqual assertEquals
assertNotEqual() failIfEqual assertNotEquals
ssertTrue() failUnless assert_
assertFalse() failIf
assertRaises() failUnlessRaises
assertAlmostEqual() failUnlessAlmostEqual assertAlmostEquals
assertNotAlmostEqual() failIfAlmostEqual assertNotAlmostEquals
assertRegex() assertRegexpMatches
assertNotRegex() assertNotRegexpMatches
assertRaisesRegex() assertRaisesRegexp

代码实例

# encoding = utf-8
import unittest
import random
import logging

mylogger = logging.Logger('TestToBeTest')

#  被测试类
class ToBeTest(object):
    @classmethod
    def sum(cls, a, b):
        return a + b

    @classmethod
    def div(cls, a, b):
        return a/b

    @classmethod
    def return_none(cls):
        return None


#  单元测试类
class TestToBeTest(unittest.TestCase):

    # assertEqual()方法实例
    def test_assertequal(self):
        try:
            a, b = 100, 200
            sum = 300
            # 断言a+b等于sum
            self.assertEqual(a + b, sum, '断言失败,%s+%s != %s ' %(a, b, sum))
        except AssertionError as e:
            print(e)
        # 断言log
        with self.assertLogs('assertlog', level='INFO') as cm:
            logging.getLogger('assertlog').info('first message')
            logging.getLogger('assertlog.bar').error('second message')
        self.assertEqual(cm.output, ['INFO:assertlog:first message', 'ERROR:assertlog.bar:second message'])

    # assertNotEqual()方法实例
    def test_assertnotequal(self):
        try:
            a, b = 100, 200
            res = -1000
            # 断言a-b不等于res
            self.assertNotEqual(a - b, res, '断言失败,%s-%s != %s ' %(a, b, res))
        except AssertionError as e:
            print(e)

    # assertTure()方法实例
    def test_asserttrue(self):
        list1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        list2 = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
        list3 = list1[::-1]
        print(list3)
        try:
            # 断言表达式为真
            self.assertTrue(list3 == list2, "表达式为假")
        except AssertionError as e:
            print(e)

    # assertFalse()方法实例
    def test_assertfalse(self):
        list1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        list2 = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
        list3 = list1[::-1]
        try:
            #  断言表达式为假
            self.assertFalse(list3 == list1, "表达式为真")
        except AssertionError as e:
            print(e)

    # assertIs()方法实例
    def test_assertis(self):
        list1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        list2 = list1
        try:
            # 断言list2和list1属于同一个对象
            self.assertIs(list1, list2, "%s 与 %s 不属于同一对象" % (list1, list2))
        except AssertionError as e:
            print(e)

    # assertIsNot()方法实例
    def test_assertisnot(self):
        list1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        list2 = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
        try:
            # 断言list2和list1不属于同一个对象
            self.assertIsNot(list2, list1, "%s 与 %s 属于同一对象" % (list1, list2))
        except AssertionError as e:
            print(e)

    # assertIsNone()方法实例
    def test_assertisnone(self):
        try:
            results = ToBeTest.return_none()
            # 断言表达式结果是none
            self.assertIsNone(results, "is not none")
        except AssertionError as e:
            print(e)

    # assertIsNotNone()方法实例
    def test_assertisnotnone(self):
        try:
            results = ToBeTest.sum(4, 5)
            # 断言表达式结果不是none
            self.assertIsNotNone(results, "is none")
        except AssertionError as e:
            print(e)

    # assertIn()方法实例
    def test_assertin(self):
        try:
            str1 = "this is unit test demo"
            str2 = "demo"
            # 断言str2包含在str1中
            self.assertIn(str2, str1, "%s 不被包含在 %s中" %(str2, str1))
        except AssertionError as e:
            print(e)

    # assertNotIn()方法实例
    def test_assertnotin(self):
        try:
            str1 = "this is unit test demo"
            str2 = "ABC"
            # 断言str2不包含在str1中
            self.assertNotIn(str2, str1, "%s 包含在 %s 中" % (str2, str1))
        except AssertionError as e:
            print(e)

    # assertIsInstance()方法实例
    def test_assertisinstance(self):
        try:
            o = ToBeTest
            k = object
            # 断言测试对象o是k的类型
            self.assertIsInstance(o, k, "%s的类型不是%s" % (o, k))
        except AssertionError as e:
            print(e)

    # assertNotIsInstance()方法实例
    def test_assertnotisinstance(self):
        try:
            o = ToBeTest
            k = int
            # 断言测试对象o不是k的类型
            self.assertNotIsInstance(o, k, "%s 的类型是%s" % (o, k))
        except AssertionError as e:
            print(e)

    # assertRaises()方法实例
    def test_assertraises(self):
        # 测试抛出指定的异常类型
        # assertRaises(exception)
        with self.assertRaises(TypeError) as exc:
            random.sample([1, 2, 3, 4, 5, 6], "j")
        # 打印详细的异常信息
        print(exc.exception)
        # assertRaises(exception, callable, *args, **kwds)
        try:
            self.assertRaises(ZeroDivisionError, ToBeTest.div, 3, 0)
        except ZeroDivisionError as e:
            print(e)

    # assertRaisesRegexp()方法实例
    def test_assertraisesregex(self):
        # 测试抛出指定的异常类型,并用正则表达式去匹配异常信息
        # assertRaisesRegex(exception, regexp)
        with self.assertRaisesRegex(ValueError, "literal") as exc:
            int("abc")
        # 打印详细的异常信息
        print(exc.exception)

        # assertRaisesRegex(exception, regexp, callable, *args, **kwds)
        try:
            self.assertRaisesRegex(ValueError, 'invalid literal for.*\'abc\'$', int, 'abc')
        except AssertionError as e:
            print(e)

    # assertLogs()方法实例
    def test_assertlogs(self):
        with self.assertLogs(mylogger) as log:
            mylogger.error("打开浏览器")
            mylogger.info('关闭并退出浏览器')
            self.assertEqual(log.output, ['ERROR:TestToBeTest:打开浏览器', 'INFO:TestToBeTest:关闭并退出浏览器'])


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

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/135091112