【超初心者向け】Pythonのテストの書き方（pytest, unittest）

# 概要

pythonでテストコードを書くときがありますが、（筆者のように）超初心者からすると難しい用語や書き方がたくさん並んでいてハードルが高いです。

テストコードの入口となる最低限（最低限過ぎるかもしれませんが）の書き方を備忘を兼ねて書きます。


## pythonでのテストコードを書く時のライブラリの種類

筆者が簡単に調べたところ、2つのライブラリがよく使われているようです。

- ```unittest``` : python標準ライブラリ。インストールが必要ない。pytestと比較すると、柔軟なテストケースを書きづらい。
- ```pytest``` : サードパーティ製のライブラリ。インストールの必要がある。柔軟なテストケースが書ける。pythonのテストコードを書く時のデファクトスタンダートになりつつある模様（これが本当かは確認していないですが、そういう記述を見かけることが多かったです）。


筆者個人としては、以下の3つの観点で使い分けるのが良さそうだと思いました。

- セキュリティの観点：
    - 会社の環境やプロジェクトによっては、セキュリティが厳しく、サードパーティ製のライブラリを入れられないこともあると思います。その時はpython標準ライブラリであるunittest一択だと思います。
- 気軽さの観点：
    - 筆者が実際に書いていて、気軽にサクッとテストケースを作る場合は、unittestの方が楽だと思いました。（これも場合によると思うのですが、筆者はJupyter notebook形式でコードを書くことが多く、unittestはnotebook内で完結させることができるため、気軽に感じた、という次第です。ピュアなpythonファイル（拡張子が.py）の場合はpytestもunittestも変わらないと思います。）
- 柔軟性や正確さの観点：
    - パラメータを色々変えてテスト（網羅的にテスト）する時は、pytest一択かなと。


# テストケース

単純なもので試したいので、入力された数値（整数）が奇数か偶数かを判定する関数に対しての動作テストをするケースを考えます。

```python
def kisuu_guusuu_hantei(num: int) -> str:
    """
    numが偶数か奇数かを返す関数
    """
    if num % 2 == 0:
        return "偶数"
    else:
        return "奇数"
```


# unittestでのテストの書き方

unittestは、以下のように書きます。


```python
import unittest


# テスト対象となる関数の定義
def kisuu_guusuu_hantei(num: int) -> str:
    """
    numが偶数か奇数かを返す関数
    """
    if num % 2 == 0:
        return "偶数"
    else:
        return "奇数"


# unittestをするためのクラスを定義する
class TestKisuuGusuu(unittest.TestCase):
    def test_2_guusuu(self):
        self.assertEqual(kisuu_guusuu_hantei(2)  , '偶数')
    
    def test_3_kisuu(self):
        self.assertEqual(kisuu_guusuu_hantei(3)  , '奇数')
    
    def test_2_kisuu(self):
        self.assertEqual(kisuu_guusuu_hantei(2)  , '奇数') # あえて違うものにする


# .pyスクリプトを実行するときは、以下で実行できる
#unittest.main()

# Google ColabやJupyter Notebookでは、
# 以下の書き方で実行させる
# （筆者はGoogle Colabで実行したため、
# こちらでじっっこうさせている）
unittest.main(argv=['first-arg-is-ignored'], exit=False)        
```

実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/7efa1e60-4041-48be-124e-271af82bca4e.png)

エラーが起きている場所と、そのエラー内容が出ていることがわかります。

ちなみに、（```test_2_kisuu```メソッドの```"奇数"```と記載している箇所を```偶数```と書き換えて正しいパターンにして）成功する場合は以下のような出力となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/38bec132-b352-55e3-f580-db50185a1736.png)



## ポイント

unittestでは（pytestも類似ルールがあります）、テストコードを書くときのクラスや関数名にルールがあります。

- クラスの名前のルール
    - ```TestKisuuGusuu```というクラスを今回作っていますが、```Test```から始まるクラス名である必要があります。
- メソッドの名前のルール
    - ```test_2_guusuu```などのメソッドを定義していますが、実行したいメソッドには、```test```から始まる名前を付ける必要があります。


# pytestでのテストの書き方

unittestとの違いを主に説明します。unittestはnotebook内でテストケースを書くことができましたが、pytestはできません。pytestの場合は、テストコードをpythonスクリプト（.pyという拡張子）で書いておく必要があります。

従って、ファイル構成は以下のようなものにする必要があります。

```markdown
./sagyou_notebook.ipynb
./func.py
./test_func.py    
```

- ```sagyou_notebook.ipynb``` ：作業しているjupyter notebookです。（pythonスクリプトで作業している場合は、このnotebookは不要です。直接コマンドライン(ターミナルやパワーシェルなど)からコマンドを打てばOKです。）
- ```func.py``` ：テスト対象としてる関数を記載したpythonスクリプトです。
- ```test_func.py``` ：テスト対象に対する実際のテストコードを書いたものです。



```python:sagyou_notebook.ipynb
# pytestはサードパーティ製のライブラリなので、インストールする
!pip install pytest

# pytestを実際に実行させるコマンド
!pytest test_func.py
```


```python:func.py
# この.pyファイルにテスト対象となる関数を記載します。
def kisuu_guusuu_hantei(num: int) -> str:
    """
    numが偶数か奇数かを返す関数
    """
    if num % 2 == 0:
        return "偶数"
    else:
        return "奇数"
```


```python:test_func.py
# この.pyファイルが、
# 実際にテスト対象（func.pyの中の関数）を
#実行させるpythonです。
from func import kisuu_guusuu_hantei

def test_kisuu_guusuu_hantei():
    assert "偶数" == kisuu_guusuu_hantei(2) # 正常ケース
    assert "奇数" == kisuu_guusuu_hantei(2) # 異常ケース
    assert "偶数" == kisuu_guusuu_hantei(3) # 異常ケース
    assert "奇数" == kisuu_guusuu_hantei(3) # 正常ケース
```

実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/0fc6d4f9-f2ed-27d0-ef1c-d020ecac2096.png)

ちゃんとエラーが出ていることがわかります。

## ポイント

unittest同様、pytestも名前の付け方にはお作法（ルール）があります。

- ファイル名
    - テストコードを書いている（今回の場合では、```test_func.py```）pythonスクリプトは、```test```から始まる名前である必要があります。
- 関数名
    - test_func.pyの中の関数の名前も```test``` から始まっている必要があります。

## 問題

エラーが出て止まっていることは良いのですが、異常ケースの一個目（```assert "奇数" == kisuu_guusuu_hantei(2) # 異常ケース```）でテストが止まってしまっており、後続のケースについてのテストができていません。

これでは全パターンを試すことができず、網羅的にいろんなパターンでテストしたい時に使えません。

pytestでは、こういった網羅的にテストしたいケースにも対応しています。網羅的にテストしたい場合は、```test_func.py```を書き換えれば良いです。


```python:test_func2.py
import pytest
from func import kisuu_guusuu_hantei

# テストケースと期待している結果を書く
@pytest.mark.parametrize(("number", "expected"),
    [
        (1,  "奇数"),
        (2,  "奇数"), # あえて奇数にしておく エラーケース
        (3,  "偶数"), # あえて偶数にしておく エラーケース
        (4,  "偶数"),
        (5,  "奇数"),
        (6,  "偶数"),
        (7,  "奇数"),
        (8,  "偶数"),
        (9,  "奇数"),
        (10, "偶数"),
    ]
)
def test_kisuu_guusuu_hantei(number, expected):
    assert kisuu_guusuu_hantei(number) == expected
```

解説の前に実行結果を先に出すと、以下のような出力となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/eecaeec3-0849-4881-3f9f-b1beb274c405.png)

## 解説

テストを実行させる関数を作成し、```test_kisuu_guusuu_hantei```としています。それに対して、```@pytest.mark.parametrize```というデコレータをつけています（デコレータが何か、というのはここでは解説しませんが、関数に対して追加機能をおまけしてくれるやつ、という程度の理解で良いと思います）。そしてそのデコレータの中で、```("number", "expected")```というタプルを定義しており、その中身をListとして定義されている構造です。タプルで定義したこの```("number", "expected")```がそのまま関数の引数の名前として使われます。

このように書くことで、テストしたいケースを網羅的に試すことができます。


# なんでテストコードを書くのか

色々理由はあると思いますし、自分が作ったプログラムが想定通りに動くのかどうかをテストする必要性は当然あると思います。ただ、筆者のようにデータサイエンスを生業としている人間は、テストコードを書くことはあまり多くないと思っています。

そこでデータサイエンスをやっている人間がテストコードを書くべき理由を筆者なりの考えを書きます。

結論は、**そのプログラムの理解度が一番高いのはそのコードを書いたデータサイエンティストだから、他人が読んで理解できるようなヒントとなる情報であるテストケースを書くのは、データサイエンティストであるべし**。他人が書いたプログラムを読み、かつその仕様を細部まで理解するのは大変な労力を要します。特に、データサイエンスの場合は、jupyter notebookを使って、実験的に色々なコードを試し、その中で一番よかったものだけを採用することが多いため、notebookは非常に汚いものとなります。

必要なものと不要なものが混在している中から、必要なものだけを抽出して、それらをつなぎ合わせて理解する、するのは至難の業です。

余談ですが、筆者が思うに、他人の書いたプログラムを理解するときに、難しいと思うポイントは以下だと思っています。①処理の内容の理解が難しい②その処理であるべき必然性を推察することが難しい。

- ①処理の内容の理解が難しい
    - 上でも書いたように、必要な処理と不要な処理が混在している中から、以下を正確に把握するのが難しい。
        - 何をインプットしているのか
        - インプットしたものをどのように加工しているのか
        - 加工したものをどういう形式のアウトプットにしているのか
- ②その処理であるべき必然性を推察するのが難しい
    - 処理の内容を理解したとしても、次の3点も併せて理解しないと、正確に仕様を理解したとは言えない
        - なぜそのデータをインプットしているのか
        - どうしてその加工をするのか、加工の仕方はなぜそのやり方が良いのか
        - なぜその形式のアウトプットである必要があるのか

↑で書いたように、他人が作ったプログラム（特に試行錯誤しながら作ることが多いデータサイエンス領域）の理解は難しく、かつ労力がかかるものであるため、その効率化、補助となるようにテストケースを書いておくことが望ましいと思います。

実際には難しいことが多いと思います（テストケースを書いている時間があれば、もっと別の切り口でのデータ分析をしたり、精度アップのための特徴量エンジニアリングやハイパラチューニングをしたりなど）が、最終的にシステムに乗せて運用していくことを考えると、テストケースを書く習慣をつけると良いのだと思います（自分への戒め含めて）。


# まとめ

- 今回は、pythonでテストケース/テストコードを書く時の超単純なケースのものを書いてみました。
    - 具体的には、```pytest```と```unittest```という2つのライブラリを使ってテストコードを作ってみました。
    - どちらも一長一短で時と場合による使い分けが必要だと思いますが、個人的には、unittestの方がわかりやすいけど、少し勉強して理解すればpytestの方が書きやすいし、使いやすいと感じました。