Skip to content

unitest

介绍

先看一个官方的例子

python
# http://docs.python.org/2/library/unittest.html?highlight=unittest#basic-example
import random
import unittest


class TestSequenceFunctions(unittest.TestCase):

    def setUp(self):
        self.seq = list(range(10))

    def test_shuffle(self):
        # make sure the shuffled sequence does not lose any elements
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, list(range(10)))

        # should raise an exception for an immutable sequence
        self.assertRaises(TypeError, random.shuffle, (1, 2, 3))

    def test_choice(self):
        element = random.choice(self.seq)
        self.assertTrue(element in self.seq)

    def test_sample(self):
        with self.assertRaises(ValueError):
            random.sample(self.seq, 20)
        for element in random.sample(self.seq, 5):
            self.assertTrue(element in self.seq)


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

先简单说下代码的意思,也就是:写 1 个测试用例(TestCase),用来测试 Python 自带的 random 模块,那 1 个测试用例里面包含了 3 个测试,分别用来测试 random 模块的 3 个函数(看 Python 源代码,其实在 random 模块中已经把一个 Random 类实例化了,所以从外部用法上看起来就是函数调用一样)。

再简单解释下代码中出现的几个概念,后续再详细讲解:

  • TestCase:测试用例,一个测试用例可以包含多个测试项。

  • test_xxxx:测试项,根据实际的功能代码逻辑来编写对应的测试项。

  • assertXXXX:检查点,执行测试中的判断测试结果是否符合测试预期结果。

  • setUp:执行测试项前的准备工作,可以做一些初始化工作。这里就是初始化一个 Python 列表。

    另外,和 setUp 对应的还有个 tearDown,后面会讲到。是平时执行测试中的 2 种行为的代码模拟,分别是:准备测试执行所需要的环境、销毁测试过程中产生的垃圾。

几个概念

unittest 模块是 Python 标准库,又叫 PyUnit。其它编程语言,也都有对应的 XUnit,X 可以替换为 Java 等其它编程语言等。

这里只是简单讲解下 unittest 模块中的几个概念,当然其他 XUnit 也有类似概念。

test fixture

平时习惯叫 fixture 了,有时候也被翻译为装置固件,就是测试需要的装备。理解上可以直接对应到上面提到过的 setUp 和 tearDown。

fixture 常用与提前准备工作例如:

  • 程序运行前需要使用数据库,得准备测试用的数据;
  • 程序运行的前提得访问某个网页,常见的那些爬虫程序就会如此。

类似这些,得需要准备好这些 fixtures,而这个装置能有清理和还原的功效(tearDown),这样不至于执行各个测试执行的时候有环境污染造成各种诡异情况。

test case

测试用例。理解上可以直接对应到上面提到过的 TestCase 这个类。对于测试用例来说,就是针对功能代码,模拟一些输入,来验证输出是否符合预期。

test suite

测试套件,就是包含了一堆 test case 的集合。使用上可以根据具体场景来归类各个 test case,比如:根据业务逻辑分(模块 A、模块 B);根据测试逻辑分(全功能测试、冒烟测试)。当然,测试套件也可以包含一堆其它测试套件。

test runner

运行测试的家伙,把各个测试丢给他,他去执行,然后把测试结果形成一份报告。

关系

画个示例图,应该可以更好理解这除了 test runner 外的几个概念的关系吧:

程序执行

上面这个图,就是一个 TestCase 执行测试代码的时候,程序执行的过程吧,想要了解更直接些,直接运行下面这个程序,看下输出信息应该就明白了。

python
import unittest


class ExampleOrderTestCase(unittest.TestCase):

    def setUp(self):
        print('I am setUp')

    def tearDown(self):
        print('I am tearDown')

    def test_do_something(self):
        print('I am test_do_something')

    def test_do_something_else(self):
        print('I am test_do_something_else')


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

运行结果如下

I am setUp
I am test_do_something
I am tearDown
I am setUp
I am test_do_something_else
I am tearDown

TestCase

一般来说,日常用 Python 写单元测试代码,最多的还是跟 TestCase 打交道。而搭建针对具体项目的测试框架时候,会用到的较多是 TestSuite、TestResult、TestLoader 这些,一旦项目中的测试框架搭建成体系了,很少会打交道。所以,先单独讲下大众化点的 TestCase。

setUp()

执行某条测试前需要准备的工作,比如:某个文件或目录必须存在、数据库需要初始化好、网络服务要准备好、访问的 URL 需要登录授权完毕等等。

每次调用测试前,都会执行这个方法。如果你运行过上面的程序就应该了解。

顺便讲一下 2 个概念:测试错误(Error)和测试失败(Failure)。

  • 测试错误,可以简单理解成测试代码执行时候报错了,比如:测试代码中 print(a),而 a 没有进行变量声明。
  • 测试失败,可以简单理解成测试代码执行正常,但没有得到预期的测试结果,比如:测试代码中调用功能代码 add(1, 2),但返回结果不是 3。
  • 另外,从 Python 2.7 开始支持了 skip 特性,也可以理解为测试忽略(Ignore),比如:某个测试只想在 Windows 下才运行,这样在 Linux 下就会被跳过,也就是忽略。

好了,现在可以讲了,如果代码在这个阶段出错,都会认为是测试错误(Error),比如:

python
import unittest


class SetUpErrorTestCase(unittest.TestCase):

    def setUp(self):
        1 / 0  # 如果代码报错是 ERROR #
        self.assertEqual(1, 2)

    def test_one(self):
        self.assertEqual(1, 2)

    def test_two(self):
        self.assertEqual(2, 1)


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

如果不删除 1/0 那行代码,报错是 Error 如果是测试错误,报错是 Failure。

修改之后,执行代码输出如下:

2 != 1

Expected :1
Actual   :2
<Click to see difference>

Traceback (most recent call last):
  File "Z:\llm_demo\unitest_demo\pythonProject\03.py", line 7, in setUp
    self.assertEqual(1, 2)
    ~~~~~~~~~~~~~~~~^^^^^^
AssertionError: 1 != 2



2 != 1

Expected :1
Actual   :2
<Click to see difference>

Traceback (most recent call last):
  File "Z:\llm_demo\unitest_demo\pythonProject\03.py", line 7, in setUp
    self.assertEqual(1, 2)
    ~~~~~~~~~~~~~~~~^^^^^^
AssertionError: 1 != 2



Ran 2 tests in 0.007s

FAILED (failures=2)

tearDown()

执行某条测试完毕后需要销毁的工作,比如:删除测试生成的文件或目录、销毁测试用的数据库等等。

每次调用测试后,都会执行这个方法,即使调用的测试错误(Error)也会调用,比如:

python
import unittest


class TearDownAlwaysTestCase(unittest.TestCase):

    def tearDown(self):
        print('I am tearDown')

    def test_one(self):
        self.assertEqual(1, 1)
        print(not_defined)

    def test_two(self):
        self.assertEqual(2, 2)


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

执行代码后输出:

Error
Traceback (most recent call last):
  File "Z:\llm_demo\unitest_demo\pythonProject\04.py", line 11, in test_one
    print(not_defined)
          ^^^^^^^^^^^
NameError: name 'not_defined' is not defined

I am tearDown
I am tearDown


Ran 2 tests in 0.008s

FAILED (errors=1)

这样设计也是为了不让某个测试的错误,影响到下个要执行的测试,所以必须要执行到清理。

如果 setUp 就测试错误(Error)了,也都会认为是测试错误(Error)。

assertXXXX()

XXXX 代码 Equal、NotEqual 等等一堆协助单元测试的判断方法,太多了直接看官方文档 最直接了。问题是这么多不经常用难免记不住,所以平时基本上就记了:

  • assertEqual
  • assertNotEqual
  • assertTrue
  • assertFalse
  • assertRaises

大多数都可以根据这些转化出来,当然,如果记住最好了,可以帮你一定程度上简化代码,以及增加代码的可读性。

比如:要明确判别一个正则输出是否符合预期,用 assertRegexpMatches,一看就知道是验证正则表达式的,就比单纯的 assertEqual 或 assertTrue 的可读性强。

当然,根据自己项目中实际情况,完全可以基于上述组合,封装出更具项目中的语义表达,提高下代码的可读性。 比如:Django 中的单元测试框架,就封装了不少适合 Web 开发中的 assertXXXX,例如判断是否 URL 跳转等。

搭建自己项目中的单元测试框架

这篇文章就先引出这个主题,暂时不详细展开,后续几篇文章逐渐来展开。

下面几个也会用到,但对于一个项目,已经搭建起来了比较完善的测试框架后,这些就不会经常用到或去改动了。组合使用下面几个,就可以根据各自项目中的实际情况,来搭建一个基本的单元测试框架,后来者基于这个框架,按照约定来填充单元测试代码就可以了。

TestSuite

上面也提到了,TestSuite 可以认为是一堆 TestCase 根据需要打个包,实际运行测试还是以 TestCase 为单位的。看官方文档,可以知道 TestSuite 有两个常用的方法,addTest 和 addTests,addTests 可以认为是循环调用了多次 addTest。这里 add 的 Test 可以是 TestCase,也可以是 TestSuite,反正是一个套一个,大鱼吃小鱼的关系。

几个实例,可以修改需要执行的不同 suite 自己执行下试试:

python
import unittest


class ExampleTestCase(unittest.TestCase):

    def test_do_somthing(self):
        self.assertEqual(1, 1)

    def test_do_somthing_else(self):
        self.assertEqual(1, 1)


class AnotherExampleTestCase(unittest.TestCase):

    def test_do_somthing(self):
        self.assertEqual(1, 1)

    def test_do_somthing_else(self):
        self.assertEqual(1, 1)


def suite_use_make_suite():
    """想把 TestCase 下的所有测试加到 TestSuite 的时候可以这样用"""

    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(ExampleTestCase))
    return suite


def suite_add_one_test():
    """想把 TestCase 下的某个测试加到 TestSuite 的时候可以这样用"""

    suite = unittest.TestSuite()
    suite.addTest(ExampleTestCase('test_do_somthing'))
    return suite


def suite_use_test_loader():
    """想用 TestLoader 方式把测试加到 TestSuite 的时候可以这样用"""

    test_cases = (ExampleTestCase, AnotherExampleTestCase)
    suite = unittest.TestSuite()
    for test_case in test_cases:
        tests = unittest.defaultTestLoader.loadTestsFromTestCase(test_case)
        suite.addTests(tests)
    return suite


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

TestLoader

可以看到上面最后一个例子,有用到 TestLoader 这个类,现在简单介绍下。根据刚才的例子,可以把 TestLoader 简单理解成辅助 TestSuite 的工具,用来收集符合要求的测试,或者可以认为是一个可以批量产生 TestCase 的工具。

看官方文档提供了很多方法,用于适应不同的场景,大多数都是类似 loadTestsFromXXXX 这种方法。

默认有个实例化完毕的可以直接拿来用,就是 unittest.defaultTestLoader,上面示例代码中也有体现。如果你觉得默认不满足实际使用,那么就自己写个 TestLoader 也可以。

另外,还有 TestResult 和 TextTestRunner 这两个很有用的东西,可以在后续介绍 Django 中的单元测试中来重点说明,顺便也可以简单阅读下 Django 的单元测试框架代码,了解下还是有好处的。如果以后在项目中,需要自定义自己特殊需求的单元测试框架的时候还是有点参考意义的。

doctest

这里简单提下,Python 中还自带 doctest 这种形式的单元测试,就是直接把测试写在文档注释。其中一个优点是,看到注释就知道这个模块、函数、类是怎么个用法了;而其中一个缺点是,测试代码的组织上很难模块化。

这里就看个简单示例吧:

python
def show_me_the_money():
    """
    >>> print(show_me_the_money())
    $
    """

    return '$'


if __name__ == '__main__':
    import doctest

    doctest.testmod()

执行之后是正常输出

总结

如何来体会 Python 中的单元测试,直接在自己的项目中写段单元测试代码吧, show me the code 最实在了。所谓实践就得,Think -> Do -> Done -> Think

  • Think:就是得有这个意识或者说想法吧,没有意识的话,一切无从谈起。
  • Do:在自己参与的项目中,先开始尝试着写上一段单元测试代码吧。比如:修复缺陷的时候,增加新特性的时候等等。
  • Done:成为一种习惯,最后就跟呼吸一样,如果停止,你会觉得难受。
  • Think:继续 Think,实践过后,每个人一定会有自己的感悟和理解。作为一个思考者、改良者、传道者,分享出来你的看法和经验吧。

这里只是很简单地介绍了下 Python 中的单元测试,更详细的可以把官方手册相关部分完整的读一遍。

这里讲的示例,估计实际项目中用起来,也就能应付个基本的加减乘除那种业务逻辑的场景。实际的项目,根据不同类型的开发项目,会有各种需要模拟的测试场景,这个时候一般需要借助更高级抽象的单元测试框架、模块,比如:

  • 可能你自己的项目中已经积累了适合你项目的单元测试类库,这样就挺好。
  • 还有各种成熟的各种开源开发库,比如:Python 的 Web 开发框架 Django,它里面就提供了适合 Web 开发场景的单元测试各种类库。
  • 还有需要模拟各种情况的类库,比如:网络请求、数据库存储、读写文件等等,Python 中就提供了不少好的模拟的库(可以 Google 下 Python Mock).

参考

https://docs.python.org/3/library/unittest.html

https://pm.readthedocs.io/unittest/python.html