test-writing

keito-isurugi's avatarfrom keito-isurugi

TDD(テスト駆動開発)に基づいたテストコード作成スキル。Red-Green-Refactorサイクルを実践し、t-wadaのベストプラクティスに従ってテストファーストで高品質なテストを作成します。Backend(Go test)とFrontend(Jest)の両方に対応。

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 31, 2025

When & Why to Use This Skill

This Claude skill automates the creation of high-quality test suites using Test-Driven Development (TDD) principles. By strictly following the Red-Green-Refactor cycle and industry best practices (t-wada), it assists developers in building robust Backend (Go) and Frontend (Jest) applications. It ensures code reliability through comprehensive test case design, including happy paths, boundary values, and edge cases, while optimizing for maintainable and clean code architecture.

Use Cases

  • Implementing new features using a test-first approach to ensure functional requirements are met before writing production code.
  • Creating reproduction tests for reported bugs to verify fixes and prevent regressions in both Go and JavaScript/TypeScript environments.
  • Refactoring legacy codebases with confidence by maintaining a 'Green' state through continuous automated verification.
  • Increasing test coverage for critical domain logic and UI components using framework-specific tools like testify for Go and Testing Library for React.
  • Designing structured test cases that cover boundary conditions and error handling to improve system resilience.
nametest-writing
description|

Test Writing Skill

TDDの原則に従って高品質なテストコードを作成するスキル。

対応フレームワーク

対象 フレームワーク 詳細ガイド
Backend Go test + testify ./agents/backend-test-writer.md
Frontend Jest + Testing Library ./agents/frontend-test-writer.md

判断フロー

テストを書く必要がある?
│
├─ 新機能実装 ──────────────────────→ ✅ TDD(テストファースト)
│
├─ バグ修正 ────────────────────────→ ✅ 再現テスト → 修正 → グリーン確認
│
├─ リファクタリング ────────────────→ ✅ 既存テスト確認 or 追加 → 実行
│
├─ 既存コードへのカバレッジ追加 ───→ ✅ テスト追加(後付け)
│
├─ PoC・プロトタイプ ───────────────→ ❌ スキップ可
│
└─ Getter/Setter・単純マッピング ──→ ❌ 価値が低い

TDDサイクル(Red → Green → Refactor)

    ┌─────────────────────────────────────────────────┐
    │                                                 │
    ▼                                                 │
┌───────┐     ┌───────┐     ┌───────────┐           │
│  Red  │ ──→ │ Green │ ──→ │ Refactor  │ ──────────┘
│       │     │       │     │           │
│失敗する│     │最小実装│     │コード改善 │
│テスト  │     │で通す │     │テスト維持 │
└───────┘     └───────┘     └───────────┘

Step 1: Red(失敗するテストを書く)

// Backend (Go)
func TestCreateImage_WithValidData_ReturnsImage(t *testing.T) {
    // Arrange
    repo := NewImageRepository(db)
    name := "test.jpg"
    url := "https://example.com/test.jpg"

    // Act
    image, err := repo.Create(name, url)

    // Assert
    assert.NoError(t, err)
    assert.NotZero(t, image.ID)
    assert.Equal(t, name, image.Name)
}
# テスト実行(失敗することを確認)
cd backend && go test -v ./...
# FAIL ← これが正しい状態

Step 2: Green(最小限の実装で通す)

// 最小限のコードでテストを通す
func (r *imageRepository) Create(name, url string) (*Image, error) {
    image := &Image{Name: name, URL: url}
    if err := r.db.Create(image).Error; err != nil {
        return nil, err
    }
    return image, nil
}
# テスト実行(成功することを確認)
cd backend && go test -v ./...
# PASS ← グリーン!

Step 3: Refactor(テストを維持しながら改善)

// テストがグリーンの状態でのみリファクタリング
func (r *imageRepository) Create(name, url string) (*Image, error) {
    if err := r.validateInput(name, url); err != nil {
        return nil, err
    }

    image := &Image{
        Name:      name,
        URL:       url,
        CreatedAt: time.Now(),
    }

    if err := r.db.Create(image).Error; err != nil {
        return nil, fmt.Errorf("failed to create image: %w", err)
    }

    return image, nil
}
# リファクタリング後もグリーンを確認
cd backend && go test -v ./...
# PASS ← 維持されている!

テストケース設計(4カテゴリ必須)

// Backend (Go)
func TestImageRepository(t *testing.T) {
    // ✅ 正常系(Happy Path)
    t.Run("Create_WithValidData_ReturnsImage", func(t *testing.T) {
        // ...
    })

    // ✅ 境界値(Boundary Value)
    t.Run("Create_WithMaxLengthName_Succeeds", func(t *testing.T) {
        // ...
    })

    // ✅ 異常系(Error Cases)
    t.Run("Create_WithEmptyName_ReturnsError", func(t *testing.T) {
        // ...
    })

    // ✅ エッジケース(Edge Cases)
    t.Run("FindByID_WhenNotFound_ReturnsNil", func(t *testing.T) {
        // ...
    })
}
// Frontend (Jest)
describe("useTodos", () => {
  // ✅ 正常系
  it("should add a new todo", async () => {
    // ...
  });

  // ✅ 境界値
  it("should handle empty title", async () => {
    // ...
  });

  // ✅ 異常系
  it("should set error when API fails", async () => {
    // ...
  });

  // ✅ エッジケース
  it("should handle empty initial todos", () => {
    // ...
  });
});

Backend テスト(Go)

基本構造

// domain/image_test.go
package domain_test

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "your-project/domain"
)

func TestNewImage(t *testing.T) {
    t.Run("WithValidData_ReturnsImage", func(t *testing.T) {
        // Arrange
        name := "test.jpg"
        url := "https://example.com/test.jpg"

        // Act
        image, err := domain.NewImage(name, url)

        // Assert
        require.NoError(t, err)
        assert.Equal(t, name, image.Name)
        assert.Equal(t, url, image.URL)
    })

    t.Run("WithEmptyName_ReturnsError", func(t *testing.T) {
        // Act
        _, err := domain.NewImage("", "https://example.com/test.jpg")

        // Assert
        assert.Error(t, err)
        assert.ErrorIs(t, err, domain.ErrEmptyName)
    })
}

テーブル駆動テスト

func TestValidateURL(t *testing.T) {
    tests := []struct {
        name    string
        url     string
        wantErr bool
    }{
        {"valid http", "http://example.com/image.jpg", false},
        {"valid https", "https://example.com/image.jpg", false},
        {"empty", "", true},
        {"invalid", "not-a-url", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateURL(tt.url)
            if tt.wantErr {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
            }
        })
    }
}

テスト実行コマンド

# 全テスト実行
cd backend && go test ./...

# 詳細出力
go test -v ./...

# カバレッジ付き
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# 特定テスト
go test -v -run TestCreateImage ./domain/...

Frontend テスト(Jest)

コンポーネントテスト

// src/components/todo/TodoItem.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { TodoItem } from "./TodoItem";

describe("TodoItem", () => {
  const mockTodo = {
    id: "1",
    title: "Test Todo",
    completed: false,
  };

  it("should render todo title", () => {
    render(<TodoItem todo={mockTodo} onToggle={() => {}} />);
    expect(screen.getByText("Test Todo")).toBeInTheDocument();
  });

  it("should call onToggle when checkbox clicked", () => {
    const handleToggle = jest.fn();
    render(<TodoItem todo={mockTodo} onToggle={handleToggle} />);

    fireEvent.click(screen.getByRole("checkbox"));

    expect(handleToggle).toHaveBeenCalledWith("1");
  });
});

カスタムフックテスト

// src/hooks/useTodos.test.ts
import { renderHook, act } from "@testing-library/react";
import { useTodos } from "./useTodos";

describe("useTodos", () => {
  it("should initialize with empty todos", () => {
    const { result } = renderHook(() => useTodos());
    expect(result.current.todos).toEqual([]);
  });

  it("should add a new todo", async () => {
    const { result } = renderHook(() => useTodos());

    await act(async () => {
      await result.current.addTodo("New Todo");
    });

    expect(result.current.todos).toHaveLength(1);
    expect(result.current.todos[0].title).toBe("New Todo");
  });
});

テスト実行コマンド

cd frontend

# 全テスト実行
npm run test

# ウォッチモード
npm run test -- --watch

# カバレッジ
npm run test -- --coverage

# 特定ファイル
npm run test -- useTodos.test.ts

よくある誤り

⚠️ テスト間の状態共有

// Bad: パッケージ変数で状態共有
var testDB *gorm.DB // ❌

func TestCreate(t *testing.T) {
    // testDBを使用
}

// Good: 各テストで独立
func TestCreate(t *testing.T) {
    db := setupTestDB(t) // テストごとに作成
    defer cleanupTestDB(t, db)
    // ...
}

⚠️ モックの過剰使用

// Bad: 全てをモック
func TestUserService(t *testing.T) {
    mockRepo := new(MockRepo)
    mockValidator := new(MockValidator)
    mockLogger := new(MockLogger)
    // 実質何もテストしていない ❌
}

// Good: 境界のみモック
func TestUserService(t *testing.T) {
    db := setupTestDB(t)
    repo := NewRealRepo(db)          // 実際のリポジトリ
    mockEmailClient := new(MockEmail) // 外部サービスのみモック

    service := NewUserService(repo, mockEmailClient)
    // ...
}

カバレッジ目標

レイヤー 目標 優先度
Domain層 90%+ 最高
Application層 80%+ 高
Hooks 80%+ 高
Utils 90%+ 高
Components 60%+ 中

参照ファイル

ファイル 説明
./agents/backend-test-writer.md Backend専門ガイド
./agents/frontend-test-writer.md Frontend専門ガイド
.claude/rules/testing-rules.md テストルール詳細