5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ラクスパートナーズAdvent Calendar 2024

Day 2

モックとスタブの違いを正しく理解する

Last updated at Posted at 2024-12-01

はじめに

ラクスパートナーズの機械学習エンジニア 寺澤 歩希と申します。
機械学習と言いつつも、普段は主にデータ分析基盤の構築や、基盤データを用いたシステムの開発をしています。

この記事は、ラクスパートナーズ AdventCalendar 2024の2日目の記事となります。

最近、システムの単体テストを書くことが増えてきたので、少し理解が曖昧だったモックとスタブについて整理したいと思います。
具体例はPythonで書いてます。が、どの言語でも共通する概念のお話になります。

対象読者

  • 単体テストについて知っている人
  • モックとスタブの違いを曖昧に感じている人

本記事の目標

モックとスタブの違いを正しく理解し、正しく使えるようになること

モックとスタブって何?

モックとスタブの共通点

本物のオブジェクトの代わりとして使う「テストダブル」の一種であること

💡テストダブルとは
テスト対象が依存する外部コンポーネントやオブジェクトを模倣することで、テストを支援するために使用されるオブジェクトの総称。テストダブルを使用することで、テスト環境を制御しやすくなり、テストの信頼性と効率を向上させることができる。

モックとは

テスト対象がモックに対してどう振る舞うかを確認するために作られるオブジェクト。
動作を記録して、テストの中で「期待通りに動いたか」を検証できる。

スタブとは

テスト対象に都合のいいデータや動作を提供するために作られるオブジェクト。
予め定義した値や挙動を返すだけで、それ以上の目的はない。

💡わかりやすく言うと、
モック:「ちゃんとこの関数が呼ばれた?」を確認するためのオブジェクト
スタブ:「こんな入力が来たら、こう返してね」という決まった答えを返すオブジェクト
こちらの記事の図解がわかりやすいです。
https://qiita.com/hirohero/items/3ab63a1cdbe32bbeadf1

モックの例

  • シナリオ
    新規ユーザー登録時にメール送信機能が正しく動いているかをテストしたい。
    しかし、実際にメールを送信したくない。

  • 解決策
    モックを使って、メール送信メソッドが呼び出されたかを確認する

test.py
from unittest.mock import Mock

# テスト対象の関数
def register_user(email_service, user_name, email):
    # 登録処理
    # メールを送信
    email_service.send_email(email, f"Welcome, {user_name}!")


# モックを利用したテスト関数の実行
mock_email_service = Mock()
register_user(mock_email_service, "Alice", "alice@example.com")


# モックを使って呼び出しを検証
mock_email_service.send_email.assert_called_once("alice@example.com", "Welcome, Alice!")

スタブの例

  • シナリオ
    ユーザー情報を取得する関数をテストしたい。テスト環境で実際のデータベースに接続したくない。

  • 解決策
    スタブを使って、データベースを仮のオブジェクトに置き換える。

test.py
# スタブの用意
class DatabaseStub:
    def get_user(self, user_id: int):
        # 実際のデータベースではなく、固定のデータを返す
        if user_id == 1:
            return {"id": 1, "name": "Alice"}
        return None


# テスト対象の関数
def get_user_name(db, user_id: int):
    user = db.get_user(user_id)
    return user["name"] if user else "Unknown"


# スタブを利用したテスト
db_stub = DatabaseStub()
assert get_user_name(db_stub, 1) == "Alice"
assert get_user_name(db_stub, 2) == "Unknown"

モックの例では、テストの対象の内部実装の検証には興味がなく、外部オブジェクト(今回はメールサーバ)とのやり取りに焦点を当てており、
一方スタブの例では、外部オブジェクト(例ではデータベース)とのやり取りには興味がなく、テストの内部実装の検証に焦点を当てていることがわかると思います。

実際のところ...

多くのテストフレームワークでは、モックにスタブの役割を持たせることができるようになっています。

Python標準ライブラリであるunittest.mockのMockオブジェクトでも、return_valueside_effectで任意の値を返すように設定できます。

先ほどの例でもMockオブジェクトで下記のように書き直すことができます。

import unittest.mock import Mock

# テスト対象の関数
def get_user_name(db, user_id: int):
    user = db.get_user(user_id)
    return user["name"] if user else "Unknown"

# モックオブジェクトに返り値を設定し、スタブとして利用
mock = Mock()
mock.get_user.side_effect = lambda user_id: {"id": 1, "name": "Alice"} if user_id == 1 else None

# モックを利用したテスト
assert get_user_name(mock, 1) == "Alice"
assert get_user_name(mock, 2) == "Unknown"

モックもスタブも合わせた概念として『モック』と呼ぶのがデファクトスタンダードになっているのでしょうか。

アンチパターン

モックオブジェクトにスタブの役割を持たせられるのは一見便利なように思われます。
しかしモックとスタブの目的の違いを理解していないと、下記のような状況に陥る可能性があります。

from unittest.mock import Mock

# 外部APIと通信するクラス
class ApiClient:
    def get_data(self):
        # 外部APIからデータを取得する処理
        return {"key": "value"}

# テスト対象のオブジェクト
class DataProcessor:
    def __init__(self, api_client):
        self.api_client = api_client

    def process_data(self):
        # 外部APIからデータを取得して処理
        data = self.api_client.get_data()
        return data["key"].upper()

# テスト
def test_data_processor():
    # モックオブジェクトの作成
    mock_api_client = Mock()

    # モックがスタブとしても機能している例
    # 内部実装に依存して、get_dataの戻り値を設定
    mock_api_client.get_data.return_value = {"key": "value"}

    processor = DataProcessor(mock_api_client)

    # 実行して結果を確認
    result = processor.process_data()

    # 結果の検証 (内部実装に依存した検証)
    assert result == "VALUE"

    # モックを使った相互作用の検証 (外部との相互作用を検証)
    mock_api_client.get_data.assert_called_once()

この例では、モックオブジェクトがモックとしての役割もスタブとしての役割も担っています。
その結果、内部実装の検証と、外部作用の検証を一つのテストの中で行なっており、
このテストは結局何を検証したいのかが曖昧になっています。

...
..
.

test_data_processor_interaction():
    # モックとしてのモックオブジェクト
    mock_api_client = Mock()
    processor = DataProcessor(mock_api_client)

    # 関数の実行
    processor.process_data()

    # 外部との相互作用の検証
    mock_api_client.get_data.assert_called_once()


test_data_processor_logic():
    # スタブとしてのモックオブジェクト
    mock_api_client = Mock()
    mock_api_client.get_data.return_value = {"key": "example"}

    # 関数の実行
    processor = DataProcessor(mock_api_client)

    # 内部ロジックの検証
    result = processor.process_data()
    assert result == "EXAMPLE"

上記のように、テストの目的を明確化するためにモックとしての役割を持つモックオブジェクトなのか、スタブとしての役割を持つモックオブジェクトなのかをしっかり切り分けて書いておくと、可読性・保守性の高いテストになりそうです。

まとめ

モックとスタブは、どちらもテスト対象と依存関係にあるオブジェクトの偽物、という役割を持っていますが、
その目的は異なり、
モックは外部オブジェクトとの相互作用を検証するためのもの
スタブはテスト対象の内部実装の検証に焦点を当てるためのもの
という違いがあります。
多くのテストフレームワークではモックオブジェクトにスタブの役割を持たせることができますが、
無闇に濫用すれば良いのではなく、テストごとに持たせる役割を明確に定義し、1つのテストで1つの目的を検証するようにテストを設計するのが、良い設計といえます。

以上となります。

どの依存をモックとするか、という議論もあるのですが、それだけで記事一本書けそうなので、
また別の機会に書けたら書こうと思います。

この記事が、どなたかのテスト実装の助けになれば幸いです。

参考文献

P.S.

モックに『モック』という名前を付けているのがよくないと思いました。
(モック = mock = 模造品、紛い物)
役割的には、スタブにこそ『モック』という名前をつけるべきじゃない?と思います。
スタブは依存先の振る舞いを模倣するのが本質ですし、
モックも一応偽物ではありますが、その本質はテスト相互作用を検証するためのものですからね。

5
2
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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?