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

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

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

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

(1)最常见的写法:unittest.main(verbosity=2)。

这是unittest提供的最直观简单的调用入口,它的底层设计涉及到了,TestCase、TestSuite、TestRunner、TextTestResult

这几个类的综合应用,这里就不再做源码分析了。

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

(2)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对象即可

(3)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方法

(4)TextTestRunner提供的run方法

参见下一章节要讲的。

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

二、TextTestRunner——文本测试运行器

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

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

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

1、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
''''

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

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

我们在测试的时候往往并不是像上面那样自,一成不变、从头到尾,可能会遇到点特殊的情况:如果我的测试需要在每次执行之前准备环境,或者在每次执行完之后需要进行一些清理怎么办?比如执行前需要连接数据库,执行完成之后需要还原数据、断开连接。总不能每个测试方法中都添加准备环境、清理环境的代码吧。幸好TestCase给我们提供了比较好的解决方案,这里主要涉及以下几个方法

setUp()

tearDown()

setUpClass()

tearDownClass()

我们通过在自定义的代码测试类(即继承自TestCase的类)中重新实现它们,来达到测试环境的搭建与销毁的目的。

1、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(1, 2))
        self.assertNotEqual(3, add(2, 2))

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

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

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

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里面的。

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

3、跳过某一个Case不测试

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

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

from functions import *
import unittest
import sys

class TestFunctions(unittest.TestCase):
    """这是其自己编写测试类"""
    
    def test_add(self):  #测试加法的方法
        """这是测试add的测试方法"""
        self.assertEqual(3, add(1, 2))
        self.assertNotEqual(3, add(2, 2))

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

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

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

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时跳过。
 

四、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来达到想要的效果。

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

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

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)
 

六、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 的使用哦,有兴趣的可以继续关注一下。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值