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` を使っている場合は、さらに注意 一部のパッケージ[wxPython](https://github.com/wxWidgets/Phoenix/blob/master/src/__init__.py) などでは、名前空間をシンプルに保つため、`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 件のコメント: