2
1
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

【Python】テスト・モックのやり方とその仕組み

Last updated at Posted at 2024-06-26

概要

Pythonでテストといえば、unittestやpytestが有名ですが、unittestやpytestを使わないやり方、unittestとpytestの違いなどを解説していきたいと思います。まず、テストするにあたり、モックのための前提知識として、再宣言について解説し、次に実際にテストの解説をしていきます。

この記事を読むと以下のことが理解できるようになります。

初心者の方やPythonのテストの仕組みについて知りたい方におすすめです。

再宣言

PythonではGoやJavaなどの言語とは異なり、再宣言が可能です。

a = 1
a = 1 # エラーにならない

他の言語では以下のようにコンパイルエラーになります。(以下はGoの例)

goの場合
func main() {
    a := 10
    a := "Hello" // コンパイルエラー
    fmt.Println(a)
}

また、関数も同様に再宣言できます。

def hoge():
    return 1

def hoge2():
    return 99
hoge = hoge2
print(hoge()) # 99

再宣言ができるということは、実装を置き換えたり、テストでモックすることが可能になります。

テストのディレクトリ

まず、今回は以下のディレクトリ構想でテストを行います。app配下にソースコード、tests配下にテストを書いていきます。

.
├── app 
│   ├── sample1.py
│   └── __init__.py
└── tests  // テストディレクトリ
    ├── __init__.py
    └── test_sample1.py

ライブラリを使わずにテストする

以下のコードのfoo()をテストするとします。

app/sample1.py
def foo():
    return hoge() + 1

def hoge():
    return 1

foo() # 2

foo()をテストするにはhoge()をモックする必要があります。先ほどの再宣言によってhoge()をモックしていきます。

tests/test_sample1.py
from app import sample1

def mock_hoge():
    return 99

def test_foo():
    sample1.hoge = mock_hoge
    assert sample1.foo() == 100 # 成功

次にクラスの場合をapp/sample2.pyに実装していきます。

app/sample2.py
class Example:
    def foo(self):
        return self.hoge() + 1

    def hoge(self):
        return 1

print(Example().foo())  # 2

クラスの場合も関数とほぼ変わりません。(ちなみに、import が from app import sample2になるとExample().foo()sample2.Example().foo()にする必要があります。

tests/test_sample2.py
from app.sample2 import Example

def test_foo():
    Example.hoge = mock_hoge
    assert Example().foo() == 100  # 成功

def mock_hoge(self):
    return 99

モックの問題点

上記のライブラリを使わない方法だと、mock対象の関数などが増えた際に、一つずつ自分で書くと、大変です。
また、モックする前に前もってコピーしておかないと、元の実装戻すことができないという問題が出てきます。
unittestやpytestなどのライブラリを使用することでこれらの問題が解決します。

unittest

標準ライブラリなので、インストールの必要がありません。

test_sample2.py
import unittest

from app.sample2 import Example


class TestSample2(unittest.TestCase):
    def test_func(self):
        expected = 2
        actual = Example().foo()
        self.assertEqual(expected, actual)


if __name__ == "__main__":
    unittest.main()

MagicMock

unittest.mockのMagicMockを使ってモックしていきます。

tests/sample2.py
import unittest
from unittest.mock import MagicMock

from app.sample2 import Example


class TestSample2(unittest.TestCase):
    def test_func(self):
        Example.hoge = MagicMock(return_value=99)
        expected = 100
        actual = Example().foo()
        self.assertEqual(expected, actual)
        Example.hoge.assert_called_once()


if __name__ == "__main__":
    unittest.main()

さて、ここでassert_called_once_with()という指定した関数で一度だけ呼ばれたことを検証する関数を使っています。MagicMockを使用すると使用することができます

例えば、hoge()をモックしたmock_hoge()の実装が以下のようになっていたら

from app.sample2 import Example


class MockSample2:
    def __init__(self):
        self.count = 0

    def mock_hoge(self):
        self.count += 1
        return 99
    def assert_called_once_with(self):
        if self.count:
            return True
        return False

def test_sample2():
    mock = MockSample2()
    Example.hoge = mock.mock_hoge
    assert 100 == Example().foo()
    assert mock.assert_called_once_with()

上記のようMockSample2を実装すると、MagicMockassert_called_once_with()と同じ動作をするassert_called_once_with()を作成することができます。MagicMockには、assert_called_once_with()の他にも色々な関数が実装されております。

メソッド名 説明
assert_called_with() 最後に実行したモックの引数を検証
assert_called_once_with() 指定した引数で一度だけ呼ばれたか検証
assert_any_call() 指定した引数で一度でも呼ばれたか検証
assert_has_calls() 順番通りに呼ばれたか検証
side_effect() モックの返り値を設定
side_effect() モックの返り値を設定(関数や例外、ランダムな値を返すとき)

patcherでモックを戻す

withを使ったやり方

withだとスコープを自由に変えることができます。

tests/sample2.py
class TestSample2(unittest.TestCase):
    def test_func(self):
        with patch("app.sample2.Example.hoge") as mock_hoge:
            mock_hoge.return_value = 99
            self.assertEqual(100, Example().foo())

        self.assertEqual(2, Example().foo())


if __name__ == "__main__":
    unittest.main()

デコレータを使ったやり方

デコレータで書くこともできます。
mockのスコープが1つの関数となります。

tests/sample2.py
class TestSample2(unittest.TestCase):
    @patch("app.sample2.Example.hoge")
    def test_sample2(self, mock_hoge):
        mock_hoge.return_value = 99
        self.assertEqual(100, Example().foo())

    def test_fuck(self):
        self.assertEqual(2, Example().foo())


if __name__ == "__main__":
    unittest.main()

pytest

pytestです。unittestよりPythonらしい書き方で、よりシンプルに書けます。ただし、インストールが必要となります。

tests/sample2.py
from app.sample2 import Example


def test_func_main(mocker):
    assert 2 == Example().foo()

モック

下記のpytest-mock以外にunittestのmockと同じようにMagicMockが使える。

pytest-mock

pytest-mockは unittest のmockをフォークしたもの。使用するにはインストールが必要。
フィクスチャを使えて、unittestのmockよりシンプルにかけます。

tests/sample2.py
from app.sample2 import Example


def test_func_main(mocker):
    mocker.patch("app.sample2.Example.hoge", return_value=99)
    assert 100 == Example().foo()

カバレッジや並列実行

pytestでは、カバレッジを出すpytest-covやテストの並列実行を可能にするpython-xdistといったライブラリがあるので特におすすめです!

pytest-covでカバレッジを出力しているときは、デバッグができないので注意

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1