查看原文
其他

python标准库系列教程(五)——unittest单元测试(中篇)

草yang年华 机器学习与python集中营 2021-09-10

python进阶教程

机器学习

深度学习

长按二维码关注

进入正文


python标准库系列教程(五)——unittest库单元测试(中篇)





声明:前面的python标准库系列文章详细讲解了Python的三大函数式编程库以及集合库,itertools、functools、operators库以及collections库,本文继续python标准库系列文章,本文为第五篇,深入详解python的单元测试原理以及unittest的基本架构,鉴于篇幅较长,第五篇将分为几个小的篇章说明,本文为中篇,后续还会有系列文章更新,希望对小伙伴有所帮助!
上一篇已经详细讲解了TestCase、TestLoader、TestSuite这三者的使用方法,这篇文章将详细介绍剩下的几大构件,TestRunner、TestFixture、TextTestResult这三者的用法。


目录

一 从单元测试的“运行代码”说起

  1.1 最常见的写法:

       unisttest.main(verbosity=2)

   1.2 TestCase提供的run方法

   1.3 TestSuite提供的run方法

   1.4 TextTestRunner提供的run方法

二 TextTesttRunnner

       ——文本测试运行器

   2.1 TextTestRunner的run方法

三 测试环境的搭建与销毁

      ——testfixture(本质是TestCase方法的重写)

   3.1 setUp()和tearDown()方法

  3.2 setUpClass()和tearDownClass()方法

   3.3 跳过某一个Case不测试

四 TextTestResult的使用

五 将单元测试的结果保存在文件中

六 unittest单元测试框架总结


01

从单元测试的“运行代码”说起

 单元测试的代码依旧是需要运行的,只不过这个运行不是像普通函数那样,需要调用某一个对应的函数,然后执行它,那么单元测试的运行入口是什么呢?要运行单元测试代码,有很多种不同的方法。


最常见的写法:

unisttest.main(verbosity=2)   

这是unittest提供的最直观简单的调用入口,它的底层设计涉及到了,TestCase、TestSuite、TestRunner、TextTestResult这几个类的综合应用,这里就不再做源码分析了。

实现代码,参见上一篇文章哦。



TestCase提供的run方法   

我们要进行单元测试,其实就是要运行TestCase里面的测试函数,TestCase提供了一个run函数是可以完成这样的操作的。

如下代码:

result = unittest.TextTestResult(f,'it is test result',verbosity=2#初始化TextTestResult类实例

testcase = TestFunctions('test_multi')  #初始化TestFunctions类实例
testcase(result) #跟testcase.run(result)的结果是一样的,我们只需要传入一个result对象即可


 TestSuite提供的run方法 

suite = unittest.TestSuite()

loader=unittest.TestLoader()

suite.addTests(loader.loadTestsFromName('test.TestFunctions'))

result = unittest.TestResult(sys.stdout,''it is test',2) #初始化TextTestResult类实例

suite.run(result)  #调用suite的run方法


 TextTestRunner提供的run方法 

参见下一章节要讲的。

总结:单元测试代码的运行推荐的运行方式为(1)和(4)



02

TextTesttRunnner ——文本测试运行器

unittest系列文章的上篇,在介绍TestSuite和TestLoader的时候,我们使用了TextTestRunner,但是当时并没有详细解释它到底是什么意思,顾名思义,它的意思是专门提供“测试代码的运行的”。虽然我们使用TestCase和TestSuite的run方法也可以运行单元测试代码,但是我们一般不这样去做,一方面是这样做需要需要手动构造一个TextTestResult的对象result,这样有可能导致一些未知的异常,如下:

AttributeError: '_io.TextIOWrapper' object has no attribute 'writeln'

本人测试过的,可能会显示但是还没有找到具体的解决办法,如果哪位大神有解决办法,望告知,将万分感谢。



TextTestRunner的run方法

该方法是unittest运行代码最主要的方式,他可以运行一个Case,也可以运行一系列的Case,也可以运行一个Suite,

如下所示:

(1)测试一个Case

if __name__ == '__main__':
    runner=unittest.TextTestRunner(verbosity=2)
    testcase = TestFunctions('test_multi')  #初始化TestFunctions类实例,此处之测试一个multi
    runner.run(testcase)  #运行测试

'''运行结果为:
test_multi (__main__.TestFunctions)
这是测试multi的测试方法 ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
'''

(2)测试一整个TestCase,即TestSuite

if __name__=='__main__'
    #第一步:构造TestSuite对象
    suite = unittest.TestSuite()

    #第二步:构造一个TestLoader对象
    loader=unittest.TestLoader()

    #第三步:通过loader将Case传递到Suite里面
    suite.addTests(loader.loadTestsFromName('test.TestFunctions'))

    #第四步:构建一个TextTestRunner对象,并且运行第二步中的suite对象
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

'''运行结果为:
test_add (test.TestFunctions)
这是测试add的测试方法 ... ok
test_divide (test.TestFunctions)
这是测试divide的测试方法 ... ok
test_minus (test.TestFunctions)
这是测试minus的测试方法 ... ok
test_multi (test.TestFunctions)
这是测试multi的测试方法 ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.004s
OK
'''
'




03

测试环境的搭建与销毁

测试环境的搭建与销毁——testfixture(本质是TestCase方法的重写)

fixture 翻译为,配置、装修的含义。

我们在测试的时候往往并不是像上面那样自,一成不变、从头到尾,可能会遇到点特殊的情况:如果我的测试需要在每次执行之前准备环境,或者在每次执行完之后需要进行一些清理怎么办?比如执行前需要连接数据库,执行完成之后需要还原数据、断开连接。总不能每个测试方法中都添加准备环境、清理环境的代码吧。幸好TestCase给我们提供了比较好的解决方案,这里主要涉及以下几个方法
setUp()
tearDown()
setUpClass()
tearDownClass()
我们通过在自定义的代码测试类(即继承自TestCase的类)中重新实现它们,来达到测试环境的搭建与销毁的目的。



       setUp()和tearDown()方法         

from functions import *
import unittest
import sys

class TestFunctions(unittest.TestCase):
    """这是其自己编写测试类"""

    def setUp(self):
        print("在测试之前,需要做一些准备工作.")

    def tearDown(self):
        print("做完测试之后做一些清理工作.")

    def test_add(self):  #测试加法的方法
        """这是测试add的测试方法"""
        self.assertEqual(3, add(12))
        self.assertNotEqual(3, add(22))

    def test_minus(self): #测试减法的方法
        """这是测试minus的测试方法"""
        self.assertEqual(1, minus(32))

    def test_multi(self): #测试乘法的方法
        """这是测试multi的测试方法"""
        self.assertEqual(6, multi(23))

    def test_divide(self): #测试除法的方法
        """这是测试divide的测试方法"""
        self.assertEqual(2, divide(63))
        self.assertEqual(2.5, divide(52))

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

'''运行结果为:
test_add (__main__.TestFunctions)
这是测试add的测试方法 ... 在测试之前,需要做一些准备工作.
做完测试之后做一些清理工作.
ok
test_divide (__main__.TestFunctions)
这是测试divide的测试方法 ... 在测试之前,需要做一些准备工作.
做完测试之后做一些清理工作.
ok
test_minus (__main__.TestFunctions)
这是测试minus的测试方法 ... 在测试之前,需要做一些准备工作.
做完测试之后做一些清理工作.
ok
test_multi (__main__.TestFunctions)
这是测试multi的测试方法 ... 在测试之前,需要做一些准备工作.
做完测试之后做一些清理工作.
ok
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
'''

总结:

(1)在每一个测试方法(以test开头的方法)执行之前,总是会先调用自定义的setUp方法进行相关的准备工作与环境配置;

在每一个测试方法执行完之后,总是会调用tearDown方法进行环境的清理;

(2)为什么称之为TestFixture呢?取这个名字的初衷不得而知,但是并不存在TestFixture这个类,但是这两个方法并不是定义在

TestFixture里面的,它是定义在基类TestCase里面的。



   setUpClass()和tearDownClass()方法    

如果我们不希望每次执行一个测试函数就执行一次“配置与清理”操作,而是想要在所有Case执行之前准备一次环境,并在所有case执行结束之后再清理环境,我们可以用 setUpClass() 与 tearDownClass()这两个方法。

from functions import *
import unittest
import sys

class TestFunctions(unittest.TestCase):
    """这是其自己编写测试类"""

    @classmethod
    def setUpClass(self):
        print("在测试之前,需要做一些准备工作,只做这一次.")

    @classmethod
    def tearDownClass(self):
        print("做完测试之后做一些清理工作,只做这一次.")

    '''其他代码和上面一样,此处省略'''

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

'''运行结果为:
在测试之前,需要做一些准备工作,只做这一次.
test_add (__main__.TestFunctions)
这是测试add的测试方法 ... ok
test_divide (__main__.TestFunctions)
这是测试divide的测试方法 ... ok
test_minus (__main__.TestFunctions)
这是测试minus的测试方法 ... ok
test_multi (__main__.TestFunctions)
这是测试multi的测试方法 ... ok
做完测试之后做一些清理工作,只做这一次.
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
'''

总结:

(1)这是两个类方法,必须使用@classmethod装饰器;

(2)在所有的测试方法运行之前,进行配置,在所有的测试函数运行结束之后,进行清理,需要注意的是这两个方法只会在开头和结尾执行一次



  跳过某一个Case不测试      

如果我们在测试的时候,我们可能会不想对某一个方法进行测试,有人会说,那你不想测试某一个方法,就不要把它load进入suite里面不就行了嘛,这当然是没问题的,但是如果我们已经添加进去了,但是我又不想测试它了,这该怎么办呢?

比如我不想测试乘法函数multi,我们可以这样做。

from functions import *
import unittest
import sys

class TestFunctions(unittest.TestCase):
    """这是其自己编写测试类"""

    def test_add(self):  #测试加法的方法
        """这是测试add的测试方法"""
        self.assertEqual(3, add(12))
        self.assertNotEqual(3, add(22))

    def test_minus(self): #测试减法的方法
        """这是测试minus的测试方法"""
        self.assertEqual(1, minus(32))

    @unittest.skip('我不想测试它了')      #表示不想测试的
    def test_multi(self): #测试乘法的方法
        """这是测试multi的测试方法"""
        self.assertEqual(6, multi(23))

    def test_divide(self): #测试除法的方法
        """这是测试divide的测试方法"""
        self.assertEqual(2, divide(63))
        self.assertEqual(2.5, divide(52))

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

'''运行结果为:
test_add (__main__.TestFunctions)
这是测试add的测试方法 ... ok
test_divide (__main__.TestFunctions)
这是测试divide的测试方法 ... ok
test_minus (__main__.TestFunctions)
这是测试minus的测试方法 ... ok
test_multi (__main__.TestFunctions)
这是测试multi的测试方法 ... skipped '我不想测试它了'
----------------------------------------------------------------------
Ran 4 tests in 0.003s
OK (skipped=1)
'''

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

总结:
(1)unittest.skip也是一个装饰器,故而要使用@语法
(2)skip装饰器一共有三个 unittest.skip(reason)、unittest.skipIf(condition, reason)、unittest.skipUnless(condition, reason),skip无条件跳过,skipIf当condition为True时跳过,skipUnless当condition为False时跳过。



04

TextTestResult的使用


TextTestResult:测试的结果会保存到TextTestResult实例中,包括运行用例数,成功数,失败数等。
首先我们看一下它的定义:

class TextTestResult(result.TestResult):
    """A test result class that can print formatted text results to a stream.
    Used by TextTestRunner.

由此可见,它是继承自TestResult的,这个类的作用是打印格式化的文本结果到流中,经常被runner使用。

通过查看定义,我们可以发现,单元测试运行的结果中,那些特殊的标记,比如成功是.,失败是E,异常是F,这些都是定义在TextTestResult里面的,包括OK,下划线------------------全都是定义在里面的。进一步我们查看,TextTestResult里面定义可以系列的以add开头的方法,如下:

class TextTestResult(result.TestResult):
    """A test result class that can print formatted text results to a stream.
    Used by TextTestRunner.
    """

    separator1 = '=' * 70
    separator2 = '-' * 70

    def __init__(self, stream, descriptions, verbosity):

    def getDescription(self, test):

    def startTest(self, test):

    def addSuccess(self, test):

    def addError(self, test, err):

    def addFailure(self, test, err):

    def addSkip(self, test, reason):

    def addExpectedFailure(self, test, err):

    def addUnexpectedSuccess(self, test):

    def printErrors(self):

    def printErrorList(self, flavour, errors):

当单元测试通过、失败、遇到错误等问题时,这一些列的方法就是将相关的信息收集起来,将最终组织的文本保存下来。

需要注意的是,
(1)TextTestRunner的run方法所返回的一个对象就是TextTestResult的一个对象。
(2)我们一般不直接操作TextTestResult,而是操作TextTestRunner来达到想要的效果。



05

将单元测试的结果保存在文件中


前面所讲的实例中,单元测试的结果只能输出到控制台,这样没有办法查看之前的执行记录,我们想将结果输出到文件。操作如下:

if __name__ == '__main__':
    suite = unittest.TestSuite()
    loader=unittest.TestLoader()
    suite.addTests(loader.loadTestsFromName('test.TestFunctions'))

    with open('result.txt''a'as f:
        runner = unittest.TextTestRunner(stream=f, verbosity=2#需要为stream指定文件
        runner.run(suite)

然后我们打开那个result文本文件,发现里面的结果和在控制台上的一模一样。即

test_add (test.TestFunctions)
这是测试add的测试方法 ... ok
test_divide (test.TestFunctions)
这是测试divide的测试方法 ... ok
test_minus (test.TestFunctions)
这是测试minus的测试方法 ... ok
test_multi (test.TestFunctions)
这是测试multi的测试方法 ... skipped '我不想测试它了'

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

OK (skipped=1)



06

unittest单元测试框架总结


测试流程:

1、第一步:创建测试类(继承TestCase):
 (1)定义测试函数(以test开头),在函数中Assert
 (2)定义配置fixture
 setUp()、tearDown()、setUpClass()、tearDownClass()
 (3)定义是否跳过测试
 unittest.skip()、unittest.skipIf()、unittest.skipUnless

2、第二步:创建TestLoader和TestSuite
                  suite = unittest.TestSuite()

                loader=unittest.TestLoader()

3、第三步:通过loader将Case传递到Suite里面去               suite.addTests(loader.loadTestsFromName('test.TestFunctions'))
4、第四步:创建runner,使用runner运行suite
上面的就是单元测试的一般架构和使用步骤了。
后面一篇文章会讲到关于 HTMLTestRunner 的使用哦,有兴趣的可以继续关注一下。



2019/01/09

Wednesday

小伙伴们,单元测试是任何编程语言都不可避免的哦,看完这篇文章你一定会有不一样的收获的,后面还有系列文章连载,请记得关注哦!如果你有需要,就添加我的公众号哦,里面分享有海量资源,包含各类数据、教程等,后面会有更多面经、资料、数据集等各类干货等着大家哦,重要的是全都是免费、无套路分享,有兴趣的小伙伴请持续关注!




推 荐 阅 读

python高级调试技巧(一)——原生态的pdb调试
python标准库系列教程(四)——collections库详细教程
python标准库系列教程(三)——operator库详细教程

python标准库系列教程(二)——functools (下篇)

python标准库系列教程(二)——functools (中篇)

python标准库系列教程(二)——functools (上篇)

python标准库系列教程(一)——itertools


赶紧关注我们吧

您的点赞和分享是我们进步的动力!

↘↘↘


: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存