先看一段代码:
import unittest    #导入此模块,用于单元测试
class MyUnitTest(unittest.TestCase):   #TestCase为测试用例的父类
    def setUp(self):
        print("之前运行..")

    def tearDown(self):
        print("之后运行.")
        print("~" * 10)

    def test2(self):
        print("这是test2")
        self.assertEqual(1 + 1, 3)   #用于参数是否相等判断

    def test1(self):
        print("这是test1")
        self.assertTrue(1 + 1 == 2)  #用于真假判断

    def willnotbecalled(self):
        print("这个方法不会自动调用")


if __name__ == "__main__":
    unittest.main()  #加载并运行该测试

测试用例中红色标注的函数在每个蓝色测试函数前运行,黄色标注在每个蓝色测试函数后运行,以test开头的函数都会运行测试。同时提示产生错误的函数。此为unittesst模块的基本用法。
结果如下:
.F之前运行..
这是test1
之后运行.
~~~~~~~~~~
之前运行..
这是test2
之后运行.
~~~~~~~~~~

======================================================================
FAIL: test2 (__main__.MyUnitTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\admin\untitled0.py", line 19, in test2
    self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3

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

FAILED (failures=1)

如果想跳过其中某项测试,比如说test1,则可以在其前加入注释符    @unittest.skip("说明文字")    ,运行,则测试1会被跳过测试。

如果你想预期某项测试提示失败,可在其前加上注释符    @unittest.expectedFailure    ,运行,对预期的失败会打印x字母。

手动的测试次数有限效率太低,不容易发现隐藏的问题,所以需要构建自动化测试(比如实现自动运行10000次)模块。

 

测试套件也可以方便的对测试用例分组,红黄两段代码为创建两个分组,蓝段代码创建两个测试实例(注意蓝色代码不要放到上述定义的测试类中)(addTest灰色为错误用法):

import unittest


class MyUnitTestA(unittest.TestCase):
def test_a1(self):
print("测试套件A的test_a1")
self.assertNotEqual(1 + 1, 3)

def test_a2(self):
print("测试套件A的test_a2")
self.assertTrue(1 + 1 == 2)

def not_called_by_default(self):
print("测试套件A:这个方法不会被默认调用")


class MyUnitTestB(unittest.TestCase):
def test_b1(self):
print("测试套件B的test_b1")
self.assertTrue(4 + 4 == 8)

def test_b2(self):
print("测试套件B的test_b2")
self.assertNotEqual(4 * 4, 15)

def not_called_by_default(self):
print("测试套件B:这个方法不会被默认调用")

# def suite(self):
# print("测试套件内部...")
# suite_a = unittest.makeSuite(MyUnitTestA)
# suite_b = unittest.makeSuite(MyUnitTestB)
# suite_b.addTest(MyUnitTestB, "not_called_by_default") # 加入默认不测试的函数
# return unittest.TestSuite((suite_a, suite_b)) # 返回一个元组,包含之前的两个TestSuite对象


def suite():
print("测试套件内部...")
suite_a = unittest.makeSuite(MyUnitTestA)
suite_b = unittest.makeSuite(MyUnitTestB)
suite_b.addTest(MyUnitTestB('not_called_by_default')) # 加入默认不测试的函数,结果运行了这个测试函数not_called_by_default
return unittest.TestSuite((suite_a, suite_b)) # 返回一个元组,包含之前的两个TestSuite对象


if __name__ == "__main__":
unittest.main(defaultTest='suite')

结果:

.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK
测试套件内部...
测试套件A的test_a1
测试套件A的test_a2
测试套件B的test_b1
测试套件B的test_b2
测试套件B:这个方法不会被默认调用

 

 实例:兽人之袭代码基础上添加测试模块,test_wargame.py

import unittest
from wargame.knight import Knight
from wargame.orcrider import OrcRider
from wargame.abstractgameunit import AbstractGameUnit
from wargame.gameutils import weighted_random_selection
from wargame.hut import Hut
from wargame.attackoftheorcs import AttackOfTheOrcs


class TestWarGame(unittest.TestCase):
    def setUp(self):
        self.knight = Knight()
        self.enemy = OrcRider()

    def test_injured_unit_selection(self):
        for i in range(100):  # 测试100次,受伤单位如果不是骑士或兽人,断言错误
            injured_unit = weighted_random_selection(self.knight, self.enemy)
            self.assertIsInstance(injured_unit, AbstractGameUnit, "受伤单位必须是一个AbstractGameUnit变量")


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

因为之前选择单位引入了bug,可能选到None,手动测试不一定能激活,

自动100次测试,就产生了错误:

  File "C:\python\lib\unittest\case.py", line 753, in fail
    raise self.failureException(msg)
AssertionError: None is not an instance of <class 'wargame.abstractgameunit.AbstractGameUnit'> : 受伤单位必须是一个AbstractGameUnit变量

如果存在多个测试文件,如:

可以写成批处理脚本,也可以命令行中在工作目录使用命令 python -m unittest discover 来批量执行测试。结果如下:

(venv) G:\pyprojects\shourenzhixi2.0.0>python -m unittest discover ./test/
调用test_hut.test_acquire_hut..
?[1m干得好! 屋子 4 被访问?[0m
.F
======================================================================
FAIL: test_injured_unit_selection (test_wargame.TestWarGame)
----------------------------------------------------------------------
Traceback (most recent call last):
File "G:\pyprojects\shourenzhixi2.0.0\test\test_wargame.py", line 25, in test_injured_unit_selection
self.assertIsInstance(injured_unit, AbstractGameUnit, "受伤单位必须是一个AbstractGameUnit变量")
AssertionError: None is not an instance of <class 'wargame.abstractgameunit.AbstractGameUnit'> : 受伤单位必须是一个AbstractGameUnit变量

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

FAILED (failures=1)

 

如何使用unittest.Mock库来单元测试?之前的测试都是用了代码中的实例,Mock可以创建空对象,替换测试程序中的某些部分。有了Mock对象,你可以专注要测试的功能,而不必担心这个功能的依赖的某个函数。

比如进行一个compute()的单元测试,该单元调用了其他一些非常耗时数据处理的函数,如果你知道这些函数提供什么样的信息,就可以用Mock对象模拟定义其行为。

>>> import unittest
>>> from unittest.mock import Mock
>>> mockobj=Mock()
>>> mockobj
<Mock id='2492896906112'>
>>> mockobj.foo    #创建了一个新Mock对象
<Mock name='mock.foo' id='2492904378032'>

>>> mockobj.mock_calls   #此时返回空列表
[]

>>> mockobj.foo()
<Mock name='mock.foo()' id='2492904482944'>    #创建一个子Mock对象
>>> mockobj.foo2(return_value=20)
<Mock name='mock.foo2()' id='2492904528816'>   #再次创建一个Mock子对象
>>> mockobj.mock_calls
[call.foo(), call.foo2(return_value=20)]      #列表包含两个对象

 以下代码用Mock对象代替原来的类方法的例子,红色表示原方法,黄色表示用mockObj.foo代替原方法MyClassA;用mockObj.foo2代替原方法MyClassA.foo2:

import unittest
from unittest.mock import Mock,call

class MyClassA:
    def foo(self):
        return 100  #返回100
    def foo2(self,num):
        return num+200   #返回foo+200
    def compute(self):
        x1=self.foo()
        x2=self.foo2(x1)
        print("x1=%d,x2=%d" %(x1,x2))
        result=x1+x2
        print("在classA.compute中,result=x1+x2=",result)
        return result  #400
class TestA(unittest.TestCase):
    def test_compute(self):
        a=MyClassA()
        mockObj=Mock()
        a.foo=mockObj.foo   #用mockObj.foo表示a.foo
        a.foo2=mockObj.foo2
        a.foo.return_value=100  #
        a.foo2.return_value=300
        result=a.compute()   #此时compute方法内部调用mockObj.foo,mockObj.foo2
        self.assertEqual(result,400)
        test_call_list=mockObj.mock_calls
        print("测试列表=",test_call_list)
        reference_call_list=[call.foo(),call.foo2()]#期望引用的形式
        self.assertEqual(test_call_list,reference_call_list)

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

 Mock库还可以以补丁装饰器的形式改变行为:

格式为:patch(原方法,new=用那个对象替换原方法),例如:import unittest

from unittest.mock import Mock,call

class MyClassA:
    def foo(self):
        return 100  #返回100
    def foo2(self,num):
        return num+200   #返回foo+200
    def compute(self):
        x1=self.foo()
        x2=self.foo2(x1)
        print("x1=%d,x2=%d" %(x1,x2))
        result=x1+x2
        print("在classA.compute中,result=x1+x2=",result)
        return result  #400
class TestA(unittest.TestCase):
    def test_compute(self):
        print("运行补丁。。。")
        with unittest.mock.patch('__main__.MyClassA.foo',new=Mock(return_value=500)):
            a=MyClassA()
            result=a.compute() #MyClassA.compute()中对self.foo()的调用被Mock对象的return_value所代替,x=self.foo()变为了x=500,实际没有调用foo方法
self.assertEqual(result,
400) if __name__ == '__main__': unittest.main()

结果:

....FMyUnitTestA.test_a1
MyUnitTestA.test_a2
MyUnitTestB.test_b1
MyUnitTestB.test_b2
运行补丁。。。
x1=500,x2=700
在classA.compute中,result=x1+x2= 1200

======================================================================
FAIL: test_compute (__main__.TestA)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\admin\untitled0.py", line 30, in test_compute
    self.assertEqual(result,400)
AssertionError: 1200 != 400

----------------------------------------------------------------------
Ran 5 tests in 0.004s

FAILED (failures=1)

 

如何检验你单元测试覆盖了多少代码呢?可以借助第三方包coverage。pip安装,能生成测试覆盖率报告。具体查阅coverage文档。