2018/04/26
doctest を unittest と統合して実行する (Python)
### doctest とは
ここに検索で来られた方には不要かもしれませんが、説明の都合上、簡単に書いておきます。
doctest は、以下のような docstring 内の対話実行例が正しく実行できるか確認するモジュールです。
docstring 内だけでなく、テキストファイルの実行例を確認する事も可能です。
```python
`title: "core.py";
# -*- coding: utf-8 -*-
def times(a, b):
"""
2つの入力を掛け算して出力
>>> times(4, 9)
35
"""
return a * b
```
例えば、上記のコードを doctest にかけると間違いを指摘してくれます。
```console
`gutter: false;
$ 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 ドキュメント](https://docs.python.jp/3/library/doctest.html)
### doctest を unittest に統合する
doctest も定常的に実行しないと意味がないので、普段のテスト実行に統合したいと思うのは当然でしょう。
別途スクリプトを書くことも可能ですが、doctest はユニットテストのインスタンスを生成するAPIを提供しているのでこれを使うのが簡単です。
> 参考
>
> [26.3. doctest — 対話的な実行例をテストする — Python 3.6.5 ドキュメント](https://docs.python.jp/3/library/doctest.html#unittest-api)
以下のようなディレクトリ構造があるとして、`test_core.py` がユニットテストファイルだとします。
ユニットテストモジュールの中で `load_tests()` 関数を定義し、`doctest.DocTestSuite()` によって生成されたユニットテストインスタンスを `tests` に加えます。
これにより、通常のユニットテストに**加えて**、doctest が実行されるようになります。
```python
`title: "tests/test_core.py"; highlight: [12, 13];
#!/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` の中で以下のように名前空間を再定義しているケースが多いと思います。
```python
`title: "mytestlib/__init__.py";
# -*- coding: utf-8 -*-
from .core import *
```
この場合、`mytestlib.times()` も `mytestlib.core.times()` もアクセス可能なので、以下のように書いても動作するように思えます。
(`.core` が削除されている)
```python
`first-line: 11;
tests.addTests(doctest.DocTestSuite(mytestlib))
```
ところが、これでは doctest が実行されません。
doctest の仕様上、import されたオブジェクトは検索対象から外れてしまうからです。
必ずオリジナルのモジュールを参照するようにしましょう。
### `del` を使っている場合は、さらに注意
一部のパッケージでは、名前空間をシンプルに保つため、`del` を用いて大元のモジュールを削除しているケースがあります。
```python
`title: "mytestlib/__init__.py"; highlight: 5;
# -*- coding: utf-8 -*-
from .core import *
del core
```
この場合、`mytestlib.core` を参照できなくなるので、上記のやり方だとうまくいきません。
```
AttributeError: module 'mytestlib' has no attribute 'core'
```
その場合は、以下のように `'mytestlib.core'` と文字列でモジュール名を指定すると動作するようになります。
```python
`title: "tests/test_core.py"; highlight: [6, 12];
#!/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()
```

0 件のコメント:
コメントを投稿