📚 背景知識(読んでから問題へ)
Day 023-025ではtesting.T/testing.Bを使ったテスト技法(テーブル駆動テスト・サブテスト・ベンチマーク)を学びました。しかしテストを書こうとすると、すぐに壁にぶつかります。「外部SMTPサーバーにメールを送る処理」「DBに書き込む処理」「外部APIを呼ぶ処理」をテストするたびに、本物のSMTPサーバーやDBが必要になってしまう、という問題です。
この壁を壊すのがDI(Dependency Injection、依存性注入)です。Goには他言語のような専用のDIフレームワーク(JavaのSpring、C#のASP.NET Core組み込みDIコンテナ等)は標準に存在しません。その代わり、Goはインターフェース+コンストラクタ関数という言語仕様だけでDIを実現します。
考え方はシンプルです。
- 「外部に依存する処理」をインターフェースとして抽象化する
- 業務ロジックを持つ構造体は、具体的な実装(SMTPクライアント等)ではなくインターフェース型のフィールドを持つ
- 実際に使う実装(本物のSMTPクライアント)は、構造体の外(
main関数やコンストラクタの呼び出し側)で組み立てて渡す
type EmailSender interface {
Send(to, subject, body string) error
}
type OrderService struct {
sender EmailSender // 具体的な実装ではなく、インターフェース型で持つ
}
func NewOrderService(sender EmailSender) *OrderService {
return &OrderService{sender: sender}
}
こうしておくと、本番ではSMTPEmailSender(本物のSMTP通信をする実装)を渡し、テストではMockEmailSender(メモリ上に記録するだけの偽実装)を渡すことができます。OrderService自身は「EmailSenderインターフェースを満たす何か」としか認識しないため、どちらを渡されても同じコードで動きます。
Day 020で学んだ「Accept interfaces, return structs」の原則が、まさにここで実務的な価値を発揮します。関数・構造体は受け取る依存をインターフェースで表現し、自分自身は具体的な構造体として返すという非対称な設計が、テスト容易性の土台になっているのです。
Goのモックには大きく2つの作り方があります。
- 手書きモック: インターフェースを満たす小さな構造体を自分で書く。今日はこちらを扱います
- モック生成ツール:
gomockやmockery等のツールがインターフェース定義から自動でモックコードを生成する(Phase 3以降、実務規模のコードベースで使う機会が増える)
小規模なインターフェース(メソッド1〜2個)であれば、手書きモックの方がツール導入のコストより軽く、Goコミュニティでも広く推奨されています。「小さいインターフェースを設計すれば、モックも小さく書ける」という点は、Day 020のインターフェース設計原則と直結しています。
📝 問題
ECサイトの注文確定処理を行うOrderServiceを、DIを使ってテスト可能な設計にリファクタリングしてください。
Part A: インターフェース抽出とコンストラクタ注入
以下の密結合な実装があります。
package main
import (
"fmt"
"net/smtp"
)
type Order struct {
ID string
CustomerEmail string
TotalYen int
}
// 密結合な実装:OrderServiceが具体的なSMTP通信を直接知っている
type OrderService struct {
smtpHost string
smtpPort int
}
func (s *OrderService) ConfirmOrder(order Order) error {
if order.TotalYen <= 0 {
return fmt.Errorf("invalid order total: %d", order.TotalYen)
}
body := fmt.Sprintf("ご注文 #%s(合計%d円)を承りました。", order.ID, order.TotalYen)
addr := fmt.Sprintf("%s:%d", s.smtpHost, s.smtpPort)
msg := []byte("Subject: ご注文確認\r\n\r\n" + body)
// smtp.SendMail は本物のネットワーク通信を行うため、単体テストで直接呼べない
err := smtp.SendMail(addr, nil, "shop@example.com", []string{order.CustomerEmail}, msg)
if err != nil {
return fmt.Errorf("send confirmation email: %w", err)
}
return nil
}
次の設計変更を行ってください。
Send(to, subject, body string) errorという1メソッドのEmailSenderインターフェースを定義するOrderServiceがEmailSenderインターフェース型のフィールドを持つように変更し、NewOrderService(sender EmailSender) *OrderServiceというコンストラクタ関数を用意する(smtpHost/smtpPortのような具体的な接続情報は、本物の実装側の責務に移す)- 本物の実装として
SMTPEmailSender構造体を用意し、EmailSenderを満たすようにする(中でsmtp.SendMail相当の処理を呼ぶ想定でよいが、テストでは呼ばれない設計にする) ConfirmOrderメソッドは、注文合計が0円以下ならメールを送らずエラーを返す仕様を維持する
Part B: 手書きモックによるテーブル駆動テスト
EmailSenderを満たすMockEmailSenderを手書きし、次の3ケースを検証するテーブル駆動テストを書いてください。
- 正常な注文(
TotalYen > 0)→ メールが1件送信され、宛先・本文に注文情報が正しく含まれる - 不正な注文(
TotalYen <= 0)→ メールは送信されず、エラーが返る MockEmailSenderが意図的にエラーを返す設定のとき →ConfirmOrderはエラーを返し、そのエラーに元のエラー(errors.Is等で検証可能な形)が含まれる
MockEmailSenderは送信されたメール(宛先・件名・本文)を[]SentEmailのようなスライスに記録し、テストから検証できるようにしてください。
Part C: 設計判断の言語化
Part Aの元コード(密結合版)をgo testで直接テストしようとすると何が起きるか、また今回のDI導入によってテストがどう変わったかを、次の観点で説明してください。
- 密結合版のままテストを書いた場合に発生する具体的な問題(実行結果・副作用の観点で)
OrderServiceが「本物のSMTP実装の詳細(ホスト名・ポート番号)」を知らなくなったことによる、テスト以外のメリット(将来SMTPをSendGridのようなAPI経由の送信に差し替える場面を想定して)
go run . / go test ./...でそのまま動作する完全なコードとして提示すること。
🔍 ヒント(段階的開示)
ヒント1 — 方向性
「OrderServiceが実際に何をしたいか」(メールを送るという意図)と「どうやって送るか」(SMTP接続の詳細という手段)を分離するのが出発点です。OrderServiceの中にnet/smtpという具体的なパッケージ名や接続情報が登場している時点で、それは「意図」と「手段」が混ざっているサインです。インターフェースは「何をしたいか」だけを表現し、「どうやって」は実装側(SMTPEmailSender)に押し込みます。
ヒント2 — アプローチ
- コンストラクタ関数
NewOrderService(sender EmailSender) *OrderServiceは、Goで「必須の依存を渡し忘れさせない」ための慣習的なパターンです。フィールドを直接初期化する(OrderService{}のゼロ値のまま使う)とsenderがnilのまま呼ばれてパニックする危険があるため、コンストラクタ経由での初期化を促します MockEmailSenderは次のような形が定石です。type SentEmail struct { To, Subject, Body string } type MockEmailSender struct { Sent []SentEmail Err error } func (m *MockEmailSender) Send(to, subject, body string) error { if m.Err != nil { return m.Err } m.Sent = append(m.Sent, SentEmail{to, subject, body}) return nil }- Part Bのケース3で「元のエラーが含まれる」ことを検証するには、
ConfirmOrder側でfmt.Errorf("...: %w", err)のように%wでラップしておき、テスト側でerrors.Is(gotErr, mockErr)を使います
ヒント3 — コード骨格
type EmailSender interface {
Send(to, subject, body string) error
}
type OrderService struct {
sender EmailSender
}
func NewOrderService(sender EmailSender) *OrderService {
return &OrderService{sender: sender}
}
func (s *OrderService) ConfirmOrder(order Order) error {
if order.TotalYen <= 0 {
return fmt.Errorf("invalid order total: %d", order.TotalYen)
}
body := fmt.Sprintf("ご注文 #%s(合計%d円)を承りました。", order.ID, order.TotalYen)
if err := s.sender.Send(order.CustomerEmail, "ご注文確認", body); err != nil {
return fmt.Errorf("send confirmation email: %w", err)
}
return nil
}
// テストではこう組み立てる
mock := &MockEmailSender{}
svc := NewOrderService(mock)
err := svc.ConfirmOrder(Order{ID: "A1", CustomerEmail: "a@example.com", TotalYen: 1000})
// mock.Sent を検証する
✅ 模範解答
ファイル: main.go
package main
import (
"fmt"
)
// Order は注文情報を表す。
type Order struct {
ID string
CustomerEmail string
TotalYen int
}
// EmailSender は「メールを送る」という意図だけを表現するインターフェース。
// OrderService はこのインターフェースにしか依存しない。
type EmailSender interface {
Send(to, subject, body string) error
}
// SMTPEmailSender は EmailSender の本番実装。
// SMTP接続の詳細(ホスト名・ポート番号)はこの構造体だけが知っている。
type SMTPEmailSender struct {
Host string
Port int
From string
}
// Send は本来 net/smtp.SendMail 等で実際のSMTP通信を行う。
// (本ファイルではネットワークI/Oの実体は割愛し、シグネチャと責務の分離を示す)
func (s *SMTPEmailSender) Send(to, subject, body string) error {
addr := fmt.Sprintf("%s:%d", s.Host, s.Port)
// 実際の送信処理はここに書く想定(例: smtp.SendMail(addr, nil, s.From, []string{to}, msg))。
_ = addr
fmt.Printf("[SMTP %s] to=%s subject=%s\n", addr, to, subject)
return nil
}
// OrderService は EmailSender インターフェース型のフィールドだけを持ち、
// 具体的な送信手段(SMTP/SendGrid API等)を知らない。
type OrderService struct {
sender EmailSender
}
// NewOrderService は依存(EmailSender)を外部から受け取るコンストラクタ。
// フィールドを直接初期化させず、必須の依存を渡し忘れさせない役割を持つ。
func NewOrderService(sender EmailSender) *OrderService {
return &OrderService{sender: sender}
}
// ConfirmOrder は注文を確定し、確認メールを送る。
func (s *OrderService) ConfirmOrder(order Order) error {
if order.TotalYen <= 0 {
return fmt.Errorf("invalid order total: %d", order.TotalYen)
}
body := fmt.Sprintf("ご注文 #%s(合計%d円)を承りました。", order.ID, order.TotalYen)
if err := s.sender.Send(order.CustomerEmail, "ご注文確認", body); err != nil {
return fmt.Errorf("send confirmation email: %w", err)
}
return nil
}
func main() {
sender := &SMTPEmailSender{Host: "smtp.example.com", Port: 587, From: "shop@example.com"}
svc := NewOrderService(sender)
order := Order{ID: "A1001", CustomerEmail: "customer@example.com", TotalYen: 3500}
if err := svc.ConfirmOrder(order); err != nil {
fmt.Println("注文確定エラー:", err)
return
}
fmt.Println("注文確定完了:", order.ID)
}
ファイル: main_test.go
package main
import (
"errors"
"testing"
)
// SentEmail はモックが記録する送信済みメールの内容。
type SentEmail struct {
To, Subject, Body string
}
// MockEmailSender は EmailSender を満たす手書きモック。
// 本物のネットワーク通信を一切行わず、呼び出しをメモリ上に記録するだけ。
type MockEmailSender struct {
Sent []SentEmail
Err error
}
func (m *MockEmailSender) Send(to, subject, body string) error {
if m.Err != nil {
return m.Err
}
m.Sent = append(m.Sent, SentEmail{To: to, Subject: subject, Body: body})
return nil
}
func TestOrderService_ConfirmOrder(t *testing.T) {
tests := []struct {
name string
order Order
senderErr error
wantErr bool
wantSentLen int
}{
{
name: "正常な注文はメールが1件送信される",
order: Order{ID: "A1", CustomerEmail: "a@example.com", TotalYen: 1000},
wantErr: false,
wantSentLen: 1,
},
{
name: "合計0円以下はメールを送らずエラーを返す",
order: Order{ID: "A2", CustomerEmail: "b@example.com", TotalYen: 0},
wantErr: true,
wantSentLen: 0,
},
{
name: "送信エラー時はConfirmOrderもエラーを返す",
order: Order{ID: "A3", CustomerEmail: "c@example.com", TotalYen: 500},
senderErr: errors.New("smtp connection refused"),
wantErr: true,
wantSentLen: 0,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
mock := &MockEmailSender{Err: tt.senderErr}
svc := NewOrderService(mock)
err := svc.ConfirmOrder(tt.order)
if (err != nil) != tt.wantErr {
t.Fatalf("ConfirmOrder() error = %v, wantErr = %v", err, tt.wantErr)
}
if len(mock.Sent) != tt.wantSentLen {
t.Fatalf("sent emails = %d, want %d", len(mock.Sent), tt.wantSentLen)
}
if tt.senderErr != nil {
if !errors.Is(err, tt.senderErr) {
t.Fatalf("error chain does not contain sender error: got %v, want wrapping %v", err, tt.senderErr)
}
}
if !tt.wantErr && len(mock.Sent) == 1 {
got := mock.Sent[0]
if got.To != tt.order.CustomerEmail {
t.Errorf("sent To = %q, want %q", got.To, tt.order.CustomerEmail)
}
if got.Subject != "ご注文確認" {
t.Errorf("sent Subject = %q, want %q", got.Subject, "ご注文確認")
}
}
})
}
}
▶ 実行結果を見る(go run . / go test -v ./... で検証済み)
$ go run .
[SMTP smtp.example.com:587] to=customer@example.com subject=ご注文確認
注文確定完了: A1001
$ go test -v ./...
=== RUN TestOrderService_ConfirmOrder
=== RUN TestOrderService_ConfirmOrder/正常な注文はメールが1件送信される
=== RUN TestOrderService_ConfirmOrder/合計0円以下はメールを送らずエラーを返す
=== RUN TestOrderService_ConfirmOrder/送信エラー時はConfirmOrderもエラーを返す
--- PASS: TestOrderService_ConfirmOrder (0.00s)
--- PASS: TestOrderService_ConfirmOrder/正常な注文はメールが1件送信される (0.00s)
--- PASS: TestOrderService_ConfirmOrder/合計0円以下はメールを送らずエラーを返す (0.00s)
--- PASS: TestOrderService_ConfirmOrder/送信エラー時はConfirmOrderもエラーを返す (0.00s)
PASS
ok example.com/day026 0.003s
go.mod・main.go・main_test.goを1モジュール(module example.com/day026)として配置し、go run . / go test -v ./...で検証済みです。テストは一切のネットワーク通信を行わずミリ秒単位で完走します。
🪜 Step-by-Step 解説
type EmailSender interface {
Send(to, subject, body string) error
}
密結合版のコードから「OrderServiceが本当にやりたいこと」(宛先・件名・本文を渡してメールを送る)だけを抜き出し、メソッド1個のインターフェースにします。smtpHostやsmtpPortのような「どうやって送るか」の詳細情報は、このインターフェースには一切登場させません。
type SMTPEmailSender struct {
Host string
Port int
From string
}
func (s *SMTPEmailSender) Send(to, subject, body string) error { /* 本物の通信 */ }
接続情報(Host/Port)は、それを実際に使うSMTPEmailSenderだけが保持します。OrderService側からはこれらのフィールドが一切見えなくなり、「OrderServiceはSMTPを使っている」という知識自体がOrderServiceの外に押し出されます。
func NewOrderService(sender EmailSender) *OrderService {
return &OrderService{sender: sender}
}
OrderService{}のようにゼロ値で組み立てられる余地を残すと、senderがnilのままConfirmOrderが呼ばれてパニックする危険があります。コンストラクタ関数を用意し「EmailSenderを渡さないとOrderServiceを作れない」という制約をコード上で表現します。
mock := &MockEmailSender{}
svc := NewOrderService(mock)
本番ではSMTPEmailSender、テストではMockEmailSender——どちらもEmailSenderを満たすので、OrderService側のコードは一切変更せずに差し替えられます。これが「テスト可能な設計」の実体です。
💡 設計思想・なぜこう書くのか
@Autowiredのようなアノテーションと実行時のリフレクションで依存関係を自動解決する「DIコンテナ」が主役になります。Goにはこの種の仕組みが標準に存在しません。その代わり、関数の引数として依存を渡すという、ただのGoの基本文法だけでDIが成立します。コンパイル時にすべての依存関係が型として検査され、実行時にリフレクションで解決される「魔法」の層が挟まらないため、「どの実装が実際に注入されているか」がコードを読むだけで常に追跡可能です。EmailSenderインターフェースはOrderServiceが置かれているパッケージ側(呼ぶ側)で定義し、SMTPEmailSender(呼ばれる側)は自分がそのインターフェースを満たすことすら知る必要がありません。この非対称性により、「新しい送信手段(SendGrid API等)を追加したい」というとき、既存のEmailSenderインターフェースにもOrderServiceにも一切手を入れず、新しい構造体を1つ追加するだけで済みます。🌐 他言語との比較
| 観点 | Go(インターフェース+コンストラクタ注入) | Java(Spring DI) | Python(unittest.mock) | TypeScript(NestJS) |
|---|---|---|---|---|
| DIの実現方法 | 言語標準のインターフェース+関数引数だけで実現。専用フレームワーク不要 | @Autowired/@Component等のアノテーションとDIコンテナが実行時に自動解決 | 型を問わずunittest.mock.patchで対象を実行時に動的に差し替える | デコレータ(@Injectable)とDIコンテナがコンストラクタ引数の型から自動解決 |
| 依存関係の可視性 | コンストラクタのシグネチャに全て明示され、静的に追跡可能 | アノテーションとコンテナ設定に分散し、実行しないと解決結果が分かりにくい場合がある | パッチ対象を文字列パス("module.Class.method")で指定するため、静的解析で追いにくい | コンストラクタに明示されるが、コンテナのモジュール設定も併せて追う必要がある |
| モックの作り方 | インターフェースを満たす構造体を手書き、またはgomock等で生成 | Mockito等のモックライブラリでインターフェース/クラスをモック化 | Mock()/patch()で対象を動的に置き換える(型チェックの恩恵は薄い) | jest.mock()やNestJSのテストモジュールでプロバイダを差し替える |
| 型安全性 | インターフェースを満たさなければコンパイルエラーになる | インターフェースベースなら型安全、リフレクション経由の解決は実行時エラーの余地あり | 動的型付けのため、モックの引数・戻り値の型ミスマッチは実行するまで分からない | TypeScriptの型システムにより比較的高い型安全性を持つ |
Pythonのunittest.mock.patchは「既存のクラス・関数を実行時に強制的に書き換える」アプローチのため、事前にインターフェースを設計しなくても後からテスト容易性を後付けできる柔軟さがあります。一方でGoは「最初からインターフェースとして依存を表現する」ことを前提にした設計を要求します。この違いは、Goが静的型付け・コンパイル時検査を重視する言語であることの表れです。
🏆 実務での使いどころ
- 外部サービス(決済API・メール配信・通知基盤)への依存を持つ全てのビジネスロジック: 今回のメール送信のように、実際のネットワークI/Oが絡む処理は、インターフェース越しに抽象化しておかないとCI環境で高速・安定したテストが書けません
- DB層の抽象化(Repository パターン):
UserRepositoryインターフェースを定義し、本番では*sql.DBを使う実装、テストではメモリ上のmapを使う実装を注入する、という同じパターンがDBアクセスにも直接応用できます - 段階的な実装差し替え: 「まずSMTP直叩きでリリースし、後でSendGrid APIに切り替える」というような技術選定の変更を、
EmailSenderを実装するインターフェースの背後に隠しておけば、呼び出し側(OrderService)のコードを一切変更せずに実現できます - 時刻・乱数など「テストのたびに結果が変わる」依存の注入:
time.Nowやrandのような非決定的な依存も、同じ考え方(関数型やインターフェースとして注入する)でテスト可能にできます
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 「GoにDIフレームワークがないから、DIができない」と思い込み、外部依存を持つコードのテストを諦める | Spring等の「DIコンテナ」のイメージが強く、DI=専用フレームワークだと考えてしまう | DIは「依存を外部から渡す」という設計原則であり、フレームワークはその実現手段の1つに過ぎない。Goではインターフェース+コンストラクタ引数という言語標準機能だけでDIが成立する |
インターフェースを「呼ばれる側」(SMTPEmailSender)のパッケージで定義してしまう | 他言語(Javaのinterface実装宣言等)の感覚で、実装クラスと同じ場所に定義したくなる | Goのインターフェースは「呼ぶ側」(OrderService側)が「自分が必要とする最小限の操作」として定義するのが定石。これにより、呼ばれる側は複数の呼び出し元のインターフェースをそれぞれ意識せず暗黙的に満たせる |
モックにSent []SentEmailのような記録用フィールドを用意せず、Sendが呼ばれたかどうかをboolのフラグだけで検証してしまう | シンプルに済ませたい気持ちが働く | 実務では「何回呼ばれたか」「どんな引数で呼ばれたか」の検証が重要になる場面が多い。スライスに記録しておけば、呼び出し回数・引数の両方を後から検証できる柔軟性を最初から確保できる |
NewOrderServiceのようなコンストラクタ関数を用意せず、呼び出し側でOrderService{sender: mock}と直接構造体リテラルを書かせてしまう | Goの構造体リテラルはフィールド名を指定でき手軽に見える | フィールドが増えたときの後方互換性(新フィールド追加時に呼び出し側を壊さない)や、必須依存の強制(sender必須の明示)の観点で、コンストラクタ関数経由の初期化を提供しておく方が保守性が高い |
🚀 次のステップ
- 発展: 今回の
EmailSenderに加えてOrderRepository(注文の永続化)インターフェースも導入し、ConfirmOrderが「メール送信」と「注文保存」の両方に依存する設計に拡張してみましょう。複数の依存を持つ構造体でのDIパターンの練習になります - 次回予告: Day 027 — HTTPサーバー実装(net/http)(実装)。今日学んだインターフェース設計とDIの考え方を土台に、
net/httpのハンドラ・ミドルウェアの基本形を学びます。ハンドラの中で今回のようなOrderServiceを呼び出す構成にも触れる予定です