🦔

実行と準備を分けてテストを書きやすくする

2024/01/13に公開

以前チームでmockの必要なテストの書き方について軽くディスカッションしたときに、テストを書きやすくするために実装の段階で「実行と準備を分ける」と良いよねと思ったので、そのときのメモです。

背景: 他のモジュールに依存するクラスの実装

他のモジュールへの依存があるクラスを実装するときに、テストコードを含めてどう書くかで、以下の2つの方針がありました。

方針1: テスト対象のメソッド内で依存モジュールを直接呼び出す

この場合、pytest-mockなどを使って依存モジュールのmockを作成してテストを書きます。

from utils import multiply  # 2つの整数を引数にとり乗算を行う関数


class Rectangle:
    def __init__(self, a: int, b: int) -> None:
        self.a = a
        self.b = b

    @property
    def area(self) -> int:
        return multiply(self.a, self.b)


class TestRectangle:
    def test_area(self, mocker):
        mocker.patch("utils.multiply", return_value=10)  # pytest-mockを使う
        rectangle = Rectangle(2, 5)
        assert rectangle.area == 10

方針2: テスト対象のクラスのコンストラクタで依存モジュールをインスタンス変数にする

この場合、依存モジュールのインスタンス変数をmockに差し替えてテストを書きます。

from unittest.mock import Mock

from utils import multiply


class Rectangle:
    def __init__(self, a: int, b: int) -> None:
        self.a = a
        self.b = b
        self._multiply = multiply

    @property
    def area(self) -> int:
        return self._multiply(self.a, self.b)


class TestRectangle:
    def test_area(self):
        rectangle = Rectangle(2, 5)
        rectangle._multiply = Mock(return_value=10)
        assert rectangle.area == 10

チームでは方針2を採用

どちらの方針でも問題ないのですが、チームでは以下の理由で方針2を採用することにしました。

  • インスタンス変数をmockに差し替えるだけなのでシンプル
  • ファイルの移動などをしてもテストコードの修正が不要

気づき: 実行と準備を分けるとテストが書きやすい

方針1だとpytest-mockなどを使う必要があり、ややテクニカルなことをしないといけないのはなぜかと考えた結果、実行と準備が分かれていないから(テスト対象のメソッド内で依存モジュールを直接呼び出している)と思いました。
方針2だと、コンストラクタでインスタンス変数に依存を登録するので、テスト対象のメソッドの呼び出しと分離されています。
テストは基本的に「準備 → 実行 → 検証」の3ステップになるので、このように実装時点で実行と準備を分けておくとテストが書きやすいのかなと思いました。

GitHubで編集を提案

Discussion