开始写一个测试文档

doctest模块被包含在python标准库中,你不需要安装就可以开始写doctest。dcotest测试函数的方法和你至今所接触的单元测试略有不同。后者要用明确的方法检查你函数的返回值或者异常,而前者基本上只需要提供该函数在python shell中使用的例子和期望的输出。适当的使用doctest,你可以像unit test一样,像日常测试一样执行他们。就像unit test一样,当你的在python shell中的输出和在doctest描述的一样,他会通过,反之,则会失败。

Python Shell

python shell 是一个交互式的提示,你可以输入、执行python 代码并看到结果。打开一个命令行(*inux的终端),输入python ,敲回车,你可以看到以下内容:

$ python
Python 2.7.6 (default, Jan  7 2014, 12:15:04)
[GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.2.79)] on Darwin
           Type "help", "copyright", "credits" or "license" for
  more
information.
>>>
When you see the >>> prompt, you can start typing Python code, like this:
>>> 1 + 1 2
>>>

原生python shell 允许你执行一个函数并直接看到结果。写一个把两个数字加到一起的函数。调用它并看到结果:

>>> def add(x, y):
...     return x + y
...
>>> add(1,1)
2
>>>

现在你了解了基本的python shell的工作方式,你可以按照相同的逻辑为你的函数写一个简单的doctest。

为函数添加doctest

使用第二章计算机的例子,你可以简单的加上doc string来解释你的函数的作用。说明:doc string是放在函数定义下面,以三个"""来标记的字符串,用来解释函数的作用。

def my_method_with_docstring(arg):
    """This is my doc string.  Here you would describe what the
    method is doing."""
    Doctest extends these strings to include examples of the method in use. Apply a doc string to your add method in the Calculate class from Chapter 2.
    def add(self, x, y):
        """Takes two integers and adds them together to produce
        the result."""
        if type(x) == int and type(y) == int:
            return x + y
    else:
        raise TypeError("Invalid type: {} and {}"
                .format(type(x), type(y)))

这个函数现在清楚的像读者描述了他的作用。你现在可以为读者加一些例子来展示你期望的函数的行为,并将他们按照doctests来运行。加一些当你输入是整数的情况的例子。

def add(self, x, y):
        """Takes two integers and adds them together to produce
        the result.
        >>> c = Calculate()
        >>> c.add(1, 1)
        2
        >>> c.add(25, 125)
        150
        """
        if type(x) == int and type(y) == int:
            return x + y
        else:
            raise TypeError("Invalid type: {} and {}".format(type(x), type(y)))

注意到,你实例化了一个Calaulate类,他可以调用你的函数,然后按照你的期望输出。这是你应该在python shell里面运行的内容。所以如果你不确定放到doctest里面是否正确,请现在python shell里面试一下。

运行你的doctest

现在,你没有办法测试你的doctest。为了让他能运行,当你执行包含该类的python文件的时候,你先要加一块代码。加下面的内容到你文件的最下面:

if __name__ == "__main__":
    import doctest
    doctest.testmod()

现在当你执行的时候,应该是没有输出的。这有一些有悖常理,但是这表示该函数按照你的预期通过啦。~

$ python app/calculate.py
$

为了让你确定你的程序已经被执行,我们可以加上-v标志,来显示正在被运行的测试的信息。

$ python app/calculate.py -v
Trying:
    c = Calculate()
Expecting nothing
ok
Trying:
    c.add(1, 1)
Expecting:
2 ok
Trying:
    c.add(25, 125)
Expecting:
    150
ok
2 items had no tests:
__main__
    __main__.Calculate
1 items passed all tests:
   3 tests in __main__.Calculate.add
3 tests in 3 items.
3 passed and 0 failed.
Test passed.

doctest的输出相当不错,清晰的显示了你的doctest执行的每一行——期望输出什么,是否通过了。doctest失败的时候,你也能得到失败的细节原因。将add(1,1)的结果改成3,去掉-v标志,你就能看到失败的doctest通知。

$ python app/calculate.py
*******************************************************************
File "app/calculate.py", line 10, in __main__.Calculate.add
Failed example:
    c.add(1, 1)
Expected:
3 Got:
    2
****************************************************************
1 items had failures:
   1 of   3 in __main__.Calculate.add
***Test Failed*** 1 failures.

你可以清晰的看到是哪一行导致了这个错误,它期望输出什么,而实际上输出了什么。因此你就可以更新你的doctest文档让他正确,或者改变你的代码,让函数返回和文档一样的值。这次再试试加上-v标准显示更多的细节。

处理错误示例

你当然也可以将错误的示例演示给读者,展示出造成你的函数抛出一个异常的情况,测试它的确会这样发生。(#: todo)

这可能是最好的指导了~当你在你的加函数中用了错误的输入。而加函数只支持两个整数,如果你传递一个float类型的数给该函数,一个 TypeError会被暴露出来。把下面的doctest加到doc string里面,去看看在该情境中异常是否被抛出。

def add(self, x, y):
    """Takes two integers and adds them together to produce
    the result.
    >>> c = Calculate()
    >>> c.add(1.0, 1.0)
    Traceback (most recent call last):
    ...     
    TypeError: Invalid type: <type 'float'> and <type \ 'float'>
    """
    if type(x) == int and type(y) == int:
        return x + y
    else:
        raise TypeError("Invalid type: {} and {}".format(type(x), type(y)))

当你期望某个异常被抛出,你必须以“Traceback(most recent call last):”或者 “Trackback (innermost last):”开头,这取决于在你的代码里这个异常是怎样被抛出的。接着你可以忽略traceback的主体部分,用省略号(...)代替他们,他们通常都是依赖与你正在使用的机器。而最后一行才是真正的你希望抛出的异常,包括了错误信息。当你现在再运行这个程序,它也会没有输出,测试通过。

$ python app/calculate.py
$

运行doctest,加上-v标志,确保你的异常测试被正确执行。

$ python app/calculate.py -v
Trying:
    c = Calculate()
Expecting nothing
ok
Trying:
    c.add(1.0, 1.0)
Expecting:
    Traceback (most recent call last):
     ...
    TypeError: Invalid type: <type 'float'> and <type 'float'>
ok
3 items had no tests:
    __main__
    __main__.Calculate
    __main__.Calculate.__init__
1 items passed all tests:
   4 tests in __main__.Calculate.add
4 tests in 4 items.
4 passed and 0 failed.
Test passed.

高级的doctest使用

如果你写doctest的技巧不够娴熟,他们很快会变的很大并且偏离你实际写的代码。doctest应该始终把阅读代码的人放在第一位并关注他们的感受。要知道,简洁的、描述性的函数的示例可以提供函数的上下文给阅读代码的开发者。你可以在你的doctest中应用一些小技巧来减少你需要写在doctest里的代码的数量。

在那个计算器例子中,你的函数是类的一部分。因为这个原因,当你写doctest的时候,你必须实例化一个Calcute的类,然后才能按照你希望的调用类方法。一个类可能有很多方法,在每个方法里面新建一个实例非常乏味,并且你还要敲很多空格。 其实dcotest支持上下文的传入,它可以在文件中的所有doctest里面调用。所以与其每次新建一个实例 c = Calculate(),你可以在doctest里面做一次。

if __name__ == "__main__":
    import doctest
    doctest.testmod(extraglobs={'c': Calculate()})

有了这一个改进,你就不需要在你的doctest里面创造类实例了,整洁的代码就像下面这样。

def add(self, x, y):
    """Takes two integers and adds them together to produce the result.
    >>> c.add(1,1)
    2
    >>> c.add(25,125)
    150
    >>> c.add(1.0, 1.0)
    Traceback (most recent call last):
     ...
    TypeError: Invalid type: <type 'float'> and <type 'float'>
    """
    if type(x) == int and type(y) == int:
        return x + y
    else:
        raise TypeError("Invalid type: {} and {}".format(type(x), type(y)))