Aller au contenu principal
友田 陽大
Go & Echo in production
Go
Echo
テスト
型安全
アーキテクチャ設計
可観測性

Echo テスト戦略 完全ガイド:httptest・echotest・モック・testcontainers で速くて壊れないテストを書く

Go Echo(v5)のテスト戦略を本番品質で設計するガイド。テーブル駆動テスト、httptest と e.NewContext によるハンドラ単体テスト、v5 新搭載の echotest ヘルパ、インターフェースのモックによる UseCase 単体テスト、testcontainers-go による実 DB 統合テスト、CI での -race/-cover とテストピラミッドまでを実コードで解説します。

Published
Reading time
8 min read
Author
友田 陽大
Partager

「テストを書く時間がない」の正体は、たいてい「テストを書きにくい設計」です。ハンドラに DB と業務ロジックが絡みついていると、1つのテストに本物の DB とネットワークが要り、遅く・脆く・誰も書かなくなります。逆に、設計が正しければテストは速く・安く・たくさん書けます。

この記事は、Go Echo 本番運用ガイドのテスト編です。Echo のハンドラが素の関数であることを最大限に活かし、クリーンアーキテクチャの層ごとに適切なテストを当てる戦略を、実コードで解説します。題材は、実際にユニット・統合テストを実装した飲食店マッチングのバックエンドです。

この記事のルール:Echo の API は 公式ドキュメント(v5・2026年6月時点) に基づきます。testcontainers-gotestify 等は更新されるため公式で最新を確認してください。テスト方針は本番運用で実証済みの形に整えています。


0. 戦略:テストピラミッドで「どこを何でテストするか」を決める

すべてを E2E で確かめるのは遅くて壊れやすく、すべてをモックで済ますと SQL の誤りを見逃します。層ごとに道具を変えるのが正解です。

        ▲  E2E(少数・遅い)         … 主要導線を本物に近い構成で
       ───
      統合テスト(中程度)          … Repository × 実DB(testcontainers)
     ─────
   単体テスト(多数・高速)         … UseCase(モック)/ ハンドラ(httptest)
何を検証道具速度
UseCase 単体業務ロジックの全分岐モック(インターフェース差替)最速
ハンドラ単体HTTP 入出力・ステータスhttptest / echotest
Repository 統合SQL の正しさtestcontainers-go(実 Postgres)
E2E主要導線の通し起動済みサーバー

1. ハンドラ単体テスト:httptest + e.NewContext

Echo のハンドラは *echo.Context を受け取る素の関数です。標準の net/http/httptest でリクエスト/レコーダを作り、e.NewContext で Context を組めば、サーバーを起動せずにテストできます。

func TestUserHandler_Create(t *testing.T) {
	e := echo.New()
	e.Validator = &CustomValidator{validator: validator.New()}

	body := `{"name":"Hinata","email":"hinata@example.com"}`
	req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	h := &UserHandler{uc: newUseCaseWithMockRepo()} // 依存はモック

	if assert.NoError(t, h.Create(c)) {
		assert.Equal(t, http.StatusCreated, rec.Code)
		assert.JSONEq(t, `{"id":"...","name":"Hinata"}`, rec.Body.String())
	}
}

パスパラメータが要るハンドラは、Context に手で設定します。

req := httptest.NewRequest(http.MethodGet, "/users/42", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("id")
c.SetParamValues("42") // c.Param("id") == "42"

2. echotest:v5 のヘルパでボイラープレートを削る

Echo v5 には新しい echotest パッケージがあり、上記の定型を宣言的に書けます。

import "github.com/labstack/echo/v5/echotest"

func TestUserHandler_Create_WithHelper(t *testing.T) {
	h := &UserHandler{uc: newUseCaseWithMockRepo()}

	rec := echotest.ContextConfig{
		Headers: map[string][]string{
			echo.HeaderContentType: {echo.MIMEApplicationJSON},
		},
		JSONBody: []byte(`{"name":"Hinata","email":"hinata@example.com"}`),
	}.ServeWithHandler(t, h.Create)

	assert.Equal(t, http.StatusCreated, rec.Code)
}

ContextConfigPathValues(パスパラメータ)、フォーム、マルチパートなども宣言的に組めます。httptest を直書きするより意図が読みやすく、テストの保守性が上がります。


3. テーブル駆動テスト:Go の王道で網羅する

入力パターンが多いとき(バリデーション境界など)は、テーブル駆動テストで網羅します。Go で最も推奨される形です。

func TestCreateUserDTO_Validation(t *testing.T) {
	v := validator.New()
	tests := []struct {
		name    string
		body    string
		wantErr bool
	}{
		{"valid", `{"name":"Hinata","email":"a@example.com"}`, false},
		{"missing name", `{"email":"a@example.com"}`, true},
		{"bad email", `{"name":"Hinata","email":"not-an-email"}`, true},
		{"name too short", `{"name":"H","email":"a@example.com"}`, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var dto CreateUserDTO
			_ = json.Unmarshal([]byte(tt.body), &dto)
			err := v.Struct(dto)
			assert.Equal(t, tt.wantErr, err != nil)
		})
	}
}

t.Run でサブテスト化すると、失敗時にどのケースが落ちたかが一目で分かります。


4. UseCase 単体テスト:モックで全分岐を高速に

テストが速くたくさん書ける最大の理由が、インターフェースのモックです。クリーンアーキテクチャで Repository をインターフェースにしておけば、UseCase をDB なしでテストできます。

// 手書きフェイク(小規模ならこれで十分。KISS)
type fakeUserRepo struct {
	createErr error
	user      *domain.User
}

func (f *fakeUserRepo) Create(ctx context.Context, u *domain.User) error { return f.createErr }
func (f *fakeUserRepo) FindByID(ctx context.Context, id string) (*domain.User, error) {
	return f.user, nil
}

func TestRegister(t *testing.T) {
	t.Run("メール重複で ErrEmailTaken", func(t *testing.T) {
		uc := usecase.NewUserUseCase(&fakeUserRepo{createErr: domain.ErrEmailTaken})
		_, err := uc.Register(context.Background(), "Hinata", "dup@example.com")
		assert.ErrorIs(t, err, domain.ErrEmailTaken)
	})
	t.Run("正常系", func(t *testing.T) {
		uc := usecase.NewUserUseCase(&fakeUserRepo{})
		u, err := uc.Register(context.Background(), "Hinata", "a@example.com")
		assert.NoError(t, err)
		assert.Equal(t, "Hinata", u.Name)
	})
}

手書きフェイク vs モック生成(go.uber.org/mock 等):インターフェースが小さく数が少ないなら手書きフェイクが読みやすく(KISS/YAGNI)、大規模・多数なら生成ツールで省力化します。プロジェクトの規模で選びます。


5. Repository 統合テスト:testcontainers-go で実 DB を使う

SQL の正しさはモックでは検証できません。 スキーマ・制約・トランザクション・実際のクエリ結果は、本物の Postgresでしか確かめられません。testcontainers-go なら、テスト実行時に Docker で使い捨ての Postgres を立てられます。

import (
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/modules/postgres"
)

func setupPostgres(t *testing.T) *pgxpool.Pool {
	ctx := context.Background()
	pgC, err := postgres.Run(ctx, "postgres:16-alpine",
		postgres.WithDatabase("test"),
		postgres.WithUsername("test"),
		postgres.WithPassword("test"),
		testcontainers.WithWaitStrategy(wait.ForListeningPort("5432/tcp")),
	)
	require.NoError(t, err)
	t.Cleanup(func() { _ = pgC.Terminate(ctx) }) // テスト後に破棄

	dsn, _ := pgC.ConnectionString(ctx, "sslmode=disable")
	pool, err := pgxpool.New(ctx, dsn)
	require.NoError(t, err)
	runMigrations(t, dsn) // スキーマを適用
	return pool
}

func TestUserRepository_CreateAndFind(t *testing.T) {
	if testing.Short() {
		t.Skip("skip integration test in -short")
	}
	pool := setupPostgres(t)
	repo := infra.NewUserRepository(pool)

	err := repo.Create(context.Background(), &domain.User{ID: "u1", Name: "Hinata", Email: "a@example.com"})
	require.NoError(t, err)

	got, err := repo.FindByID(context.Background(), "u1")
	require.NoError(t, err)
	assert.Equal(t, "Hinata", got.Name)
}

testing.Short()-short 時にスキップできるようにし、速い単体テストと遅い統合テストを分離します。ローカルやプレ commit は単体中心、CI では統合まで回す、という使い分けが定番です。


6. CI ゲート:-race / -cover / golangci-lint を必須化

テストは「書く」だけでなく「CI で必ず通す」ことで初めて品質ゲートになります。

# .github/workflows/ci.yml(抜粋)
- name: Test
  run: go test ./... -race -coverprofile=coverage.out -covermode=atomic
- name: Lint
  run: golangci-lint run ./...
  • -race:データ競合検出。Go の並行バグは本番でしか出ないことが多く、CI での -race が唯一の現実的な防波堤。
  • -cover:カバレッジ計測。数字を目的化しないが、重要パスの未テストを可視化する。
  • golangci-lint:静的解析(go vet・未使用・nil 参照可能性など)。テストと並ぶ品質ゲート。

このゲート設計の思想(型・テスト・静的解析・セキュリティを CI で機械的に強制する)は、AI駆動開発の品質ゲートで体系化しています。実プロジェクトでも go test/golangci-lintGitHub Actions の必須チェックにし、PR ごとに自動検証していました。


7. ミドルウェア・認可のテスト:境界を回帰検証する

認可(RBAC・所有者チェック)は、壊れても 200 が返ってしまうため気づきにくい。だからこそテストで固定します。ミドルウェアを通した状態をテストするには、e.ServeHTTP でルーティングごと検証します。

func TestRequireRole_Forbidden(t *testing.T) {
	e := echo.New()
	g := e.Group("/admin")
	g.Use(injectClaims(&AccessClaims{Roles: []string{"user"}})) // 一般ユーザー
	g.Use(RequireRole("admin"))
	g.GET("/x", func(c *echo.Context) error { return c.NoContent(http.StatusOK) })

	req := httptest.NewRequest(http.MethodGet, "/admin/x", nil)
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req) // ルーター + ミドルウェアを通す

	assert.Equal(t, http.StatusForbidden, rec.Code) // user は admin ルートを叩けない
}

「権限のないユーザーが叩けないこと」を明示的にテストするのが要点です。正常系だけでなく拒否系を回帰テストに入れることで、リファクタで認可が緩んだ瞬間に CI が落ちます。


まとめ:壊れないテストの7原則

  1. テストピラミッドで層ごとに道具を変える(単体・統合・E2E)。
  2. ハンドラは httptest + e.NewContext、v5 なら echotest で簡潔に。
  3. 多入力はテーブル駆動で網羅し、t.Run で失敗箇所を明示。
  4. UseCase はモックで全分岐を高速に。インターフェース設計の見返り。
  5. SQL は testcontainers で実 DB。モックでは SQL の誤りを検出できない。
  6. CI で -race/-cover/golangci-lint を必須ゲートに。
  7. 認可の拒否系を回帰テストし、権限の緩みを CI で捕まえる。

テスト容易性は、後付けの努力ではなく設計の結果です。Echo のハンドラが素の関数であること、Repository をインターフェースで隠すこと——この2つを守れば、テストは自然に書けるようになります。設計の土台はクリーンアーキテクチャ、全体像は本番運用ガイドへ。

友田

友田 陽大

Créateur d’un produit lauréat du prix du ministre (METI). Avec TypeScript + Python + AWS, je livre SaaS, transformation numérique industrielle et IA générative (RAG) prête pour la production — des spécifications à l’infrastructure et à l’exploitation, seul.

I can take on the implementation from this article as an engagement

I build Go / Echo backends, from design to production

API design and migration to Echo v5, clean architecture (Controller/UseCase/Repository + DI), middleware and security, centralized error handling, graceful shutdown, and testing/CI. With experience building a clean-architecture backend in Go/Echo + google/wire, I implement APIs that don't fall over, are traceable, and are easy to change.

Disponible en mission au forfait comme en conseil technique. Commencez par un échange gratuit de 30 minutes.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

À lire également