pytestの使い方メモ



## 1. pytestについて


ユニットテストを簡潔に書けるパッケージです


公式ドキュメント
https://docs.pytest.org/en/stable/


## 2. インストール


```terminal
pip install pytest
```


## 3. 使う


### 3-1. 実行


pytestが入った環境でpytestを実行すると「test_*.py」となっているファイルすべてについてpytestを実行してくれます


```terminal:terminal
pytest
```


ファイルを指定して実行することもできます


```terminal:terminal
pytest test_1.py
```


### 3-2. テストの書き方


pyファイル内の関数名が「test_*」となっている関数をすべて順番に実行して、どの関数でエラーが発生したかを教えてくれます


```python:test_1.py
def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5
```


実行すると以下のように表示されます


```python:terminal
> pytest

=================================================== test session starts ===================================================
platform win32 -- Python 3.12.1, pytest-8.3.3, pluggy-1.5.0
rootdir: C:\Users\jjaka\マイドライブ\python\20241009_pytest\code
plugins: anyio-4.3.0
collected 1 item                                                                                                            

test1.py F                                                                                                           [100%]

======================================================== FAILURES ========================================================= 
_______________________________________________________ test_answer _______________________________________________________ 

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test1.py:7: AssertionError
================================================= short test summary info ================================================= 
FAILED test1.py::test_answer - assert 4 == 5
==================================================== 1 failed in 0.09s ==================================================== 
```


今回はエラーがでるようにfuncが書かれているので、後はテストを通るように修正するだけです


### 3-3. 複数テストを実行


1つのファイルに関数名が「test_*」である複数の関数を書いておくと順番にテストを実行してくれます


```python:test_1.py
def func(x):
    return x + 2

def test_type():
    assert isinstance(func(3), int)

def test_answer1():
    assert func(3) == 5
    assert func(4) == 6

def test_answer2():
    assert func(3) == 4
```


成功すると「.」、エラーが出ると「F」で表示して、エラーが出た場合にエラー発生場所とエラーコードを表示してくれます


```python:terminal
> pytest test_1.py
=================================================== test session starts ===================================================
platform win32 -- Python 3.12.1, pytest-8.3.3, pluggy-1.5.0
rootdir: C:\Users\jjaka\マイドライブ\python\20241009_pytest\code
plugins: anyio-4.3.0
collected 3 items                                                                                                           

test_1.py ..F                                                                                                        [100%]

======================================================== FAILURES ========================================================= 
______________________________________________________ test_answer2 _______________________________________________________ 

    def test_answer2():
>       assert func(3) == 4
E       assert 5 == 4
E        +  where 5 = func(3)

test_1.py:15: AssertionError
================================================= short test summary info ================================================= 
FAILED test_1.py::test_answer2 - assert 5 == 4
=============================================== 1 failed, 2 passed in 0.10s =============================================== 
```


### 3-4. 複数ファイルを実行


複数のテストファイルを作っておけばまとめて実行してくれます


```python:terminal
> pytest
=================================================== test session starts ===================================================
platform win32 -- Python 3.12.1, pytest-8.3.3, pluggy-1.5.0
rootdir: C:\Users\jjaka\マイドライブ\python\20241009_pytest\code
plugins: anyio-4.3.0
collected 3 items                                                                                                           

test_1.py ..
test_2.py . 

==================================================== 3 passed in 0.02s ==================================================== 
```


## 4. テストの書き方


### 4-1. 関数で書く


先ほどのように関数を実行してエラーが出るかテストします


```python:test_1.py
def test_answer():
    assert func(3) == 5
    assert func(4) == 6
```


### 4-2. クラスで書く


一連のテストをクラスにまとめて書いて整理することができます
「Test」からはじまるクラスで、「test_」からはじまるメソッドが順番に実行されます


```python:test_2.py
class TestClassDemoInstance:
    value = 0

    def test_one(self):
        self.value = 1
        assert self.value == 1

    def test_two(self):
        self.value = 2
        assert self.value == 2
```


```python:terminal
> pytest test_3.py
=================================================== test session starts ===================================================
platform win32 -- Python 3.12.1, pytest-8.3.3, pluggy-1.5.0
rootdir: C:\Users\jjaka\マイドライブ\python\20241009_pytest\code
plugins: anyio-4.3.0
collected 2 items                                                                                                           

test_3.py ..

==================================================== 2 passed in 0.01s ==================================================== 
```


### 4-3. 例外処理をテストする


大抵のテストツールは例外を拾ってテストの失敗判定をするので、例外が狙い通り出たかテストするには別の仕組みが必要になります。pytestの場合はpytest.raises()を使うことで例外を受け取って狙い通りか確認することができます

以下の場合はExceptionGroupをraiseする関数f()を実行して、raiseされた内容が正しいかテストしています


```python:test_4.py
import pytest

def f():
    raise ExceptionGroup(
        "Group message",
        [
            RuntimeError(),
        ],
    )


def test_exception_in_group():
    with pytest.raises(ExceptionGroup) as excinfo:
        f()
    assert excinfo.group_contains(RuntimeError)
    assert not excinfo.group_contains(TypeError)
```


ExceptionGroupでなく普通の例外であっても同様にテストできます


```python:test_5.py
def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0
    assert excinfo.type is ZeroDivisionError
```
```python:test_6.py
def test_recursion_depth():
    with pytest.raises(RuntimeError) as excinfo:

        def f():
            f()

        f()
    assert "maximum recursion" in str(excinfo.value)
```


```python:test_7.py
def test_foo_not_implemented():
    def foo():
        raise NotImplementedError

    with pytest.raises(RuntimeError) as excinfo:
        foo()
    assert excinfo.type is RuntimeError
```


## 5. テスト用のディレクトリ


テスト関数の引数にtmp_pathを書くと、pytestがテスト用に一時的に準備したディレクトリを使う事ができます

以下のテストコードでなんというPATHのディレクトリが作成されるか確認できます
assert 0はエラーにして止める為に入れています


```python:test_8.py
def test_needsfiles(tmp_path):
    print(tmp_path)
    assert 0
```


'PYTEST_TMPDIR/test_needsfiles0'というディレクトリが作成されてtmp_pathに渡されているので、print行で書きだされて確認できます。


```python:terminal
F                                                                    [100%]
================================= FAILURES =================================
_____________________________ test_needsfiles ______________________________

tmp_path = PosixPath('PYTEST_TMPDIR/test_needsfiles0')

    def test_needsfiles(tmp_path):
        print(tmp_path)
>       assert 0
E       assert 0

test_tmp_path.py:3: AssertionError
--------------------------- Captured stdout call ---------------------------
PYTEST_TMPDIR/test_needsfiles0
========================= short test summary info ==========================
FAILED test_tmp_path.py::test_needsfiles - assert 0
1 failed in 0.12s
```


一時ファイルの書きだしなどで利用すると終了後に消してくれます


## まとめ


シンプルで使いやすくて見やすいので、すごく良いと思います
次はCI/CDで使ってみる予定です

以前doctestの使い方メモを作っていましたが、書き方と実行方法がかなり違っていて面白いと思います
https://qiita.com/studio_haneya/items/d44fc73781e88694cd3e

じつはunittestを使ったことが無いのでそちらも近々さわりたいと思ってます


## 参考


https://docs.pytest.org/en/stable/getting-started.html


https://qiita.com/okoppe8/items/eb7c3be5b9f6be244549