2018/04/26

doctest を unittest と統合して実行する (Python)

doctest とは

ここに検索で来られた方には不要かもしれませんが、説明の都合上、簡単に書いておきます。

doctest は、以下のような docstring 内の対話実行例が正しく実行できるか確認するモジュールです。 docstring 内だけでなく、テキストファイルの実行例を確認する事も可能です。

core.py
1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*-
 
 
def times(a, b):
    """
    2つの入力を掛け算して出力
 
    >>> times(4, 9)
    35
    """
 
    return a * b

例えば、上記のコードを doctest にかけると間違いを指摘してくれます。

$ python -m doctest mytestlib/core.py
**********************************************************************
File "mytestlib/core.py", line 8, in core.times
Failed example:
    times(4, 9)
Expected:
    35
Got:
    36
**********************************************************************
1 items had failures:
   1 of   1 in core.times
***Test Failed*** 1 failures.

さらに詳しい情報は公式ドキュメントをご確認ください。

参考

26.3. doctest — 対話的な実行例をテストする — Python 3.6.5 ドキュメント

doctest を unittest に統合する

doctest も定常的に実行しないと意味がないので、普段のテスト実行に統合したいと思うのは当然でしょう。

別途スクリプトを書くことも可能ですが、doctest はユニットテストのインスタンスを生成するAPIを提供しているのでこれを使うのが簡単です。

参考

26.3. doctest — 対話的な実行例をテストする — Python 3.6.5 ドキュメント

以下のようなディレクトリ構造があるとして、test_core.py がユニットテストファイルだとします。

ユニットテストモジュールの中で load_tests() 関数を定義し、doctest.DocTestSuite() によって生成されたユニットテストインスタンスを tests に加えます。 これにより、通常のユニットテストに加えて、doctest が実行されるようになります。

tests/test_core.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import unittest
import doctest
import mytestlib.core
 
# ここに普通のユニットテスト
 
 
def load_tests(loader, tests, ignore):
    tests.addTests(doctest.DocTestSuite(mytestlib.core))
    return tests
 
 
if __name__ == '__main__':
    unittest.main()

__init__.py 内で名前空間を再定義している場合は注意

これだけだと公式ドキュメントを読めば良いのですが、上記のようにパッケージを定義している場合、少し注意が必要です。

パッケージの場合、__init__.py の中で以下のように名前空間を再定義しているケースが多いと思います。

mytestlib/__init__.py
1
2
3
# -*- coding: utf-8 -*-
 
from .core import *

この場合、mytestlib.times()mytestlib.core.times() もアクセス可能なので、以下のように書いても動作するように思えます。 (.core が削除されている)

11
tests.addTests(doctest.DocTestSuite(mytestlib))

ところが、これでは doctest が実行されません。 doctest の仕様上、import されたオブジェクトは検索対象から外れてしまうからです。 必ずオリジナルのモジュールを参照するようにしましょう。

del を使っている場合は、さらに注意

一部のパッケージ1では、名前空間をシンプルに保つため、del を用いて大元のモジュールを削除しているケースがあります2

mytestlib/__init__.py
1
2
3
4
5
# -*- coding: utf-8 -*-
 
from .core import *
 
del core

この場合、mytestlib.core を参照できなくなるので、上記のやり方だとうまくいきません。

AttributeError: module 'mytestlib' has no attribute 'core'

その場合は、以下のように 'mytestlib.core' と文字列でモジュール名を指定すると動作するようになります。

tests/test_core.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import unittest
import doctest
# import mytestlib.core
 
# ここに普通のユニットテスト
 
 
def load_tests(loader, tests, ignore):
    tests.addTests(doctest.DocTestSuite('mytestlib.core'))
    return tests
 
 
if __name__ == '__main__':
    unittest.main()
  1. wxPython など 
  2. ベストプラクティスなのかどうかは怪しい 
?

0 件のコメント: