[Python] 関数テストと手法の概要比較（doctest, unittest, pytest）

# 初めに
Pythonのテストについて調べたことをまとめた。
本記事では単体テストで主に用いられているdoctest、unittest、pytestを取り扱う。
それぞれの概要と最初のお試しレベルの簡便な実施テスト例を記載している。

|module|概要|特徴
|---|---|---|
|doctest|docstringを使用した標準モジュール|docstringのみで簡単だが他モジュールの使用制限がある。|
|unittest|他言語と類似する標準モジュール|標準だが他言語と類似形式のためpythonらしくない（らしい）。|
|pytest|サードパーティのテストモジュール|pythonらしくテストが記述できる。現在比較的人気らしい。|

実行環境は以下の通り。

|||
|---|---|
|os|windows|
|python|3.7.1|
|pytest|6.1.1|
|anaconda|4.8.5|

## 自身の関連記事
・docstringのスタイルと書き方。（2021/01/30）
　[[Python] docstringのスタイルと書き方](https://qiita.com/flcn-x/items/393c6f1f1e1e5abec906)
・VSCodeでのdocstring生成に関する機能拡張。（2021/02/01）
　[[Python] VSCodeでのdocstring生成拡張](https://qiita.com/flcn-x/items/724fb59bcdf54758aead)
・docstringを利用した関数テスト。（2021/02/07）←本記事
　[[Python] 関数のテストとその手法概要（dcstest, unittest, pytest）](https://qiita.com/flcn-x/items/fcbbc2fb291b970290f2)

# テストとは
コーディングにおけるテストとは、仕様の達成または未達成の発見を確認すること。

>ソフトウェアテストは、コンピュータのプログラムから仕様にない振舞または欠陥（バグ）を見つけ出す作業のことである。
ソフトウェアテストで見つかったプログラム中の欠陥を修正する作業をデバッグという。
[@wiki](https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%83%86%E3%82%B9%E3%83%88)

テストはフェーズ毎にその粒度や検証数が大きく異なるので、同一視できない。
したがって、どのフェーズでのテストかを意識することは採用する方法や範囲を分けて考える必要がある。

|フェーズ|説明|
|---|---|
|単体テスト|一部分 ( モジュール内のクラスや関数 )のテスト|
|結合テスト|機能を結合したもの（単体テスト済みのモジュールを組み合わたもの）のテスト|
|システムテスト|システム全体（機能・非機能）のテスト|

## テストフレームワーク
テストフレームワークとは、テストを行うフレームワーク（モジュール）のこと。
サポートのためのフレームワークはフェーズごと、用途ごと言語ごとなどにより数多く存在する。
[ユニットテスト・フレームワーク一覧 @wiki](https://ja.wikipedia.org/wiki/%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88%E3%83%BB%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E4%B8%80%E8%A6%A7)　では70種以上紹介されている。


>ソフトウェアフレームワークとは、プログラミングにおいて、アプリケーションプログラム等に必要な一般的な機能が、あらかじめ別に実装されたものである。
[@wiki](https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF)

# doctest
doctestとは、ドキュメント(docstring)として書かれたテストを実行するための標準モジュール。
docstringに入出力を書くだけでテストコードになるため、説明のためのdocstringとテストを同時に運用できる。
ただし、テストに際しての他モジュールの利用が制限されていたり、ソースコード自体の肥大化などがデメリットとなる。

>doctest モジュールは、対話的 Python セッションのように見えるテキストを探し出し、セッションの内容を実行して、そこに書かれている通りに振舞うかを調べます。 
doctest は以下のような用途によく使われています:
>
 * モジュールの docstring (ドキュメンテーション文字列) 中にある対話実行例のすべてが書かれている通りに動作するか検証することで、docstring の内容が最新かどうかチェックする。
 * テストファイルやテストオブジェクト中の対話実行例が期待通りに動作するかを検証することで、回帰テストを実現します。
 * 入出力例を豊富に使ったパッケージのチュートリアルドキュメントが書けます。
入出力例と解説文のどちらに注目するかによって、ドキュメントは「読めるテスト」にも「実行できるドキュメント」にもなります。
>
[公式：doctest --- 対話的な実行例をテストする](https://docs.python.org/ja/3/library/doctest.html)

## テスト方法
テストの実施に関して以下にまとめる。

 * テスト対象とするコードに対して、テストのためのdocstringを記載する。
    * 該当の関数とその引数（具体値）を記載する。
    * 期待される戻り値を直下に記載する。
 * テスト対象のソースコード実行時にテストが実施されるように記載する。
    * doctestをインポートする。
    * テストの実行メソッドdoctest.testmod()を記載する。
 * doctest.testmod()を実行することでテストを実施する。
 * 結果がterminalに出力される。

### docstringの記載方法
実行時にdoctestを実行するためのコードを記載する。
GoogleスタイルやNumPyスタイルの記述時にはExamplesに記載する。
docstringについては過去の記事を参考にしてもらいたい。
[[自身の記事] [Python] docstringのスタイルと書き方](https://qiita.com/flcn-x/items/393c6f1f1e1e5abec906)

```
"""
>>> func(arg1, arg2) # 実行する関数とその引数(具体値)
value # 期待される戻り値(具体値)
"""
```

### テストコード
以下のようなコードを作成し、実行することでテストを行う。
テスト結果については、エラー時のみ出力される。※設定で変更可能

```Python:main.py
def func(arg1 : int, arg2 : int):
    """sum
    
    >>> func(1, 2)
    3
    """
    return arg1 + arg2

if __name__ == '__main__':
    import doctest # doctestのための記載
    doctest.testmod()
```

## テストのエラー結果

テストの実行は、上記main.pyをターミナルで実行する。

```:tarminal
>>> python main.py
```

エラーが発生するように戻り値を加工した。

```Python:main.py
def func(arg1 : int, arg2 : int):
    """sum
    
    >>> func(1, 2)
    3
    """
    return arg1 + arg2 * 2 #  * 2 を追加し、エラーになるように変更

if __name__ == '__main__':
    import doctest
    doctest.testmod()
```

### エラーメッセージ
ターミナルで実行した際には、エラーメッセージがターミナルに出力される。

```console:terminal
**********************************************************************
File "xxx/main.py", line 7, in __main__.func
Failed example:
    func(1, 2)
Expected:
    3
Got:
    5
**********************************************************************
1 items had failures:
   1 of   1 in __main__.func
***Test Failed*** 1 failures.
```

# unittest
unittestとは、テストを行うためのテストフレームワークで標準モジュール。
JUnitに類似したものとして開発されており、他言語と類似形式でテストを実施できる。

## テスト方法
テストの実施に関して以下にまとめる。

 * テスト対象となるコードを作成する。（通常）
 * テストを実行するコードを作成する。
    * unittestをインポートする。
    * テスト対象となるモジュールをインポートする。
    * テスト用のクラス [ TestXxxxYyyy ] を作成する。
        * テスト用のクラスはunittest.TestCaseを継承する。
        * テスト用のメソッドを [ TestXxxxYyyy ] 内に記載する。
            * テストの実行は、self.assertZzzz() （※合否判断のためのメソッド）で行う。
 * unittest.main()を実行することでテストを実施する。
 * 結果がterminalに出力される。

### テストコード

対象とする関数を含むモジュールは以下の通り。

```python:main.py
def func(arg1 : int, arg2 : int):
    return arg1 + arg2
```

上記のモジュール内の関数をテストするためのコードは以下の通り。

```python:test_main.py
import unittest # testのためのライブラリ
import main # test対象のライブラリ

class TestFunc(unittest.TestCase): # テストのためのクラス
    def test_func(self): # 関数テストのためのメソッド
        value1 = 1 # 引数1
        value2 = 2 # 引数2
        expected = 3 # 期待値
        actual = main.func(value1, value2) # 関数実行結果
        self.assertEqual(expected, actual) # 合否判断（結果比較）

if __name__ == '__main__':
    unittest.main()
```

## テストのエラー結果

テストの実行は、ターミナルにてモジュールを実行することでテストを行う。

```:tarminal
>>> python test_main.py
```

エラーが発生するように以下のように変更して実行する。
なお、テスト用のtest_main.pyは変更なし。

```python:main.py
def func(arg1 : int, arg2 : int):
    return arg1 + arg2 * 2 # * 2 を追加し、エラーになるように変更
```

### エラーメッセージ

エラーメッセージが以下のように出力される。

```console:terminal
F
======================================================================
FAIL: test_func (__main__.TestFunc)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_main.py", line 10, in test_func
    self.assertEqual(expected, actual)
AssertionError: 3 != 5

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
```
# pytest
pytestとは、テストを行うためのテストフレームワークのモジュール。
pytestの方がpython的な記載が可能。

|pytest|UnitTest|
|---|---|
|サードパーティ（インストールが必要）|標準モジュール（インストール不要）|
|継承必要ないため、関数でも実行可能|unittest.TestCaseを継承する必要あり|
|判定部分は比較演算子を使用|assertメソッド複数で判定|
|pep8準拠可能|pep8違反あり|

## インストール
anacondaへのインストールは  [ [ anaconda / packages / pytest ] ](https://anaconda.org/anaconda/pytest) から行う。


```
conda install -c anaconda pytest
```

## テスト方法
テストの実施に関して以下にまとめる。

 * テストコードを実行するファイル名は test_*.py とする。
 * 関数名の頭に [ test ] をつけ、テスト対象とする。
   * 期待する結果をassert文で定義する。
 * pytestを実行することでテストを実施する。
 * 結果がterminalに出力される。

### テストコード

対象とする関数を含むモジュールは以下の通り。

```python:main.py
def func(arg1 : int, arg2 : int):
    return arg1 + arg2
```

上記のモジュール内の関数をテストするためのコードは以下の通り。

```python:test_main.py
from main import func
def test_func():
    value1 = 1
    value2 = 2
    expected = 3
    assert func(value1, value2) == expected
```

## テストのエラー結果
テストの実行は、ターミナルにてモジュールを実行することでテストを行う。
実行はファイル名に [ test ] があるものが対象となる。

```:tarminal
>>> pytest
```

エラーが発生するように以下のように変更して実行する。
なお、テスト用のtest_main.pyは変更なし。

```python:main.py
def func(arg1 : int, arg2 : int):
    return arg1 + arg2 * 2
```

### エラーメッセージ
エラーメッセージが以下のように出力される。

```console:terminal
======================================= test session starts ===============
platform win32 -- Python 3.7.1, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: C:\Users\workspace
plugins: dash-1.16.2
collected 1 item

test_main.py F 

======================================= FAILURES =========================
_______________________________________ test_func _______________________

    def test_func():
E       assert 5 == 3
E        +  where 5 = func(1, 2)

test_main.py:9: AssertionError
====================================== short test summary info ===========
FAILED test_main.py::test_func - assert 5 == 3
====================================== 1 failed in 0.18s =================
```

# Pandas、NumPyのテスト
Pandasは演算・解析ライブラリの充実により、複雑な演算を端的に記述できる。
一方、複雑な演算を軽量に記載した十分に複雑な関数の単体テストは演算そのもののテストは困難となる。
このため、正常な過去のデータ（つまり初回は自力テスト）を設置し、プログラム変更に対して異常な出力にならないかを確認する手法がある。
また、pandasにはpandas.util.testingにassrtionメソッドが用意されている。
また、Numpyにもnumpy.testingにassrtメソッドが用意されている。

# 最後に
Pythonにおける、テスト3種紹介と簡易的な実施方法をまとめた。
各モジュールは用途に応じた設定が多数存在するため、実用する場合はより詳細を理解する必要がある。
また、本紹介に限らず開発するシステムに応じて適切な粒度のテストを考え、それに応じてフレームワークや方法を設置する必要がある。

# 参考・引用

[doctest]
[Pythonでdoctestを試してみる](https://qiita.com/__init__/items/fd3395dc64eee238a773)
[忙しい研究者のためのテストコードとドキュメントの書き方](https://qiita.com/hmkz/items/0689cd85fb3e1adcda1a)

[unittest]
[ユニットテストの書き方！Pythonのunittestを使う方法【初心者向け】](https://techacademy.jp/magazine/18783)
[Python標準のunittestの使い方メモ](https://qiita.com/aomidro/items/3e3449fde924893f18ca)

[pytest]
[pytestに入門してみたメモ](https://qiita.com/everylittle/items/1a2748e443d8282c94b2)

※2021年2月8日記載内容修正