1078: a+b(多实例测试1)_测试 - unittest框架 - hanfe1

About#

unittest是Python内置的单元测试框架(模块),不仅可以完成单元测试,也适用于自动化测试中。

unittest提供了丰富的断言方法,判断测试用例是否通过,然后生成测试结果报告。

必要的准备与注意事项#

首先,我们准备这样一个目录:

M:\tests\  # 我的是M盘的tests目录,所有操作都在tests目录内完成
    ├─discover   
    │  ├─son
    │  │  ├─ 
    │  │  └─
    │  ├─
    │  ├─
    │  └─
    ├─loadTestsFromTestCaseDemo
    │  └─
    ├─
    ├─   # 代码演示文件,所有演示脚本文件
    ├─
    └─

如果你跟我的流程走, 请务必建立和理解这样的一个目录,目前这些文件都是空的,后续会一一建立,各目录内的也必须建立,虽然它是空的,但是它无比重要,因为它标明它所在目录是Python的包。

有4个函数,分别计算加减乘除,并且代码不变:

有4个函数,分别计算加减乘除,并且代码不变:

"""
用例集
"""


def add(x, y):
    """ 两数相加 """
    return x + y


def sub(x, y):
    """ 两数相减 """
    return x - y


def mul(x, y):
    """ 两数相乘 """
    return x * y


def div(x, y):
    """ 两数相除 """
    return x / y


if __name__ == '__main__':
    print(div(10, 5))
    print(div(10, 0))

上述4个函数将成为我们的测试用例。

另外,示例演示环境是:

python3.6 + windows10 + pycharm2018.1

注意!注意!!注意!!!

如果你对pycharm的使用烂熟于心,那么在运行接下来的示例时,请不要右键运行或者点击运行按钮执行脚本,而是通过Terminal或者终端执行脚本,因为pycharm的集成环境会影响测试结果。

unittest简单上手#

runTest

import unittest  # 导入unittest框架
import case_set  # 导入用例集

class myUnitTest():

    def setUp(self):
        """ 用例初始化 """
        print("用例初始化 setup")
    def runTest(self):
        """ 执行用例 """
        print((2, 3) == 5)
    def tearDown(self):
        """ 用例执行完,收尾 """
        print("用例执行完毕,收尾")
if __name__ == '__main__':
    demo = myUnitTest()
    ()   # 固定的调用方法run

执行结果:

Ran 1 test in 0.002s

OK
用例初始化 setup
True
用例执行完毕,收尾

由结果可以看到,1个用例在多少时间内执行完毕,并且用例执行通过。

用例的执行流程是:

  • setUp先开第一枪,处理一些初始化操作。
  • 接着runTest执行用例,用例返回True。
  • 最后,tearDown打扫战场!

在每个用例执行时,setUp和tearDown都会执行。

注意:

  • myUnitTest类名可以自定义,但是必须继承
  • 示例中的setUp和tearDown方法名是固定的,但如果,我们测试用例时,没有初始化和收尾的工作,setUp和tearDown方法可以省略不写。

至于runTest方法名叫什么,取决于在实例化myUnitTest类时,是否传参,我们来看类的__init__方法和run方法做了什么:

class TestCase(object):

    def __init__(self, methodName='runTest'):
         = methodName
        self._outcome = None
         = 'No test'  # 也请留意这个鬼东西 No test
    
    def run(self, result=None):
        # run方法反射了methodName
        testMethod = getattr(self, )

可以看到,在实例化的时候,其实有个methodName默认参数,正好也叫runTest。而在实例化后,实例化对象调用run方法的时候,反射了那个methodName值,然后用例正常执行了。

所以,runTest方法名可以自定义:

import unittest
import case_set

class myUnitTest():

    def add_test(self):
        """ 执行用例 """
        print((2, 3) == 5)

if __name__ == '__main__':
    demo = myUnitTest(methodName='add_test')
    ()

执行多个用例

那么,如果要执行多个用例怎么办?

import unittest
import case_set

class myUnitTestAdd():

    def runTest(self):
        """ 执行用例 """
        print((2, 3) == 5)

class myUnitTestSub():

    def runTest(self):
        """ 执行用例 """
        print((2, 3) == 5)  # 用例结果不符合预期

if __name__ == '__main__':
    demo1 = myUnitTestAdd()
    demo2 = myUnitTestSub()
    ()
    ()

上面的示例,也可以写成:

import unittest
import case_set

class myUnitTest():

    def add_test(self):
        """ 执行用例 """
        print((2, 3) == 5)

    def sub_test(self):
        """ 执行用例"""
        print((10, 5) == 2)

if __name__ == '__main__':
    demo1 = myUnitTest('add_test')
    demo2 = myUnitTest('sub_test')
    ()
    ()

如上方式,每个用例都要实例化一次,虽然可以执行多个用例,但是这么写实在是太low了,反倒没有之前测试除法用例来的简单。

另外,用print打印也不符合真实的测试环境。

我们先来解决print的问题。

使用unittest提供的断言#

来看看unittest为我们提供了哪些断言方法吧!

提供了一些断言方法用来检查并报告故障。

下表列出了最常用的方法:

MethodChecks thatdescriptionNew in
assertEqual(a, b, msg)a == b如果a不等于b,断言失败
assertNotEqual(a, b, msg)a != b如果a等于b,断言失败
assertTrue(x, msg)bool(x) is True如果表达式x不为True,断言失败
assertFalse(x, msg)bool(x) is False如果表达式x不为False,断言失败
assertIs(a, b, msg)a is b如果a is not 2,断言失败3.1
assertIsNot(a, b, msg)a is not b如果a is b,断言失败3.1
assertIsNone(x, msg)x is not None如果x不是None,断言失败3.1
assertIn(a, b, msg)a in b如果a not in b,断言失败3.1
assertNotIn(a, b, msg)a not in b如果a in b,断言失败3.1
assertIsInstance(a, b, msg)isinstance(a, b)如果a不是b类型,断言失败
assertNotIsInstance(a, b, msg)not isinstance(a, b)如果a是b类型,断言失败

示例:

import unittest
class TestStringMethods():

    def test_assertEqual(self):
        (1, 2, msg='1 != 2')  # AssertionError: 1 != 2 : 1 != 2

    def test_assertTrue(self):
        ('')

    def test_assertFalse(self):
        ('')
	def test_func01(self):
        '''xxxxx'''
        print(,self.) #用例描述+用户名
        #xxxxx,test_func01

if __name__ == '__main__':
    ()#()会自动额找到当前模块的的子类,然后找该子类内部以test开头的用例名,完事去一一的执行。

所有的assert方法都接收一个msg参数,如果指定,该参数将用作失败时的错误提示。

结果示例:


======================================================================
FAIL: test_assertEqual ()
----------------------------------------------------------------------
Traceback (most recent call last):
  File "", line 251, in test_assertEqual
    (1, 2, msg='1 != 2')  # AssertionError: 1 != 2 : 1 != 2
AssertionError: 1 != 2 : 1 != 2

======================================================================
FAIL: test_assertTrue ()
----------------------------------------------------------------------
Traceback (most recent call last):
  File "", line 254, in test_assertTrue
    ('')
AssertionError: '' is not true

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

FAILED (failures=2)

结果中,表示,如果用例通过返回.,失败返回F,所以结果告诉我们执行了3个用例,成功1个,失败两个FAILED (failures=2)AssertionError是错误信息。

测试套件(test suite)是由许多测试用例组成的复合测试,也可以理解为承载多个用例集合的容器。
使用时需要创建一个TestSuite实例对象,然后使用该对象添加用例:

  • (self, test),添加一个测试用例。
  • s(self, tests),添加多个测试用例。
  • 在实例化方法中添加测试用例。

当添加完所有用例后,该测试套件将被交给测试执行(运行)器,如TextTestRunner,该执行器会按照用例的添加顺序执行各用例,并聚合结果。

TestSuite有效的解决了:

  • 因为是顺序执行,当多个用例组成一个链式测试操作时,谁先谁后的问题就不存在了。
  • 有效地将多个用例组织到一起进行集中测试,解决了之前一个一个测试的问题。

(self, test)

import unittest
import case_set

class myUnitTest():

    def add_test(self):
        """ 执行用例 """
        ((2, 3), 5)

    def sub_test(self):
        """ 执行用例"""
        ((10, 5), 5)

def create_suite():
    """ 创建用例集 """
    # 拿到两个用例对象
    add = myUnitTest('add_test')
    sub = myUnitTest('sub_test')
    # 实例化suite对象
    suite_obj = ()
    # 添加用例
    (add)
    (sub)
    return suite_obj
    
if __name__ == '__main__':
    suite = create_suite()
    # 可以查看suite中的用例数量
    # print(())  # 2
    # 拿到执行器对象
    runner = ()
    # 你想用执行器执行谁?就把它传进去
    (suite)

代码注释已经说得很明白了,只需要记住可以通过()方法获取suite中用例的数量。

s(self, tests)

一个一个往suite中添加用例比较麻烦,所以,再来个简单的:

import unittest
import case_set

class myUnitTest():

    def add_test(self):
        """ 执行用例 """
        ((2, 3), 5)

    def sub_test(self):
        """ 执行用例"""
        ((10, 5), 5)


def create_suite():
    """ 创建用例集 """
    '''
    # 拿到两个用例对象
    add = myUnitTest('add_test')
    sub = myUnitTest('sub_test')
    # 实例化suite对象
    suite_obj = ()
    # 添加用例
    s([add, sub])
    '''
    # 上面的代码也可以这么写
    map_obj = map(myUnitTest, ['add_test', 'sub_test'])
    suite_obj = ()
    s(map_obj)
    return suite_obj

if __name__ == '__main__':

    suite = create_suite()
    # 可以查看suite中的用例数量
    # print(())  # 2
    # 拿到执行器对象
    runner = ()
    # 你想用执行器执行谁?就把它传进去
    (suite)

实例化时添加用例

import unittest
import case_set

class myUnitTest():

    def add_test(self):
        """ 执行用例 """
        ((2, 3), 5)

    def sub_test(self):
        """ 执行用例"""
        ((10, 5), 5)

def create_suite():
    """ 创建用例集 """
    map_obj = map(myUnitTest, ['add_test', 'sub_test'])
    suite_obj = (tests=map_obj)
    return suite_obj

if __name__ == '__main__':
    suite = create_suite()
    runner = ()
    (suite)

怎么玩的呢?其实我们在实例化时做了添加用例的操作,以下示例演示了实例化的过程:

import unittest
import case_set

class myUnitTest():

    def add_test(self):
        """ 执行用例 """
        ((2, 3), 5)

    def sub_test(self):
        """ 执行用例"""
        ((10, 5), 5)

class myUnitTestSuite():
    def __init__(self):
        # 当实例化suite对象时,传递用例
        map_obj = map(myUnitTest, ['add_test', 'sub_test'])
        # 调用父类的 __init__ 方法
        super().__init__(tests=map_obj)

if __name__ == '__main__':
    suite_obj = myUnitTestSuite()
    runner = ()
    (suite_obj)

虽然在一定程度上,我们优化了代码,但是还不够,因为,我们还需要手动的将用例添加到suite的中。接下来,我们来学习,如何自动添加。

#

import unittest
import case_set

class myUnitTest():

    def add_test(self):
        ((2, 3), 5)

    def sub_test(self):
        ((10, 5), 2)

    def test_mul(self):
        ((10, 5), 50)

    def test_div(self):
        ((10, 5), 2)

def create_suite():
    """ 创建用例集 """
    suite_obj = (testCaseClass=myUnitTest, prefix='test')
    return suite_obj

if __name__ == '__main__':
    suite_obj = create_suite()
    print(())  # 2
    runner = ()
    (suite_obj)

想要自动添加,需要使用类来完成,在实例化(testCaseClass, prefix='test')时,需要告诉makeSuite添加用例的类名,上例是myUnitTest,然后makeSuite将myUnitTest类中所有以prefix参数指定开头的用例,自动添加到suite中。

再次强调,prefix参数默认读取以test开头的用例,也可以自己指定:

import unittest
import case_set

class myUnitTest():

    def my_add_test(self):
        ((2, 3), 5)

    def my_sub_test(self):
        ((10, 5), 2)  # AssertionError: 5 != 2

    def test_mul(self):
        ((10, 5), 50)

    def test_div(self):
        ((10, 5), 2)

def create_suite():
    """ 创建用例集 """
    suite_obj = (myUnitTest, prefix='my')
    return suite_obj

if __name__ == '__main__':
    suite_obj = create_suite()
    print(())  # 2
    runner = ()
    (suite_obj)

如上例示例,读取myUnitTest类中所有以my开头的用例方法。但建议还是按照人家默认的test就好了。

除此之外,这都9102年了, 车车都是手自一体的,咱们除了能玩自动添加,也能手动的将指定的用例添加到suite中:

import unittest
import case_set

class myUnitTest():

    def my_add_test(self):
        ((2, 3), 5)

    def my_sub_test(self):
        ((10, 5), 2)  # AssertionError: 5 != 2

    def test_mul(self):
        ((10, 5), 50)

    def test_div(self):
        ((10, 5), 2)

def create_suite():
    """ 创建用例集 """
    suite_obj = (myUnitTest, prefix='my')
    s(map(myUnitTest, ['test_mul', 'test_div']))
    return suite_obj

if __name__ == '__main__':
    suite_obj = create_suite()
    print(())  # 4
    runner = ()
    (suite_obj)

上例,使用makeSuite自动添加所有以my开头的用例,然后又使用addTests添加两个用例。

到目前为止,我们所有的用例方法都封装在一个用例类中,但是有的时候,我们会根据不同的功能编写不同的测试用例文件,甚至是存放在不同的目录内。

这个时候在用addTest添加就非常的麻烦了。
unittest提供了TestLoader类来解决这个问题。先看提供了哪些方法:

  • ,返回testCaseClass中包含的所有测试用例的suite。
  • ,返回包含在给定模块中的所有测试用例的suite。
  • ,返回指定字符串的所有测试用例的suite。
  • s,返回指定序列中的所有测试用例suite。
  • ,从指定的目录开始递归查找所有测试模块。

执行脚本文件为,目录结构,参见开头的目录结构示例。

首先,代码如下:

import unittest

class LoadTestsFromTestCaseDemo():

    def test_is_upper(self):
        ('FOO'.isupper())

    def test_is_lower(self):
        ('foo'.islower())

LoadTestsFromTestCaseDemo类中有两个测试用例。

下面,在中:

import unittest
from loadTestsFromTestCaseDemo.loadTestsFromTestCaseDemo import LoadTestsFromTestCaseDemo

class MyTestCase():

    def test_upper(self):
        ('FOO', 'foo'.upper())

if __name__ == '__main__':
    # 使用loadTestsFromTestCase获取当前脚本和loadTestsFromTestCaseDemo脚本中的用例类
    test_case1 = ().loadTestsFromTestCase(MyTestCase)
    test_case2 = ().loadTestsFromTestCase(LoadTestsFromTestCaseDemo)
    # 创建suite并添加用例类
    suite = ()
    ([test_case1, test_case2])
    (verbosity=2).run(suite)

上例中,loadTestsFromTestCase需要传入用例类的类名。无所谓这个用例类所处的目录或者文件。

代码稍微有些变动:

import unittest

class LoadTestsFromTestCaseDemo1():

    def test_is_upper(self):
        ('FOO'.isupper())

    def test_is_lower(self):
        ('foo'.islower())

class LoadTestsFromTestCaseDemo2():

    def test_startswith(self):
        ('FOO'.startswith('F'))

    def test_endswith(self):
        ('foo'.endswith('o'))

再来看

import unittest
from loadTestsFromTestCaseDemo import loadTestsFromTestCaseDemo

class MyTestCase():

    def test_upper(self):
        ('FOO', 'foo'.upper())

if __name__ == '__main__':
    # 使用 loadTestsFromTestCase 获取当前脚本的用例类
    test_case1 = ().loadTestsFromTestCase(MyTestCase)
    # 使用 loadTestsFromModule 获取 loadTestsFromTestCaseDemo 脚本中的用例类
    test_case2 = ().loadTestsFromModule(loadTestsFromTestCaseDemo)
    # 创建suite并添加用例类
    suite = ()
    ([test_case1, test_case2])
    (verbosity=2).run(suite)

上例中,loadTestsFromModule只要传入用例类所在的脚本名即可。

&& s代码不变。
再来看

import unittest
from loadTestsFromTestCaseDemo import loadTestsFromTestCaseDemo


class MyTestCase():

    def test_upper(self):
        ('FOO', 'foo'.upper())

if __name__ == '__main__':
    # 使用 loadTestsFromName 获取当前脚本用例类的用例方法名称
    test_case1 = ().loadTestsFromName(name='', module=__import__(__name__))
    # 使用 loadTestsFromNames 获取 loadTestsFromTestCaseDemo脚本中的LoadTestsFromTestCaseDemo1用例类的用例方法名
    test_case2 = ().loadTestsFromNames(
        names=['',
               ''
               ],
        module=loadTestsFromTestCaseDemo
    )
    # 创建suite并添加用例类
    suite = ()
    ([test_case1, test_case2])
    (verbosity=2).run(suite)

切记,无论是loadTestsFromName还是loadTestsFromNames,name参数都必须传递的是用例类下的方法名字,并且,方法名必须是全名。module参数就是脚本名字。

().loadTestsFromNames(
	name="",   # 类名点方法名
	module=ModuleName			   # 脚本名
)


首先,创建一些测试用例,注意,一定要知道各文件所在的位置。M:\tests\discover\代码如下:

import unittest

class TextCaseList():

    def test_list_append(self):
        l = []
        ('a')
        (l, ['a'])   # 判断 l 是否等于 ['a']

    def test_list_remove(self):
        l = ['a']
        ('a')
        (l, [])

创建了两个关于list的测试用例。
来看M:\tests\discover\的代码示例:

import unittest

class TextCaseStr():

    def test_str_index(self):
        ('abc'.index('a'), 0)

    def test_str_find(self):
        ('abc'.find('a'), 0)

创建了两个关于str的测试用例。
来看M:\tests\discover\son\的代码示例:

import unittest

class TextCaseDict():

    def test_dict_get(self):
        d = {'a': 1}
        (('a'), 1)

    def test_dict_pop(self):
        d = {'a': 1}
        (('a'), 1)

创建了两个关于dict的测试用例。
来看M:\tests\的代码示例:

import unittest

class TextCaseTuple():

    def test_tuple_count(self):
        t = ('a', 'b')
        (('a'), 1)

    def test_tuple_index(self):
        t = ('a', 'b')
        (('a'), 0)

这样,在不同的目录中,新建了8个测试用例。
来研究一下discover怎么玩的。
discover部分无比重要,需要注意的地方有很多。要打起精神哦!
首先,来看discover的语法:

discover = ().discover(
	start_dir=base_dir,   # 该参必传
	pattern='test*.py',   # 保持默认即可。
	top_level_dir=None
	)
(verbosity=2).run(discover)

通过TestLoader()实例化对象,然后通过实例化对象调用discover方法,discover根据给定目录,递归找到子目录下的所有符合规则的测试模块,然后交给TestSuit生成用例集suite。完事交给TextTestRunner执行用例。
该discover方法接收三个参数:

  • start_dir:要测试的模块名或者测试用例的目录。
  • pattern="test*.py":表示用例文件名的匹配原则,默认匹配以test开头的文件名,星号表示后续的多个字符。
  • top_level_dir=None:测试模块的顶层目录,如果没有顶层目录,默认为None。

注意!!!意!!

  • discover对给定的目录是有要求的,它只识别Python的包,也就是目录内有文件的才算是Python的包,只要是要读取的目录,都必须是包
  • 关于start_dir和top_level_dir的几种情况:
    • start_dir目录可以单独指定,这个时候,让top_level_dir保持默认(None)即可。
    • start_dir == top_level_dir, start_dir目录与top_level_dir目录一致,discover寻找start_dir指定目录内的符合规则的模块。
    • start_dir < top_level_dir,start_dir目录是top_level_dir目录的子目录。discover寻找start_dir指定目录内的符合规则的模块。
    • start_dir > top_level_dir,start_dir目录如果大于top_level_dir目录,等待你的是报错AssertionError: Path must be within the project。说你指定的路径(start_dir)必须位于项目内(top_level_dir)。

这里再补充一点。
我们知道,TestLoader类根据各种标准加载测试用例,并将它们返回给测试套件(suite)。但一般的,我们也可以不需要创建这个类实例(想要用某个类的方法,通常都是通过个该类的实例化对象调用)。unittest已经帮我们实例化好了TestLoader对象————defaultTestLoader,我们可以直接使用default。

discover = unittest.default(
	start_dir=base_dir, 
	pattern='test*.py', 
	top_level_dir=base_dir
	)
(verbosity=2).run(discover)

最后,仔细品味示例吧:

import os
import unittest

class MyTestCase():

    def test_upper(self):
        ('FOO', 'foo'.upper())

if __name__ == '__main__':
    base_dir = ((__name__))  # M:\tests
    discover_dir = (base_dir, 'discover')  # M:\tests\discover
    son_dir = (discover_dir, 'son')  # M:\tests\discover\son
    print(base_dir, discover_dir, son_dir)
    '''
    # start_dir 和top_level_dir 的目录一致,获取该 start_dir 目录及子目录内的所有以 test 开头的 py 文件中的测试用例类
    discover = unittest.default(start_dir=base_dir, pattern='test*.py', top_level_dir=base_dir)
    (verbosity=2).run(discover)  # 8个用例被执行
    '''
    # start_dir 是 top_level_dir 的子目录,获取该 start_dir 目录及子目录内的所有以 test 开头的 py 文件中的测试用例类
    discover = unittest.default(start_dir=discover_dir, pattern='test*.py', top_level_dir=base_dir)
    (verbosity=2).run(discover)  # 6个用例被执行
    
    # discover = ().discover(start_dir=base_dir)
    # (verbosity=2).run(discover)

在参考示例时,心里默念注意事项。

注意:discover只会收集Python的包中以pattern开头的脚本,在找脚本中的子类中以test开头的函数(测试用例)

一探#

现在,makeSuite虽然很好用,但是依然不够,我们需要更加便捷和省事,一般情况下,我们更加倾向专注于编写测试用例,而后直接使用unittest执行即可,希望makeSuite这一步都能由unittest来完成,而不是我们自己来。
是的,懒惰既是美德!Python或者unittest做到了:

import unittest
import case_set

class myUnitTest():

    def test_add(self):
        """ 测试加法用例 """
        print(, )   # test_add 测试加法用例
        ((2, 3), 5)

    def test_sub(self):
        ((10, 5), 2) # AssertionError: 5 != 2

    def test_mul(self):
        ((10, 5), 50)

    def test_div(self):
        ((10, 5), 2)

if __name__ == '__main__':
    ()

正如上例,我们只需要在用例类中将用例方法以test开头,然后直接()就可以直接测试了。
我想通过前面的铺垫,这里也能大致的知道()在内部做了什么了。我们将在最后来剖析它背后的故事。现在还有一些重要的事情等着我们。
另外,你也可以通过来查看用例名称;可以使用来查看用例注释(如果你写了注释的话)。

setUpClass && tearDownClass#

在开始,我们学习了在测试某一个用例时,都会对应的执行三个方法:

  • setUp,开头一枪的那家伙,它负责该用例之前可能需要的一些准备,比如连接数据库。
  • runTest,执行用例逻辑,没的说,干活的长工。
  • tearDown,负责打扫战场,比如关闭数据库连接。

示例:

import unittest
import case_set

class myUnitTest():

    def test_add(self):
        ((2, 3), 5)

    def test_sub(self):
        ((10, 5), 5)

    def setUp(self):
        """ 如果myUnitTest中有我,我将在用例之前执行,无论我在myUnitTest的什么位置 """
        print('敌军还有三十秒到达战场, 碾碎他们....')

    def tearDown(self):
        """ 如果myUnitTest中有我,我将在用例之后执行,无论我在myUnitTest的什么位置 """
        print('ace .....')

if __name__ == '__main__':
    ()

结果:

敌军还有三十秒到达战场, 碾碎他们....
True
打完收工,阿sir出来洗地了.....
.敌军还有三十秒到达战场, 碾碎他们....
False
打完收工,阿sir出来洗地了.....
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

由结果可以看到,有两个用例被执行并通过,并且,每一个用例执行前后都触发了setUp和tearDown方法执行。
但是,同志们,如果这是由1000甚至更多的用例组成的用例集,并且每一个用例都去操作数据,那么每个用例都会做连接/关闭数据库的操作。这就蛋疼了,就不能一次连接,所有用例都完事后,再关闭?这一下一下的......
是的,可以解决这个问题:

import unittest
import case_set

class myUnitTest():

    def test_add(self):
        ((2, 3), 5)

    def test_sub(self):
        ((10, 5), 5)

    def setUp(self):
        print('敌军还有三十秒到达战场, 碾碎他们....')

    def tearDown(self):
        print('打完收工,阿sir出来洗地了.....')

    @classmethod
    def setUpClass(cls):
        print('在用例集开始执行,我去建立数据库连接......')

    @classmethod
    def tearDownClass(cls):
        print('全军撤退, 我收工.......')

if __name__ == '__main__':
    ()

结果:

在用例集开始执行,我去建立数据库连接......
敌军还有三十秒到达战场, 碾碎他们....
True
打完收工,阿sir出来洗地了.....
.敌军还有三十秒到达战场, 碾碎他们....
False
打完收工,阿sir出来洗地了.....
.全军撤退, 我收工.......

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

由结果可以看到,setUpClasstearDownClass这两个类方法完美的解决我们的问题,这让我们在某些情况下可以更加灵活的组织逻辑。

verbosity参数#

verbosity
上述的断言结果虽然很清晰,但是还不够!我们可以控制错误输出的详细程度。

import unittest

class TestStringMethods():

    def test_assertFalse(self):
        ('')

if __name__ == '__main__':
    (verbosity=1)

在执行(verbosity=1)时,可以通过verbosity参数来控制错误信息的详细程度。verbosity=0

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

OK

verbosity=1

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

OK

verbosity=2

test_assertFalse () ... ok

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

OK

由结果可以总结,verbosity有3种的错误信息状态提示报告:

  • 0,静默模式,对于测试结果给予简单提示。
  • 1,默认模式,与静默模式类似,只是在每个成功的用例前面有个.每个失败的用例前面有个F,跳过的用例有个S
  • 2,详细模式,测试结果会显示每个用例的所有相关的信息。

切记,只有0、1、2三种状态。
默认的是1。-v
除此之外,我们在终端执行时也可以输出详细报告:

M:\tests>python36  -v
test_assertFalse () ... ok

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

OK

如上示例,使verbosity参数保持默认,我们通过在终端加-v来输入详细报告信息。

除了-v,还可以有:

M:\tests>python36  -p    # 等效于verbosity=0

什么都不加,就是verbosity=1

跳过测试用例:skip#

从版本开始,unittest支持跳过单个测试方法甚至整个测试类。
也就是说,某些情况下,我们需要跳过指定的用例。
我们可以使用unittest提供的相关装饰器来完成:

decoratorsdescription
@(reason)无条件地跳过装饰测试用例。 理由应该描述为什么跳过测试用例。
@If(condition, reason)如果条件为真,则跳过修饰的测试用例。
@Unless(condition, reason)除非条件为真,否则跳过修饰的测试用例。
@将测试标记为预期的失败。如果测试失败,将被视为成功。如果测试用例通过,则认为是失败。
expection unittest.SkipTest(reason)引发此异常以跳过测试测试用例。

示例:

import unittest

class TestCase01():

    def test_assertTrue(self):
        ('')

    @('no test')  # 跳过该条用例
    def test_assertFalse(self):
        ('')

@('no test')  # 跳过这个用例类
class TestCase02():

    def test_assertTrue(self):
        ('')

    def test_assertFalse(self):
        ('')

if __name__ == '__main__':
    ()

看结果:

M:\tests>python36 
sFss
======================================================================
FAIL: test_assertTrue ()
----------------------------------------------------------------------
Traceback (most recent call last):
  File "", line 27, in test_assertTrue
    ('')
AssertionError: '' is not true

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

FAILED (failures=1, skipped=3)

毋庸置疑,在结果中,总共4个用例,一个用例类被跳过,另一个用例类中跳过一个方法,那么就是执行4个用例,跳过3个

再探#

在解释器的Lib\unittest框架内,主要目录和文件,故事将会在这里展开。

**\Lib\unittest\
    ├─test\		# 目录
    ├─
    ├─
    ├─
    ├─
    ├─
    ├─
    ├─
    ├─
    ├─
    ├─
    └─

现在,我们在脚本中执行这样一段代码:

import unittest

class TestStringMethods():

    def test_upper(self):
        ('foo'.upper(), 'FOO')

    def test_isupper(self):
        ('FOO'.isupper())

if __name__ == '__main__':
    ()

当我们在终端执行:

M:\tests>python36  -v
test_isupper () ... ok
test_upper () ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

unittest源码是这样执行的.........

文件中。main = TestProgram

class TestProgram(object):
    # defaults for testing
    module=None
    verbosity = 1
    failfast = catchbreak = buffer = progName = warnings = None
    _discovery_parser = None

    def __init__(self, module='__main__', defaultTest=None, argv=None,
                    testRunner=None, testLoader=loader.defaultTestLoader,
                    exit=True, verbosity=1, failfast=None, catchbreak=None,
                    buffer=None, warnings=None, *, tb_locals=False):
        print(argv)  # ['', '-v']
        (argv)   # 检查参数 
        ()   # 执行测试用例集
main = TestProgram

首先可以看到,main = TestProgram,所以,()相当于()。类加括号是实例化的过程,所以,我们将目光集中在__init__方法中,为实例化对象添加属性我们先略过,主要来看在这里都是执行了哪些方法。
可以看到主要做了两件事,(argv)检查终端是否有参数传入,是有参数-v的。完事执行()
先来研究检查参数的方法做了什么?

: TestProgram.parseArgs

class TestProgram(object):
    def parseArgs(self, argv):
        ()

parseArgs经过一系列的操作,我们来到该方法的最后一行,(),见名知意,这家伙是要创建用例集啊,看看具体怎么玩的。: TestProgram.createTests

class TestProgram(object):
    def createTests(self):
    	# : None
    	# : <module '__main__' from ''>
        if  is None:
             = ()
        else:
             = Loader.loadTestsFromNames(, )

首先判断是不是为None,这个参数是(defaultTest=None)中的defaultTest参数,我们并没有传参,所以是None,那么就执行if条件。在if条件中执行了()方法,并传递了参数,该参数其实就是我们运行的脚本文件名。
我们看看这个方法做了什么。: loadTestsFromModule方法位于unittest框架下的TestLoader类中。

class TestLoader(object):
    """ 根据各种标准生成测试用例集 """

    def loadTestsFromModule(self, module, *args, pattern=None, **kws):
        """返回给定模块中用例类(可能有多个用例类)中的用例 suite """

        tests = []
        # dir(module)获取 中所有顶级属性,包括类名、函数名、变量名
        # 循环判断每一个属性并判断是否是的派生类
        for name in dir(module):
            obj = getattr(module, name)
            # 如果是的派生类,就添加到tests的列表中
            # 但在添加之前,要做类型检查判断
            if isinstance(obj, type) and issubclass(obj, ):
                ((obj))
        # module:
        # module中没有 load_tests,所以 load_tests为None
        load_tests = getattr(module, 'load_tests', None)
        tests = (tests)
        # 因为load_tests为None,所以if语句不会执行,
        if load_tests is not None:
            try:
                return load_tests(self, tests, pattern)
            except Exception as e:
                error_case, error_message = _make_failed_load_tests(
                    module.__name__, e, )
                (error_message)
                return error_case
        # tests: <unittest. tests=[<unittest. tests=[< testMethod=test_isupper>, < testMethod=test_upper>]>]>
        return tests  # 用例集 suite

    def loadTestsFromTestCase(self, testCaseClass):
        """返回testCaseClass中包含的所有测试用例的 suite"""
        # testCaseClass:是中的用例类名 <class ''>
        if issubclass(testCaseClass, ):
            raise TypeError("Test cases should not be derived from "
                            "TestSuite. Maybe you meant to derive from "
                            "TestCase?")
        # 获取testCaseClass中的所有以prefix指定的用例名
        testCaseNames = (testCaseClass)
        # print(testCaseClass, testCaseNames)  # <class ''> ['test_isupper', 'test_upper']
        # 很明显,咱们的脚本中没有runTest
        if not testCaseNames and hasattr(testCaseClass, 'runTest'):
            testCaseNames = ['runTest']
        # 这就很明显了 (map(testCaseClass, testCaseNames)) 在生成用例集的suite
        loaded_suite = (map(testCaseClass, testCaseNames))
        # loaded_suite:<unittest. tests=[< testMethod=test_isupper>, < testMethod=test_upper>]>
        # loaded_(): 2
        return loaded_suite  # 返回用例集 suite

    def getTestCaseNames(self, testCaseClass):
        """
            返回在testCaseClass中找到的方法名的排序序列
        """

        # :test
        def isTestMethod(attrname, testCaseClass=testCaseClass,
                         prefix=):
            return attrname.startswith(prefix) and \
                   callable(getattr(testCaseClass, attrname))

        testFnNames = list(filter(isTestMethod, dir(testCaseClass)))
        if :
            testFnNames.sort(key=())
        return testFnNames  # ['test_isupper', 'test_upper']

中的TestLoader中一共做了三件事:

  • : TestProgram.createTests方法触发了: 方法执行,在这个方法中,首先循环判断取出测试脚本中的所有的用例类。
  • 然后在循环判断中,如果判断测试脚本中的类是的派生类,就调用: 方法调用: TestLoader.getTestCaseNames并将用例类传递进去,该方法获取到传过来的用例类名,然后去这个用例类中去找所有prefix开头的用例,然后以列表的形式返回给: 方法。
  • : 方法拿到用例列表后,生成用例集suite并返回调用者。

程序在执行完毕,回到: TestProgram.createTests中。: TestProgram.createTests成功完成任务,生成了用例集 suite。程序再次回到了调用: TestProgram.createTests的方法中——: TestProgram.parseArgs,然后: TestProgram.parseArgs方法也执行完毕。程序继续回到调用处——: 方法中。
此时,创建用例集的suite完成。
程序由此继续往下执行。: TestProgram.runTests
有了用例集就要执行了,往下看。

class TestProgram(object):
    def runTests(self):
        # 实例化时没有传参,所以 self.catchbreak: None
        if self.catchbreak:
            installHandler()
        # 同样没有传参,为None
        if  is None:
            # runner.TextTestRunner是中的TextTestRunner对象
             = runner.TextTestRunner

        if isinstance(, type):
            try:
                try:
                    testRunner = (verbosity=self.verbosity,
                                                 failfast=self.failfast,
                                                 buffer=self.buffer,
                                                 warnings=,
                                                 tb_locals=self.tb_locals)
                except TypeError:
                    # didn't accept the tb_locals argument
                    testRunner = (verbosity=self.verbosity,
                                                 failfast=self.failfast,
                                                 buffer=self.buffer,
                                                 warnings=)
            except TypeError:
                # didn't accept the verbosity, buffer or failfast arguments
                testRunner = ()
        else:
            # it is assumed to be a TestRunner instance
            testRunner = 
            # 实例化中的TextTestRunner类得到testRunner对象
        # ()依次执行每一个用例
        # 将结果收集到中
        # :<unittest. tests=[<unittest. tests=[< testMethod=test_isupper>, < testMethod=test_upper>]>]>

         = ()
        if self.exit:
            # .wasSuccessful(): <bound method TestResult.wasSuccessful of <unittest.runner.TextTestResult run=2 errors
=0 failures=0>>

            sys.exit(not .wasSuccessful())

执行用例没啥好说的,调用了: TextTestRunner.run方法依次执行每个用例并收集结果。

: TextTestRunner.run

class TextTestResult():
    """ 一个测试结果类,它可以将格式化的文本结果打印到流中 """
class TextTestRunner(object):
    """ 以文本形式显示结果的测试运行器 """
    def __init__(self, stream=None, descriptions=True, verbosity=1,
                 failfast=False, buffer=False, resultclass=None, warnings=None,
                 *, tb_locals=False):
        """ 构造一个TextTestRunner. """
        # :  <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>
        # 将结果输出到屏幕
        if stream is None:
            stream = 
        self.stream = _WritelnDecorator(stream)  # _WritelnDecorator:文件类的装饰对象
        self.descriptions = descriptions
        self.verbosity = verbosity
        self.failfast = failfast
        self.buffer = buffer
        self.tb_locals = tb_locals
         = warnings
        if resultclass is not None:
            # class: TextTestResult
            class = resultclass

    def _makeResult(self):
        # print(self.stream, self.descriptions, self.verbosity)  # <unittest.runner._WritelnDecorator object at 0x0373D690> True 2
        # 返回 TextTestResult 实例化对象
        return class(self.stream, self.descriptions, self.verbosity)

    def run(self, test):
        "运行给定的测试用例或测试套件"
        # result: TextTestResult 实例化对象
        result = ()
        registerResult(result)
        '''
        failfast是 TextTestRunner 的一个属性,缺省为False
        作用: 如果failfast为True,一旦测试集中有测试案例failed或发生error立即终止当前整个测试执行,跳过剩下所有测试案例,也就是实现“短路测试”
        '''
        result.failfast = self.failfast  # self.failfast: False
        result.buffer = self.buffer
        result.tb_locals = self.tb_locals
        with warnings.catch_warnings():
            if :
                # if  is set, use it to filter all the warnings
                warnings.simplefilter()
                # if the filter is 'default' or 'always', special-case the
                # warnings from the deprecated unittest methods to show them
                # no more than once per module, because they can be fairly
                # noisy.  The -Wd and -Wa flags can be used to bypass this
                # only when  is None.
                if  in ['default', 'always']:
                    ('module',
                            category=DeprecationWarning,
                            message=r'Please use assert\w+ instead.')
            startTime = ()
            # result: TextTestResult
            # TextTestResult中并没有 startTestRun,但是父类的 TestResult 中有
            startTestRun = getattr(result, 'startTestRun', None)
            if startTestRun is not None:
                # 执行TestResult的startTestRun
                startTestRun()
            try:
                # BaseTestSuite执行了 __call__ 方法,test加括号等于执行了 BaseTestSuite 的 run 方法
                test(result)
            finally:
                # 用例执行完毕,触发 TestResult 的 stopTestRun 方法
                stopTestRun = getattr(result, 'stopTestRun', None)
                if stopTestRun is not None:
                    stopTestRun()
            stopTime = ()
        timeTaken = stopTime - startTime
        result.printErrors()
        if hasattr(result, 'separator2'):
            ()
        run = result.testsRun
        ("Ran %d test%s in %.3fs" %
                            (run, run != 1 and "s" or "", timeTaken))
        ()

        expectedFails = unexpectedSuccesses = skipped = 0
        try:
            results = map(len, (result.expectedFailures,
                                result.unexpectedSuccesses,
                                result.skipped))
        except AttributeError:
            pass
        else:
            expectedFails, unexpectedSuccesses, skipped = results

        infos = []
        if not result.wasSuccessful():
            ("FAILED")
            failed, errored = len(), len()
            if failed:
                ("failures=%d" % failed)
            if errored:
                ("errors=%d" % errored)
        else:
            ("OK")
        if skipped:
            ("skipped=%d" % skipped)
        if expectedFails:
            ("expected failures=%d" % expectedFails)
        if unexpectedSuccesses:
            ("unexpected successes=%d" % unexpectedSuccesses)
        if infos:
            (" (%s)" % (", ".join(infos),))
        else:
            ("\n")
        return result

总结:

e851274ebc91dbe219d7ffbece0e0f52.bmp

自定义删除用例方法#

我们之前学习时,学过两个添加用例的方法,但是我讲过删除用的方法了吗?并没有!现在,我们已经剖析了源码,知道了添加用例是addTestaddTests干的。: BaseTestSuite:

class BaseTestSuite(object):

    def addTest(self, test):
        # sanity checks
        if not callable(test):
            raise TypeError("{} is not callable".format(repr(test)))
        if isinstance(test, type) and issubclass(test,
                                                 (, TestSuite)):
            raise TypeError("TestCases and TestSuites must be instantiated "
                            "before passing them to addTest()")
        self._(test)

    def addTests(self, tests):
        if isinstance(tests, str):
            raise TypeError("tests must be an iterable of tests, not a string")
        for test in tests:
            (test)

可以看到,addTest是一个一个添加,而addTests则是for循环调用addTest添加,本质上一样的。
让我们将目光聚集到addTest中,可以看到使用的是(test)。现在,我们的删除方法也有了——把添加方法复制一份,改几个字即可:

class BaseTestSuite(object):

    def addTest(self, test):
        # sanity checks
        if not callable(test):
            raise TypeError("{} is not callable".format(repr(test)))
        if isinstance(test, type) and issubclass(test,
                                                 (, TestSuite)):
            raise TypeError("TestCases and TestSuites must be instantiated "
                            "before passing them to addTest()")
        self._(test)

    def removeTest(self, test):
        # sanity checks
        if not callable(test):
            raise TypeError("{} is not callable".format(repr(test)))
        if isinstance(test, type) and issubclass(test,
                                                 (, TestSuite)):
            raise TypeError("TestCases and TestSuites must be instantiated "
                            "before passing them to addTest()")
        (test)

没错,你没看错,就是把addTest复制一份,方法名改为removeTest,完事把self._(test)改为(test)就行了。

调用也类似:

import unittest


class TestStringMethods():

    def test_upper(self):
        ('foo'.upper(), 'FOO')

    def test_isupper(self):
        ('FOO'.isupper())

if __name__ == '__main__':
    case = TestStringMethods('test_upper')
    suite = ()
    (case)   # suite中有一个test_upper用例
    print(())  # 1
    suite.removeTest(case)  # 删除掉它
    print(())  # 0

将执行结果输出到文件#

我们尝试着讲用例执行结果输出到文件中。

import unittest
class TestStringMethods():

    def test_upper(self):
        ('foo'.upper(), 'FOO')

    def test_isupper(self):
        ('FOO'.isupper())

if __name__ == '__main__':
    f = open(r'M:\tests\', 'w', encoding='utf-8')
    suite = (TestStringMethods)
    (stream=f).run(suite)

生成用例报告#

如上小节中,虽然能将结果输出到某个文件中,但更多的是根据模板生成报告,这里就来研究一下,如何生成模板报告。

参见:

好看的报告参考:

发送测试报告邮件#

参见:https://www.cnblogs.com/Neeo/articles/11478853.html

unittest.mock#

参见:

小结:在unittest中,我们需要掌握的几个类:

  • :所有测试用例的基类,给定一个测试用例方法的名字,就会返回一个测试用例实例。
  • :组织测试用例的用例集,支持测试用例的添加和删除。
  • :测试用例的执行,其中Text是以文本的形式显示测试结果,测试结果会保存到TextTestResult中。
  • :保存测试用例信息,包括运行了多少个测试用例,成功了多少,失败了多少等信息。
  • :加载TestCase到TESTSuite中。
  • :等于()
  • :TestProgram类名被赋值给了main变量,然后通过()的形式调用。

欢迎斧正,that's all,see also:

作者: 听雨危楼

出处:

版权:本作品采用「署名-非商业性使用-相同方式共享 国际」许可协议进行许可。

仰望星空,脚踏实地

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值