単体テストを書く際、テスト対象のコードが外部のデータベース、APIサーバー、ファイルシステムなどに依存していると、以下のような問題が発生する:
これらの問題を解決するために使われるのがテストダブル(Test Double)である。
テストダブルとは、テストにおいて依存先のコンポーネントを本物の代わりに置き換える「代役」のことである。映画のスタントダブルのように、本物の代わりにテスト専用の実装を使用する。
本記事では、テストダブルの5つの種類(Dummy、Stub、Fake、Spy、Mock)について、それぞれの目的と使い分けを、Goのコード例を交えて解説する。
テストダブルには5つの種類がある。それぞれ目的と使い方が異なる。
| 種類 | 目的 | 特徴 |
|---|---|---|
| Dummy | 引数を埋めるだけ | 実際には使用されない |
| Stub | 決まった値を返す | 状態検証に使用 |
| Fake | 簡易的な実装 | 実際に動作する軽量版 |
| Spy | 呼び出しを記録 | 履歴を後で検証 |
| Mock | 期待を事前設定 | 振る舞い検証に使用 |
以下の例では、データストアに依存するサービスをテストする。
package main
import "errors"
// Store はデータストレージのインターフェース
type Store interface {
Get(key string) (string, error)
Put(key string, value string) error
}
// UserService はStoreに依存するサービス
type UserService struct {
store Store
}
func NewUserService(s Store) *UserService {
return &UserService{store: s}
}
// FetchValue は内部でstore.Get()を呼ぶ
func (svc *UserService) FetchValue(key string) (string, error) {
v, err := svc.store.Get(key)
if err != nil {
return "", err
}
if v == "" {
return "", errors.New("value not found")
}
return v, nil
}
// SaveValue は内部でstore.Put()を呼ぶ
func (svc *UserService) SaveValue(key, value string) error {
if value == "" {
return errors.New("value cannot be empty")
}
return svc.store.Put(key, value)
}
それぞれのテストダブルについて、具体的なコード例と使いどころを見ていく。
Dummyは、引数を埋めるためだけに存在し、実際には使用されないオブジェクトである。
package main
import "testing"
// Dummy実装
type DummyStore struct {
t *testing.T
}
func NewDummyStore(t *testing.T) *DummyStore {
return &DummyStore{t: t}
}
func (d *DummyStore) Get(key string) (string, error) {
d.t.Fatal("Get should not be called")
return "", nil
}
func (d *DummyStore) Put(key, value string) error {
d.t.Fatal("Put should not be called")
return nil
}
// Logger インターフェース
type Logger interface {
Info(msg string)
}
// SimpleLogger は簡易的なLogger実装
type SimpleLogger struct{}
func (l *SimpleLogger) Info(msg string) {
// 実際にはログ出力するが、ここでは何もしない
}
// ProcessData は複数の依存を持つ関数(storeは使わない)
func ProcessData(store Store, logger Logger) error {
// この関数ではloggerだけを使い、storeは使わない
logger.Info("processing started")
return nil
}
func TestProcessData(t *testing.T) {
// storeは使われないので、Dummyで十分
dummy := NewDummyStore(t)
logger := &SimpleLogger{}
err := ProcessData(dummy, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// もしstoreのメソッドが呼ばれたら、t.Fatal()でテストが失敗する
}
Stubは、呼び出しに対して決まった値を返すだけの単純な実装である。状態検証に使われる。
package main
import (
"errors"
"testing"
)
// Stub実装
type StubStore struct {
value string
err error
}
func (s *StubStore) Get(key string) (string, error) {
return s.value, s.err
}
func (s *StubStore) Put(key, value string) error {
return nil
}
// 正常系のテスト
func TestFetchValue_Success(t *testing.T) {
stub := &StubStore{value: "hello"}
svc := NewUserService(stub)
got, err := svc.FetchValue("foo")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "hello" {
t.Errorf("got %q, want %q", got, "hello")
}
}
// エラー系のテスト
func TestFetchValue_Error(t *testing.T) {
stub := &StubStore{err: errors.New("connection failed")}
svc := NewUserService(stub)
_, err := svc.FetchValue("foo")
if err == nil {
t.Error("expected error, got nil")
}
}
// 空文字列のテスト
func TestFetchValue_EmptyValue(t *testing.T) {
stub := &StubStore{value: ""}
svc := NewUserService(stub)
_, err := svc.FetchValue("foo")
if err == nil {
t.Error("expected error for empty value")
}
}
Fakeは、実際に簡易的な動作をする軽量実装である。本物に近い振る舞いをするが、テスト用に簡略化されている。
package main
import (
"errors"
"testing"
)
// Fake実装:メモリ内でデータを管理
type FakeStore struct {
data map[string]string
}
func NewFakeStore() *FakeStore {
return &FakeStore{data: make(map[string]string)}
}
func (f *FakeStore) Get(key string) (string, error) {
value, exists := f.data[key]
if !exists {
return "", errors.New("key not found")
}
return value, nil
}
func (f *FakeStore) Put(key, value string) error {
f.data[key] = value
return nil
}
// Fakeを使ったテスト
func TestFetchValue_Fake(t *testing.T) {
fake := NewFakeStore()
fake.Put("foo", "bar")
fake.Put("hello", "world")
svc := NewUserService(fake)
// 存在するキーの取得
got, err := svc.FetchValue("foo")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "bar" {
t.Errorf("got %q, want %q", got, "bar")
}
// 別のキーの取得
got2, err := svc.FetchValue("hello")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got2 != "world" {
t.Errorf("got %q, want %q", got2, "world")
}
}
Spyは、呼び出し履歴(引数、回数など)を記録し、後で検証することを目的とする。Mockとの違いは、Spyは事前に期待値を設定せず、実行後に履歴を確認する点である。
package main
import "testing"
// Spy実装
type SpyStore struct {
GetCalls []string // Getが呼ばれたキーのリスト
PutCalls []struct {
Key string
Value string
}
value string
}
func (s *SpyStore) Get(key string) (string, error) {
s.GetCalls = append(s.GetCalls, key)
return s.value, nil
}
func (s *SpyStore) Put(key, value string) error {
s.PutCalls = append(s.PutCalls, struct {
Key string
Value string
}{key, value})
return nil
}
// Spyを使ったテスト
func TestFetchValue_Spy(t *testing.T) {
spy := &SpyStore{value: "hello"}
svc := NewUserService(spy)
_, _ = svc.FetchValue("foo")
// 呼び出し履歴を検証
if len(spy.GetCalls) != 1 {
t.Errorf("expected 1 call, got %d", len(spy.GetCalls))
}
if spy.GetCalls[0] != "foo" {
t.Errorf("expected Get('foo'), got Get('%s')", spy.GetCalls[0])
}
}
// 複数回呼び出しのテスト
func TestFetchMultipleValues_Spy(t *testing.T) {
spy := &SpyStore{value: "test"}
svc := NewUserService(spy)
svc.FetchValue("key1")
svc.FetchValue("key2")
svc.FetchValue("key3")
// 呼び出し順序と引数を検証
expected := []string{"key1", "key2", "key3"}
if len(spy.GetCalls) != len(expected) {
t.Fatalf("expected %d calls, got %d", len(expected), len(spy.GetCalls))
}
for i, want := range expected {
if spy.GetCalls[i] != want {
t.Errorf("call %d: expected %q, got %q", i, want, spy.GetCalls[i])
}
}
}
Mockは、事前に期待(expectation)を設定し、テスト終了後にその期待が満たされたかを検証する。振る舞い検証に特化している。Spyとの違いは、Mockはテスト実行前に「こう呼ばれるべき」という期待を明示する点である。
package main
import (
"errors"
"testing"
)
// Mock実装
type MockStore struct {
expectations []struct {
key string
value string
err error
}
callIndex int
t *testing.T
}
func NewMockStore(t *testing.T) *MockStore {
return &MockStore{t: t}
}
// 期待値を設定(チェーン可能)
func (m *MockStore) ExpectGet(key string) *MockStore {
m.expectations = append(m.expectations, struct {
key string
value string
err error
}{key: key})
return m
}
func (m *MockStore) WillReturn(value string, err error) *MockStore {
if len(m.expectations) > 0 {
idx := len(m.expectations) - 1
m.expectations[idx].value = value
m.expectations[idx].err = err
}
return m
}
func (m *MockStore) Get(key string) (string, error) {
if m.callIndex >= len(m.expectations) {
m.t.Errorf("unexpected call to Get(%q)", key)
return "", errors.New("unexpected call")
}
expected := m.expectations[m.callIndex]
if key != expected.key {
m.t.Errorf("call %d: expected Get(%q), got Get(%q)",
m.callIndex, expected.key, key)
}
m.callIndex++
return expected.value, expected.err
}
func (m *MockStore) Put(key, value string) error {
return nil
}
// 期待が満たされたか検証
func (m *MockStore) Verify() {
if m.callIndex != len(m.expectations) {
m.t.Errorf("expected %d calls, got %d", len(m.expectations), m.callIndex)
}
}
// Mockを使ったテスト
func TestFetchValue_Mock(t *testing.T) {
mock := NewMockStore(t)
mock.ExpectGet("foo").WillReturn("bar", nil)
svc := NewUserService(mock)
result, err := svc.FetchValue("foo")
// 期待通りに呼ばれたか検証
mock.Verify()
// 結果も検証
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result != "bar" {
t.Errorf("got %q, want %q", result, "bar")
}
}
// 複数回の呼び出しをテスト
func TestFetchMultipleValues_Mock(t *testing.T) {
mock := NewMockStore(t)
mock.ExpectGet("key1").WillReturn("value1", nil)
mock.ExpectGet("key2").WillReturn("value2", nil)
svc := NewUserService(mock)
result1, _ := svc.FetchValue("key1")
result2, _ := svc.FetchValue("key2")
mock.Verify()
if result1 != "value1" {
t.Errorf("got %q, want %q", result1, "value1")
}
if result2 != "value2" {
t.Errorf("got %q, want %q", result2, "value2")
}
}
テストダブルは、単体テストを高速で安定させ、テストしづらいコードをテスト可能にする強力なツールである。
適切なテストダブルを選択することで、保守性が高く、リファクタリング耐性のあるテストを書くことができる。