8

我正在编写一个 python 模块,我想对它进行单元测试。我是 python 新手,对可用的选项有些迷惑。

目前,我想将我的测试编写为doctest,因为我喜欢声明式而不是命令式的风格(但是,如果它被误导,请随时取消我的这种偏好)。然而,这提出了几个问题:

  1. 我应该把测试放在哪里?在与他们正在测试的代码相同的文件中(或在 doctests 的文档字符串中)?还是认为将它们分开到自己的目录中更好?
  2. 如何从命令行一次性运行整个模块中的所有测试?
  3. 如何报告测试套件的代码覆盖率?
  4. 对于 python 中的单元测试,我应该注意哪些其他最佳实践?
4

6 回答 6

12

如果有误,请随时取消我的此偏好

我相信我比任何其他开源开发人员都doctest更广泛地使用(扩展其预期使用边界的方式),至少在一个项目中——我的gmpy项目中的所有测试都是 doctests。它在刚开始时是全新的,这似乎是一个很棒的小技巧,如果某件事值得做,那就值得做多余的事情——对吧?-)gmpy

错误的。除了gmpy,将所有内容重做为适当的单元测试将需要太多的返工,我再也没有犯过这个错误:这些天,我使用单元测试作为单元测试,而 doctests 只是为了检查我的文档,因为它们一直都是意味着被使用。doctest 所做的(将预期结果与实际结果进行比较——仅此而已)并不是构建可靠测试套件的良好基础。从未有过其他打算。

我建议你看看鼻子。新的 Python 2.7 中的unittest模块更加丰富和美观,如果您卡在 2.4、2.5 或 2.6 上,您仍然可以使用可以下载和安装的unittest2的新功能;nose互补unittest的很好。

如果你不能忍受 unittest(但是——试一试,它会在你身上生长!-),也许可以试试py.test,一个具有完全不同理念的替代包。

但是,不要doctest试图测试文档中示例以外的内容!完全相等的比较经常会阻碍你,因为我不得不以我的(隐喻的;-)费用来学习gmpy......

于 2010-07-14T02:15:19.790 回答
3

I don't like doctests for these reasons:

  • You can't run a subset of the tests. When a test fails, it's useful to run just one test. Doctest provides no way to do that.
  • If a failure happens in the middle of the doctest, the whole thing stops. I'd rather see all the results to decide how to tackle a breakage.
  • The coding style is stylized, and has to have printable results.
  • Your code is executed in a special way, so it's harder to reason about how it will be executed, harder to add helpers, and harder to program around the tests.

This list was taken from my blog post Things I don't like about doctest, where there's more, and a long thread of comments debating the points.

About coverage: I don't believe there's a coverage tool for Python that will measure coverage within doctests. But since they are simply long lists of statements with no branches or loops, is that a problem?

于 2010-07-14T02:25:35.220 回答
2

我怀疑 Alex 在程序员的曲线上可能比我领先一点,但如果你想要有一些 Python 经验的人(作为“用户”而不是专家或传道者)的观点,但不是同一个联盟,我对单元测试的发现几乎是一样的。

Doctests 一开始对于简单的测试可能听起来很棒,我在家里的一些个人项目中朝着这个方向前进,因为它已在其他地方被推荐。在工作中我们使用nose(尽管如此罐装和包装我的印象是直到不久前我们一直在使用pyUnit),几个月前我也搬到了家里的鼻子。

最初的设置时间和管理开销,以及与实际代码的分离,一开始似乎没有必要,特别是当您测试的代码库不是那么大的东西时,但从长远来看,我发现 doctests 越来越以我想做的每一次重构或重组的方式,很难维护,几乎不可能扩展,并且很快就抵消了最初的节省。是的,我知道单元测试与集成测试不同,但文档测试往往过于严格地为您定义单元。如果您确定它是有效的草图工具或开发模型,它们也不太适合基于单元的敏捷。

您可能需要花一些时间来计划,然后按照 pyUnit 或鼻子引导您的方式改进您的单元测试,但很有可能即使在短期内,您也会发现它实际上在许多层面上都对您有所帮助。我知道它对我有用,而且我对这些天正在处理的代码库的复杂性和规模还比较陌生。刚开始的几周必须咬紧牙关。

于 2010-07-14T05:36:46.810 回答
1

For coverage, check out the excellent coverage.py.

Otherwise, everything Alex Martelli wrote is very much on point.

于 2010-07-14T02:26:11.963 回答
1

doctests are great for quick, minor unit tests that describe what some of the basic usages of the objects in question, (as they show up in docstrings, and hence help(whatever), etc).

I've personally found extensive and more thorough testing to be more effective using the unittest module, and now the 2.7 module (back ported to unittest2) has even more handy assertions. You can set up test suites and as complex a scenario as you like with the unit testing framework and cover whole swaths of different tests in one go (command-line wise)

coverage.py, by Ned Batchelder and as @bstpierre mentions will work with either of these, and I recommend it for seeing what you've got tested of the code and what doesn't. You can add it into a CI system (i.e. Hudson or whatever you like to use) to keep up on what's covered and not, and the HTML reports are fantastic for seeing what hasn't been hit with testing coverage. Coverage supports Junit xml output, which many CI systems know how to provide charted on-going results to let you see the build getting better or worse over time.

于 2010-07-14T02:31:29.223 回答
0

我同意上述关于 doctest 不缩放的所有观点,我更喜欢坚持使用 unittest。

我可以贡献的一个技巧是从代码处理中调用单元测试,__name__ == "__main__因此如果测试文件作为脚本运行,它将运行它的测试。

例如:

#!/usr/bin/env python

"""
Unit tests for the GetFiles.py utility
"""

import unittest
from FileUtilities import getTree

class TestFileUtilities(unittest.TestCase):

   def testGetTree(self):
      """
      Tests that a known tree is found and incidentally confirms
      that we have the tree we expected to use for our current
      sample extraction.
      """
      found = getTree('./anzmeta-dtd', '.pen')
      expected_path_tail = ['ISOdia.pen',
                       'ISOgrk1.pen',
                       'ISOtech.pen']
      for i, full_path in enumerate(found):
         assert full_path.endswith( expected_path_tail[i] ), expected_path_tail[i]

# other tests elided         

if __name__ == "__main__":
   # When this module is executed from the command-line, run all its tests
   unittest.main()
于 2010-07-14T08:49:10.410 回答