Python单元测试

在Python中,利用自带的单元测试框架unittest模块,可以很好的做单元测试。在实际开发环境里,先写好单元测试,确定好边界条件,可以很好的对写好的程序做调试。

工作原理

unittest是xUnit系列框架中的一员,其中最核心的四个概念是:

  • testcast
    一个TestCase实例就是一个测试用例,也就是一个完整的测试流程,包括测试前环境的准备(SetUp),执行测试代码(Run),以及测试后环境的还原(TearDown)。一个测试用例就是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证
  • testsuite
    多个测试用例放在一起就是TestSuite。TestSuite之间也可以嵌套
  • testrunner
    TestRunner就是用来执行测试用例。并且将结果输出到TestResult,包括执行了多少次测试,成功了多少,失败了多少等信息

  • testfixture
    对一个测试环境的搭建和销毁

所以工作流程如下:
写好TestCast -> TestLoader加载TestCase到TestSuite -> TestRunner运行 -> 结果保存在TestResult

unittest实例

我们先准备一些待测的方法

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

接下来我们来写一个测试

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


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

截屏2020-07-01 19.16.45

能够看到一共运行了4个测试,失败了1个,并且给出了失败原因,2.5 != 2 也就是说我们的divide方法是有问题的。

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

  • 在第一行给出了每一个用例执行的结果的标识,成功是 .,失败是 F,出错是 E,跳过是 S。从上面也可以看出,测试的执行跟方法的顺序没有关系,test_divide写在了第4个,但是却是第2个执行的。

  • 每个测试方法均以 test 开头,否则是不被unittest识别的。

  • 在unittest.main()中加 verbosity 参数可以控制输出的错误报告的详细程度,默认是 1,如果设为 0,则不输出每一用例的执行结果,即没有上面的结果中的第1行;如果设为 2,则输出详细的执行结果,如下
    截屏2020-07-01 19.22.50

常见的assert

  • assertEqual(a, b) # a = b
  • assertNotEqual(a, b) # a != b
  • assertTrue(a) # bool(a) is true
  • assertFalse(a) # bool(a) if false
  • assertIsNone(x) # x is none
  • assertIsNotNone(x) # x is not none
  • assertIn(a, b) # a in b
  • assertNotIn(a, b) # a not in b

可以看到,每一个用例的详细执行情况以及用例名,用例描述均被输出了出来(在测试方法下加代码示例中的”“”Doc String”“”,在用例执行时,会将该字符串作为此用例的描述,加合适的注释能够使输出的测试报告更加便于阅读)

TestSuite实例

上面的代码示例了如何编写一个简单的测试,但有两个问题,我们怎么控制用例执行的顺序呢?(这里的示例中的几个测试方法并没有一定关系,但之后你写的用例可能会有先后关系,需要先执行方法A,再执行方法B),我们就要用到TestSuite了。我们添加到TestSuite中的case是会按照添加的顺序执行的。

问题二是我们现在只有一个测试文件,我们直接执行该文件即可,但如果有多个测试文件,怎么进行组织,总不能一个个文件执行吧,答案也在TestSuite中。

在文件夹中我们再新建一个文件,test_suite.py

import unittest
from testMathFunc import TestMathFunc

if __name__ == '__main__':
    suite = unittest.TestSuite()

    tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")]
    suite.addTests(tests)

    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

截屏2020-07-01 19.29.51

可以看到,执行情况跟我们预料的一样:执行了三个case,并且顺序是按照我们添加进suite的顺序执行的。

上面用了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

注意,用TestLoader的方法是无法对case进行排序的,同时,suite中也可以套suite。

将结果输出到文件中

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

修改 test_suite.py:

mport unittest
from testMathFunc import TestMathFunc

if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))

    with open('UnittestTextReport.txt', 'a') as f:
        runner = unittest.TextTestRunner(stream=f, verbosity=2)
        runner.run(suite)

执行此文件,可以看到,在同目录下生成了UnittestTextReport.txt,所有的执行报告均输出到了此文件中,这下我们便有了txt格式的测试报告了。

testFixture

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

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

mport unittest
from mathfunc import *


class TestMathFunc(unittest.TestCase):
    """Test mathfuc.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))
    ... ...

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

截屏2020-07-01 19.40.33

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

...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    @classmethod
    def setUpClass(cls):
        print "This setUpClass() method only called once."

    @classmethod
    def tearDownClass(cls):
        print "This tearDownClass() method only called once too."

...

跳过某个case

如果我们临时想要跳过某个case不执行怎么办?unittest也提供了几种方法:

  • skip装饰器:
    skip装饰器一共有三个

    • unittest.skip(reason):无条件跳过

    • unittest.skipIp(condition, reason):condtion为True跳过

    • unittest.skilUnless(condition, reason):condition为False跳过

...
class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    ...

    @unittest.skip("I don't want to run this case.")
    def test_divide(self):
        """Test method divide(a, b)"""
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))
...
  • TestCase.skipTest()方法
...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    ...

    def test_divide(self):
        """Test method divide(a, b)"""
        self.skipTest('Do not run this.')
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))

小技巧

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

买卖股票 上一篇
Python语法规范 下一篇