返回介绍

使用 unittest 和 doctest 做测试

发布于 2025-04-20 18:52:16 字数 4495 浏览 0 评论 0 收藏

unittest

unittest 是 Python 标准库里用于单元测试的模块。单元测试用来对最小可测试单元进行正确性检验。产品开发需要快速迭代功能,排期很紧,而写测试看起来是一件很麻烦的事。单元测试的价值就在于维护现有功能时,尤其是不熟悉现有功能的新人,可以通过单元测试确认对代码的修改是否引入了新的错误或导致旧代码产生错误,帮助我们在上线之前就发现问题。Python 标准库和优秀的 Python 项目都附带测试代码,如果一个项目不包含测试代码,很难说服用户它是可信赖的,继而在生产环境中去使用它。笔者在阅读项目源代码的时候,如果遇到文档匮乏或者表述不清、代码晦涩难懂,也会通过查看测试用例来理解。

通过测试 collections 模块中的 Counter 类,先来了解 unittest 的用法(ut_case.py):

import unittest
     
from collections import Counter
     
          
class TestCounter(unittest.TestCase):
    def setUp(self):
        self.c=Counter('abcdaba')
        print 'setUp starting ...'
     
    def test_basics(self):
        c=self.c
        self.assertEqual(c, Counter(a=3, b=2, c=1, d=1))
        self.assertIsInstance(c, dict)
        self.assertEqual(len(c), 4)
        self.assertIn('a', c)
        self.assertNotIn('f', c)
        self.assertRaises(TypeError, hash, c)
                    
    def test_update(self):
        c=self.c
        c.update(f=1)
        self.assertEqual(c, Counter(a=3, b=2, c=1, d=1, f=1))
        c.update(a=10) # a 是累加的
        self.assertEqual(c, Counter(a=13, b=2, c=1, d=1, f=1))
                            
    def tearDown(self):
        print 'tearDown starting ...'
  
      
if __name__=='__main__':
    unittest.main()

setUp 方法列出了测试前的准备工作,常用来做一些初始化,非必需方法。tearDown 方法列出了测试完成后的收尾工作,用来销毁测试过程中产生的影响,非必需方法,但是应该合理使用。

TestCase,顾名思义表示测试用例,一个测试用例可以包含多个测试方法,每个方法都要以 test_开头。测试方法中用到的 self.assertXXX 的方法是断言语句,单元测试都是使用这样的断言语句判断测试是否通过的:如果断言为 False,会抛出 AssertionError 异常,测试框架就会认为此测试用例测试失败。

运行一下:

> python chapter8/section1/ut_case.py
setUp starting ...
tearDown starting...
.setUp starting ...
tearDown starting ...
.
-------------------------------------------------
Ran 2 tests in 0.004s
    
OK

可以看到每次执行 test_开头的方法时都会执行 setUp 和 tearDown。

如果测试失败,会列出出错的详细行数、实际结果和预期结果等帮助我们定位问题。现在修改第 16 行,让它失败:

> python chapter8/section1/ut_case.py
...
======================================================================
FAIL:test_basics (__main__.TestCounter)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "chapter8/section1/ut_case.py", line 16, in test_basics
    self.assertEqual(len(c), 1)
AssertionError:4 !=1
     
----------------------------------------------------------------------
Ran 2 tests in 0.002s
     
FAILED (failures=1)

另一个常见的测试用法是使用 unittest.TestSuite(测试套件),它将一组测试用例作为一个测试对象(ut_suite.py):

import unittest
     
from collections import Counter
     
     
class TestCounter(unittest.TestCase):
 
   def setUp(self):
       self.c=Counter('abcdaba')
       print 'setUp starting ...'
        
   def runTest(self):#需要重载这个方法
       c=self.c
       self.assertEqual(c, Counter(a=3, b=2, c=1, d=1))
        
   def tearDown(self):
       print 'tearDown starting ...'
             
             
if__name__=='__main__':
   suite=unittest.TestSuite()
   suite.addTest(TestCounter()) # 可以用 addTest 添加更多的 TestCase 实例
   runner=unittest.TextTestRunner()
   runner.run(suite)

还可以更细粒度地添加测试用例中的一部分方法(ut_suite_with_case.py):

import unittest
from ut_case import TestCounter
          
          
if__name__=='__main__':
   suite=unittest.TestSuite()
   suite.addTest(TestCounter('test_basics'))
   suite.addTest(TestCounter('test_update'))
   runner=unittest.TextTestRunner()
   runner.run(suite)

笔者对单元测试的理解如下:

  • 软件质量不是测试出来的,而是设计和维护出来的。
  • 并不是所有的模块都需要添加单元测试,单元测试应该用来测试那些可能会出错的地方。与其追求代码覆盖率,不如将重点关注在确保写出更有质量的业务代码和更有意义的测试上。
  • 保持测试的独立性。测试用例之间最好不要有相互依赖,也不能依赖执行的先后次序。

doctest

doctest 模块是一种非常直观的表述型测试方法,它通常查找代码文件里文档字符串中的交互式会话部分,执行那些会话以验证代码工作是否正常(doc_test.py):

def import_object(name):
    """Imports an object by name.
    >>>import os.path
    >>>import_object('os.path') is os.path
    True
    >>>import_object('os.missing_module')
    Traceback (most recent call last):
        ...
    ImportError:No module named missing_module
    """
            
    parts=name.split('.')
    obj=__import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
    try:
        return getattr(obj, parts[-1])
    except AttributeError:
        raise ImportError('No module named{}'.format(parts[-1]))
      
      
if __name__=='__main__':
    import doctest
    doctest.testmod()

也可以使用-m 的方式执行:

python -m doctest chapter8/section1/doc_test.py

没有输出就表示测试通过。

给功能函数添加 doctest 是一个好习惯。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。