#3 Stub&Spy&Mockを解説

xUnit Test Patternsざっくり解説シリーズ

Eiji Ienaga
時を超えたプログラミングの道

--

はじめに

xUnit Test Patterns解説シリーズで今回は3回目です。今回は、この本のなかで引用されることが多い、Stub、Mockについて解説していきます。

アジャイルな開発を継続的に行っていく際は、インクリメントに作りつづけていくためにもユニットテストの自動化に力をいれているチームも多いでしょう。ユニットテストを行っていると、どうしてもStubやMockが必要になるシーンがいくつか出てきます。今日は、StubやMockが不要なシーンから順に解説します。

テスト対象が依存先がなく間接入力や間接出力がないならTest Doubleは不要

テスト対象であるクラスやモジュールで依存先がなく間接入力(indirect input)間接出力indirect outputもなければ、StubやMockといったTest Doubleは不要です。

def sum(a, b):
return a + b
def test_sum():
# When&Then
assert sum(1, 2) == 3

上記は、sumがテスト対象です。sumのinputは、a,bの引数、outputは足し算した結果がリターンで返ってきます。sumのテストは簡単ですね。ただsumようにテスト対象は常に依存先がゼロというわけにはいきません。

Stubが必要なケース:テスト対象のユニットやモジュールに依存先があり間接入力によって結果が変わる場合

テスト対象(SUT)には依存先が存在して間接入力によって結果が変わることをテストしたい場合があります。

# Before
def sum_plus_random(a, b):
# random.randint(1, 6)が間接入力
return sum(a, b) + random.randint(1, 6)
def test_sum_plus_random():
# When & Then
assert(sum_plus_random(1, 2) == ??)

上記のsum_plus_randomがテスト対象で、random.randintに依存しています。random.randintの結果が間接入力に相当し、間接入力によって結果がかわります。何も工夫しなければ、結果はランダムでassertが書きづらい状況になります。pythonのモックライブラリーを使えば、このコードのままでもテスト可能なのですが、今日はモックライブラリーなしで間接入力をテストコードから制御する例を示します。

# After
def create_sum_plus_random(randint = random.randint):
def sum_plus_random(a, b):
return sum(a, b) + randint(1, 6)
return sum_plus_random
def test_sum_plus_random():
# Given
def stub_randint_return_4(a, b):
return 4
sum_plus_random = create_sum_plus_random(stub_randint_return_4)

# When & Then
assert(sum_plus_random(1, 2) == 7)

上記はcreate_sum_plus_random関数の引数を使って、テストコードから間接入力を制御できるように細工しました。randintは常に4を返すStubになるので、テストの期待結果は1+2+4=7と値が確定します。テスト対象(SUT)のユニットやモジュールに依存先があり間接入力によってテスト結果が変わる場合にはStubが必要になってきます。

SpyかMockが必要なケース:テスト対象に依存先がありindirect out(間接出力)がある場合

テスト対象(SUT)が常に戻り値を返してくれるわけではありません。テスト対象の結果は間接出力しており、間接出力が期待通りかをテストしたい場合もあります。まずはSpyを使って間接出力が期待通りかをテストが必要になってくる例を示します。

# Before
def send_when_sum_is_3(a, b):
sum_message = sum(a, b)
if sum_message == 3:
send(sum_message) # 間接出力
def test_send_when_sum_is_3_when_message_is_three_then_send():
// When
send_when_sum_is_3(1, 2)
// Then
assert ??

上のコードのsend_when_sum_is_3には戻り値はなく、sendが依存先で間接出力しています。期待する振る舞いは足し算結果が3の場合はメッセージを送付し、そうでなければメッセージを送らないです。sendが呼ばれたか、引数が何だったかがテストできるように工夫が必要になってきます。これもモックライブラリを使わずに、間接出力のsendをテストコード側から制御できる例を示します。

# After
def
create_send_when_sum_is_3(send):
def send_when_sum_is_3(a, b):
sum_message = a + b
if sum_message == 3:
send(sum_message)
return send_when_sum_is_3
def test_send_when_sum_is_3_when_message_is_three_then_send():
# Given
send, send_called_times, send_message_contents = create_sender_spy()
send_when_sum_is_3 = create_send_when_sum_is_3(send)
# When
send_when_sum_is_3(1, 2)
# Then(1回sendが呼ばれ、引数は3であること)
assert send_called_times() == 1
assert send_message_contents() == [3]
def test_send_when_sum_is_3_when_message_is_not_three_then_send():
# Given
send, send_called_times, send_message_contents = create_sender_spy()
send_when_sum_is_3 = create_send_when_sum_is_3(send)
# When
send_when_sum_is_3(1, 1)
# Then(sendは呼ばれないこと)
assert send_called_times() == 0
def create_sender_spy():
messages = []

def send(message):
messages.append(message)
def send_called_times():
return len(messages)
def send_message_contents():
return messages
return send, send_called_times, send_message_contents

テストのポイントだけ絞ると合計が3の場合はsendを呼び、引数が3であること。

# When
send_when_sum_is_3(1, 2)
# Then(1回sendが呼ばれ、引数は3であること)
assert send_called_times() == 1
assert send_message_contents() == [3]

合計が3でない場合は sendを呼ばないこと。

# When
send_when_sum_is_3(1, 1)
# Then(sendは呼ばれないこと)
assert send_called_times() == 0

テストできるようにsendの呼び出しを記録し、テストコード側から記録にアクセスできるようにしているがSpyのポイントです。SpyではなくMockを使った間接出力をテストする例を示します。これもモックライブラリーを使わずに。

def create_send_when_sum_is_3(send):
def send_when_sum_is_3(a, b):
sum_message = a + b
if sum_message == 3:
send(sum_message)

return send_when_sum_is_3
def test_send_when_sum_is_3_when_message_is_three_then_send():
# Given
send, assert_send_called_times, assert_send_message_contents = create_sender_mock()
send_when_sum_is_3 = create_send_when_sum_is_3(send)
# When
send_when_sum_is_3(1, 2)
# Then(1回sendが呼ばれ、引数は3であること)
assert_send_called_times(1)
assert_send_message_contents([3])
def test_send_when_sum_is_3_when_message_is_not_three_then_send():
# Given
send, assert_send_called_times, assert_send_message_contents = create_sender_mock()
send_when_sum_is_3 = create_send_when_sum_is_3(send)
# When
send_when_sum_is_3(1, 1)
# Then(sendは呼ばれないこと)
assert_send_called_times(0)
def create_sender_mock():
messages = []
def send(message):
messages.append(message)
def assert_send_called_times(expected):
assert len(messages) == expected
def assert_send_message_contents(expected):
assert messages == expected
return send, assert_send_called_times, assert_send_message_contents

テストしたいポイントだけ切り取ると

# When
send_when_sum_is_3(1, 2)
# Then(1回sendが呼ばれ、引数は3であること)
assert_send_called_times(1)
assert_send_message_contents([3])
# When
send_when_sum_is_3(1, 1)
# Then(sendは呼ばれないこと)
assert_send_called_times(0)

MockもSpyと同様にテスト対象からの間接出力が期待通りかを検証しますが、検証自体をMock側に任せてしまうのが特徴です。

依存先には注意

ただし、テスト対象の依存関係は本当に妥当なものかに注意を払いながら、StubやMockを使っていきたいところです。テスト対象の依存先が多すぎや低レベルの技術要素に直接結合してしまうと、理解がむずかしくなったり、依存先のちょっと変更しただけで、テストが敏感に壊れてしまい、メンテナンスコスト高を引き起こすなどの問題をかかえてしまいます。StubやMockは、現存のいけてないコードをむりやりテストするために使うのではなく、設計のフィードバック改善を得るための道具として使っていきたいところです。

まとめ

今日はモックのライブラリーを活用せずにStub、Spy、Mockについて解説しました。テスト対象に依存先がなければ、Stub、Spy、Mockは不要です。もし、テスト対象に依存先があり、依存先からの間接入力を考慮してテストしたい場合は、Stubを活用します。もし、テスト対象に依存先があり、依存先への間接出力をテストしたい場合は、Spy、もしくはMockを使ってテストします。ただし、テストを記述しながら依存関係が妥当なのかは問いかけながら進めていきたいところです。

--

--