Python单元测试-unittest

unittest作为一个python中的基本模块,是其他框架和工具的基础,官方文档神马的最实用了:https://docs.python.org/2/library/unittest.html
对官方文档做了一个粗略的翻译,稍有调整。

python版本:2.7

基本概念

  • test fixture
    一个test fixture代表执行一个或者多个测试时需要准备环境,以及相关联的清理环境的工作。这包含很多内容,比如创建临时的数据库、目录等。
  • test case
    一个test case就是测试用例,测试当中的最小单元。unittest提供一个基本的类TestCase,用来创建一个test case。
  • test suite
    test suite是一组test case或者test suite的集合,也可以两者都有。用来将需要一同执行的测试用例聚合到一起。
  • test runner
    一个test runner是用来执行测试用例的,对测试进行编排并把结果返回给用户。

一个TestCase的实例应该是完全独立的,可以独立的运行测试,也可以和其他测试用例进行组合。最简单的TestCase子类简单的重写runTest()方法执行特定代码就可以:

    import unittest
    class JustForTest(unittest.TestCase):
        def runTest(self):
            length = 10
            self.assertEqual(10, length)

通过unittest模块可以在命令行下对一个模块、一个类或者是一个方法执行测试,比如对这个test.py模块执行单元测试:

➜ python -m unittest test
.
---------------------------------------------------------
Ran 1 test in 0.000s

OK
#

一个简单用例

unittest模块为构建和执行测试提供了非常丰富的工具集,下面这个例子用来测试三个字符串的方法:

    import unittest
    class TestStringMethods(unittest.TestCase):
        def test_upper(self):
            self.assertEqual('foo'.upper(), 'FOO')
        def test_isupper(self):
            self.assertTrue('FOO'.isupper())
            self.assertFalse('Foo'.isupper())
        def test_split(self):
            s = 'hello world'
            self.assertEqual(s.split(), ['hello', 'world'])
            with self.assertRaises(TypeError):
                s.split(2)
    if __name__ == '__main__':
        unittest.main()

测试用例是unittest.TestCase的子类,三个独立的测试是以test开头的。用这样的命名规则来约定哪些方法是test runner需要执行的。
每个测试的关键在于调用assertEqual()进行检查期望的结果;assertTrue()和assertFalse()用来判断一个条件;assertRaises()用来确认抛出了一个指定的异常。这些方法用来代替assert语句,test runner可以收集所有的结果并生成最后的报告。

代码总13~14行是一个简单的用来执行测试的方式。unittest.main()给测试脚本提供了一个命令行接口。当从命令行执行的时候,上面的脚本会有如下输出:

➜ python basic_example.py 
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

不使用unittest.main()的话也可以用其他方式替代,比如13、14行可以替换为:

    #if __name__ == '__main__':
    #    unittest.main()
    suite = unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)
    unittest.TextTestRunner(verbosity=2).run(suite)

可以得到更加清晰的输出:

➜  python_unit_test_study python basic_example.py 
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK

命令行接口

unittest模块可以从命令行执行模块中的测试,可以是一个类也可以使单独的测试用例:

➜  python -m unittest basic_example.TestStringMethod$

...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK
➜  python_unit_test_study python -m unittest basic_example.TestStringMethods
.test_isupper
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

也可以传递一个模块列表,执行多个模块中的测试用例:

➜  python -m unittest test_module1 test_module2

-v参数可以获得更多的内容:

➜  python -m unittest -v basic_example 
test_isupper (basic_example.TestStringMethods) ... ok
test_split (basic_example.TestStringMethods) ... ok
test_upper (basic_example.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

-h可以获得完整命令行参数的帮助说明:

➜ python -m unittest -h              
Usage: python -m unittest [options] [tests]

Options:
  -h, --help       Show this message
  -v, --verbose    Verbose output
  -q, --quiet      Minimal output
  -f, --failfast   Stop on first failure
  -c, --catch      Catch control-C and display results
  -b, --buffer     Buffer stdout and stderr during test runs

Examples:
  python -m unittest test_module               - run tests from test_module
  python -m unittest module.TestClass          - run tests from module.TestClass
  python -m unittest module.Class.test_method  - run specified test method

[tests] can be a list of any number of test modules, classes and test
methods.

Alternative Usage: python -m unittest discover [options]

Options:
  -v, --verbose    Verbose output
  -f, --failfast   Stop on first failure
  -c, --catch      Catch control-C and display results
  -b, --buffer     Buffer stdout and stderr during test runs
  -s directory     Directory to start discovery ('.' default)
  -p pattern       Pattern to match test files ('test*.py' default)
  -t directory     Top level directory of project (default to
                   start directory)

For test discovery all test modules must be importable from the top
level directory of the project.

测试发现

unittest支持非常简单的测试发现。为了兼容测试发现,所有的测试文件(test_xxx.py)必须是可从项目的顶级目录导入的模块或包。测试发现由TestLoader.discover()实现,但是可以通过命令行使用,基本的用法如下:

➜  python -m unittest discover             
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

➜  python_unit_test_study python -m unittest discover -h
Usage: python -m unittest discover [options]

Options:
  -h, --help            show this help message and exit
  -v, --verbose         Verbose output
  -f, --failfast        Stop on first fail or error
  -c, --catch           Catch Ctrl-C and display results so far
  -b, --buffer          Buffer stdout and stderr during tests
  -s START, --start-directory=START
                        Directory to start discovery ('.' default)
  -p PATTERN, --pattern=PATTERN
                        Pattern to match tests ('test*.py' default)
  -t TOP, --top-level-directory=TOP
                        Top level directory of project (defaults to start
                        directory)

组织代码

单元测试最基本的结构应该是测试用例——必须设置并检查正确性的单独场景。在unittest中,测试用例就是unittest的TestCase类的一个实例。写测试代码时,必须书写TestCase的子类,或者使用FunctionTestCase。
文章开头讲过,最基本的TestCase子类就是重写runTest()方法即可,在runTest()中执行响应的测试代码。比如:

    import unittest
    class DefaultWidgetSizeTestCase(unittest.TestCase):
        def runTest(self):
            widget = Widget('The widget')
            self.assertEqual(widget.size(), (50, 50), 'incorrect default size')

为了执行测试内容,使用TestCase基类提供的assert*()中的方法来检查结果。如果测试失败,就会抛出异常,unittest会将这个测试标记为Failure。而其他的异常都会被当做Error处理。这可以帮助我们判断代码中的问题:failure是由于测试结果引起的错误-期望值是5得到的却是6。而Errors是由于代码本身的错误引起,比如常见的TypeError等。
当我们对同一类型的测试内容写测试代码时,很多测试方法的构建和初始化可能是重复的,这种情况下我们可以使用setUp()方法,setUp()可以将初始化统一抽离出来,在一个测试方法执行前都会先执行,比如:
如果setUp()方法抛出异常的话,测试框架认为测试过程遇到了错误,runTest()方法不会被执行的。

    import unittest
    class SimpleWidgetTestCase(unittest.TestCase):
        def setUp(self):
            self.widget = Widget("The widget")
    class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):
        def runTest(self):
            self.assertEqual(self.widget.size(), (50, 50),
                             'incorrect default size')
    class WidgetResizeTestCase(SimpleWidgetTestCase):
        def runTest(self):
            self.widget.resize(100, 150)
            self.assertEqual(self.widget.size(), (100, 150),
                             'wrong size after resize')

执行结果:

➜  python -m unittest test_widget   
EE
======================================================================
ERROR: runTest (test_widget.DefaultWidgetSizeTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_widget.py", line 5, in setUp
    self.widget = Widget("The widget")
NameError: global name 'Widget' is not defined

======================================================================
ERROR: runTest (test_widget.WidgetResizeTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_widget.py", line 5, in setUp
    self.widget = Widget("The widget")
NameError: global name 'Widget' is not defined

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

FAILED (errors=2)

同样的,我们可以使用tearDown()方法在runTest()执行结束后进行清理,如果setUp()方法执行成功,不论runTest()是否成功,tearDown()都会执行。
这样一个初始化、清理的完整的测试环境叫做fixture,通常很多小的测试使用的是相同的fixture的,

    import unittest
    class WidgetTestCase(unittest.TestCase):
        def setUp(self):
            self.widget = Widget('The widget')
        def tearDown(self):
            self.widget.dispose()
            self.widget = None
        def test_default_size(self):
            self.assertEqual(self.widget.size(), (50, 50),
                             'incorrect default size')
        def test_resize(self):
            self.widget.resize(100, 150)
            self.assertEqual(self.widget.size(), (100, 150),
                             'wrong size after resize')

这里没有使用runTest()方法,但是使用了其他两个测试方法作为替代。测试类实例会执行每一个test_*()方法,self.widget会对每一个实例进行创建和销毁。这种构建方法,创建实例的时候我们需要具体的指出需要执行的测试方法:

    defaultSizeTestCase = WidgetTestCase('test_default_size')
    resizeTestCase = WidgetTestCase('test_resize')

unittest提供test suite(测试套件)可以将测试用例的实例很好的按照功能特性进行合理的组织,在unittest中通过TestSuite类实现:

    widgetTestSuite = unittest.TestSuite()
    widgetTestSuite.addTest(WidgetTestCase('test_default_size'))
    widgetTestSuite.addTest(WidgetTestCase('test_resize'))

为了方便测试,在每一个模块中提供一个可调用的预构建的test suite是一个不错的主意:

    def suite():
        suite = unittest.TestSuite()
        suite.addTest(WidgetTestCase('test_default_size'))
        suite.addTest(WidgetTestCase('test_resize'))
        return suite

也可以这样写:

    def suite():
        tests = ['test_default_size', 'test_resize']
        return unittest.TestSuite(map(WidgetTestCase, tests))

由于使用相似名字的测试函数来创建一个TestCase子类是非常通用的模式,unittest提供了一个TestLoader类可以自动的创建测试套件并用独立的测试进行填充,比如:

    suite = unittest.TestLoader().loadTestsFromTestCase(WidgetTestCase)

这就创建了一个测试套件,将会执行WidgetTestCase.test_default_size()和WidgetTestCase.test_resize。TestLoader将会自动的识别以test_开头的测试方法。

各个测试的执行顺序是由测试的函数名按照字符串内建顺序执行的。
测试套件本身也可以像测试用例一样组织起来:

    suite1 = module1.TheTestSuite()
    suite2 = module2.TheTestSuite()
    alltests = unittest.TestSuite([suite1, suite2])

我们可以将测试用例与测试套件的定义放在与测试代码相同的模块中,但将测试代码放在单独的模块中又几个好处:

  • 测试模块可以独立的通过命令行进行执行
  • 测试代码可以更容易的与发布代码分离
  • 没有必要的情况下不必为了适应被测试代码而频繁更改测试代码
  • 测试的代码更容易重构
    。。。等等

跳过测试以及异常测试

unittest支持跳过单独的测试方法甚至是整个测试类。也支持将一个测试标记为“expected failure”。
跳过一个测试可以使用skip()描述符或者它的一个条件语句,最基本的skip使用像这样:

    import unittest
    import sys
    class MyTestCase(unittest.TestCase):
        @unittest.skip('demonstrating sipping')
        def test_nothing(self):
            self.fail("shouldn't happen")
        @unittest.skipUnless(sys.platform.startswith("win"), 'requires Windows')
        def test_windows_support(self):
            pass

执行结果:

➜  python_unit_test_study python -m unittest -v mylib
test_nothing (mylib.MyTestCase) ... skipped 'demonstrating sipping'
test_windows_support (mylib.MyTestCase) ... skipped 'requires Windows'

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK (skipped=2)

如果想跳过整个测试类,方法是和上面是一样的。
期望失败通过expectedFailure()描述符实现的:

        @unittest.expectedFailure
        def test_format(self):
            pass

执行结果:

➜ python -m unittest -v mylib
test_format (mylib.MyTestCase) ... unexpected success

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK (unexpected successes=1)

被跳过的测试不会执行setUp和tearDown,被跳过的类也不会执行setUpClass和tearDownClass。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容