課題16: テストスイート作成

課題概要

この課題では、実践的なGoプログラムに対して包括的なテストスイートを作成します。テーブル駆動テスト、ベンチマーク、カバレッジ測定など、Goのテスト機能をフル活用します。

マンダトリー要件(80点)

要件1: 文字列操作ライブラリとテスト(30点)

以下の関数を実装し、テーブル駆動テストを作成してください。

実装ファイル: strutil/strutil.go

package strutil

import (
    "strings"
    "unicode"
)

// Reverse は文字列を逆転します(UTF-8対応)
func Reverse(s string) string {
    // TODO: 実装
}

// IsPalindrome は文字列が回文かどうかを判定します
func IsPalindrome(s string) bool {
    // TODO: 実装
}

// CountWords は文字列内の単語数をカウントします
func CountWords(s string) int {
    // TODO: 実装
}

// Capitalize は各単語の最初の文字を大文字にします
func Capitalize(s string) string {
    // TODO: 実装
}

テストファイル: strutil/strutil_test.go

少なくとも各関数に5つ以上のテストケースを含むテーブル駆動テストを作成してください。

package strutil

import "testing"

func TestReverse(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        expected string
    }{
        // TODO: 最低5つのテストケース
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // TODO: テスト実装
        })
    }
}

// 他の関数も同様に実装

要件2: 計算機ライブラリとエラーテスト(25点)

エラー処理を含む計算機を実装し、エラーケースも含めてテストしてください。

実装ファイル: calculator/calculator.go

package calculator

import (
    "errors"
    "math"
)

var (
    ErrDivisionByZero = errors.New("division by zero")
    ErrNegativeSqrt   = errors.New("square root of negative number")
    ErrInvalidInput   = errors.New("invalid input")
)

// Divide は除算を実行します
func Divide(a, b float64) (float64, error) {
    // TODO: 実装
}

// Sqrt は平方根を計算します
func Sqrt(x float64) (float64, error) {
    // TODO: 実装
}

// Power は累乗を計算します
func Power(base, exp float64) (float64, error) {
    // TODO: 実装
}

// Percentage はパーセンテージを計算します
func Percentage(value, percent float64) (float64, error) {
    // TODO: 実装
}

テストファイル: calculator/calculator_test.go

正常ケースとエラーケースの両方をテストしてください。

package calculator

import (
    "testing"
)

func TestDivide(t *testing.T) {
    tests := []struct {
        name      string
        a, b      float64
        expected  float64
        expectErr bool
        err       error
    }{
        // 正常ケース
        {"正の数の除算", 10, 2, 5, false, nil},
        {"小数の除算", 7, 2, 3.5, false, nil},
        // エラーケース
        {"ゼロ除算", 5, 0, 0, true, ErrDivisionByZero},
        // TODO: 追加のテストケース
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // TODO: テスト実装
        })
    }
}

// 他の関数も同様に実装

要件3: データ構造とベンチマーク(25点)

スタック構造を実装し、ベンチマークを作成してください。

実装ファイル: stack/stack.go

package stack

import "errors"

var ErrEmptyStack = errors.New("stack is empty")

type Stack struct {
    items []interface{}
}

func New() *Stack {
    return &Stack{
        items: make([]interface{}, 0),
    }
}

func (s *Stack) Push(item interface{}) {
    // TODO: 実装
}

func (s *Stack) Pop() (interface{}, error) {
    // TODO: 実装
}

func (s *Stack) Peek() (interface{}, error) {
    // TODO: 実装
}

func (s *Stack) IsEmpty() bool {
    // TODO: 実装
}

func (s *Stack) Size() int {
    // TODO: 実装
}

テストとベンチマーク: stack/stack_test.go

package stack

import "testing"

func TestStack(t *testing.T) {
    t.Run("Push and Pop", func(t *testing.T) {
        // TODO: 実装
    })

    t.Run("Empty Stack", func(t *testing.T) {
        // TODO: 実装
    })

    // 他のテストケース
}

func BenchmarkStackPush(b *testing.B) {
    s := New()
    for i := 0; i < b.N; i++ {
        s.Push(i)
    }
}

func BenchmarkStackPop(b *testing.B) {
    s := New()
    // セットアップ
    for i := 0; i < 1000; i++ {
        s.Push(i)
    }

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        if !s.IsEmpty() {
            s.Pop()
        }
    }
}

// 他のベンチマーク

実行と検証

# すべてのテストを実行
go test ./...

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

# カバレッジ測定
go test -cover ./...

# カバレッジレポート生成
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# ベンチマーク実行
go test -bench=. ./...

# ベンチマークとメモリ統計
go test -bench=. -benchmem ./...

期待される出力

テスト実行

=== RUN   TestReverse
=== RUN   TestReverse/空文字列
=== RUN   TestReverse/1文字
=== RUN   TestReverse/通常の文字列
--- PASS: TestReverse (0.00s)
    --- PASS: TestReverse/空文字列 (0.00s)
    --- PASS: TestReverse/1文字 (0.00s)
    --- PASS: TestReverse/通常の文字列 (0.00s)
PASS

カバレッジ

ok      strutil     0.005s  coverage: 100.0% of statements
ok      calculator  0.006s  coverage: 95.2% of statements
ok      stack       0.004s  coverage: 100.0% of statements

ベンチマーク

BenchmarkStackPush-8        10000000       120 ns/op      48 B/op    1 allocs/op
BenchmarkStackPop-8         20000000       85 ns/op       0 B/op     0 allocs/op

ボーナス課題(20点)

ボーナス1: モックを使った統合テスト(10点)

データベース操作をシミュレートするサービスを実装し、モックを使ってテストしてください。

実装ファイル: userservice/service.go

package userservice

import "errors"

var ErrUserNotFound = errors.New("user not found")

type User struct {
    ID    int
    Name  string
    Email string
}

// Database はデータベース操作のインターフェース
type Database interface {
    GetUser(id int) (*User, error)
    SaveUser(user *User) error
    DeleteUser(id int) error
}

type UserService struct {
    db Database
}

func NewUserService(db Database) *UserService {
    return &UserService{db: db}
}

func (s *UserService) GetUserByID(id int) (*User, error) {
    // TODO: 実装
}

func (s *UserService) UpdateUserEmail(id int, email string) error {
    // TODO: 実装
    // 1. ユーザーを取得
    // 2. メールアドレスを更新
    // 3. 保存
}

func (s *UserService) DeleteUserByID(id int) error {
    // TODO: 実装
}

テストファイル: userservice/service_test.go

package userservice

import "testing"

// MockDatabase はテスト用のモックデータベース
type MockDatabase struct {
    users map[int]*User
    // TODO: 必要なフィールドを追加
}

func NewMockDatabase() *MockDatabase {
    return &MockDatabase{
        users: make(map[int]*User),
    }
}

func (m *MockDatabase) GetUser(id int) (*User, error) {
    // TODO: 実装
}

func (m *MockDatabase) SaveUser(user *User) error {
    // TODO: 実装
}

func (m *MockDatabase) DeleteUser(id int) error {
    // TODO: 実装
}

func TestUserService(t *testing.T) {
    t.Run("GetUserByID", func(t *testing.T) {
        // TODO: モックDBを使ったテスト
    })

    t.Run("UpdateUserEmail", func(t *testing.T) {
        // TODO: モックDBを使ったテスト
    })

    // 他のテストケース
}

ボーナス2: テーブル駆動テストの高度な使用(5点)

複雑なビジネスロジックをテーブル駆動テストでテストしてください。

実装ファイル: pricing/pricing.go

package pricing

// DiscountType は割引タイプ
type DiscountType int

const (
    NoDiscount DiscountType = iota
    PercentageDiscount
    FixedAmountDiscount
    BuyOneGetOne
)

type Item struct {
    Name     string
    Price    float64
    Quantity int
}

type Discount struct {
    Type   DiscountType
    Value  float64 // パーセンテージまたは固定額
}

// CalculateTotal は合計金額を計算します
func CalculateTotal(items []Item, discount *Discount) float64 {
    // TODO: 実装
    // - 各アイテムの小計を計算
    // - 割引を適用
    // - 合計を返す
}

テストファイル: pricing/pricing_test.go

package pricing

import "testing"

func TestCalculateTotal(t *testing.T) {
    tests := []struct {
        name     string
        items    []Item
        discount *Discount
        expected float64
    }{
        {
            name: "割引なし",
            items: []Item{
                {Name: "商品A", Price: 100, Quantity: 2},
                {Name: "商品B", Price: 200, Quantity: 1},
            },
            discount: nil,
            expected: 400,
        },
        {
            name: "10%割引",
            items: []Item{
                {Name: "商品A", Price: 100, Quantity: 1},
            },
            discount: &Discount{Type: PercentageDiscount, Value: 10},
            expected: 90,
        },
        // TODO: 追加のテストケース
        // - 固定額割引
        // - BOGO(Buy One Get One)
        // - 複雑な組み合わせ
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := CalculateTotal(tt.items, tt.discount)
            if result != tt.expected {
                t.Errorf("CalculateTotal() = %f; want %f", result, tt.expected)
            }
        })
    }
}

ボーナス3: カバレッジ100%達成とレポート(5点)

すべてのパッケージでカバレッジ100%を達成し、詳細なカバレッジレポートを生成してください。

# すべてのパッケージのカバレッジを測定
go test -coverprofile=coverage.out ./...

# カバレッジ統計を表示
go tool cover -func=coverage.out

# HTMLレポート生成
go tool cover -html=coverage.out -o coverage.html

カバレッジレポートのスクリーンショットを提出してください。

評価基準

項目 配点 詳細
文字列操作ライブラリ 30点 すべての関数が正しく実装され、テーブル駆動テストが完備
計算機ライブラリ 25点 エラー処理が適切で、エラーケースのテストも含む
スタック構造 25点 データ構造が正しく実装され、ベンチマークが提供されている
**ボーナス1: モック** 10点 モックを使った統合テストが適切に実装されている
**ボーナス2: 高度なテスト** 5点 複雑なビジネスロジックが包括的にテストされている
**ボーナス3: カバレッジ** 5点 100%カバレッジを達成し、レポートが提出されている

提出方法

以下のディレクトリ構造で提出してください:

submission/
├── strutil/
│   ├── strutil.go
│   └── strutil_test.go
├── calculator/
│   ├── calculator.go
│   └── calculator_test.go
├── stack/
│   ├── stack.go
│   └── stack_test.go
├── bonus/              # ボーナス課題(オプション)
│   ├── userservice/
│   │   ├── service.go
│   │   └── service_test.go
│   ├── pricing/
│   │   ├── pricing.go
│   │   └── pricing_test.go
│   └── coverage.html
└── README.md           # テスト実行方法とカバレッジ結果

ヒント

  • テーブル駆動テスト: 構造体のスライスでテストケースを定義
  • エラーテスト: errors.Is()またはerr.Error()で比較
  • ベンチマーク: b.ResetTimer()でセットアップ時間を除外
  • カバレッジ: エッジケースも忘れずにテスト
  • モック: インターフェースを使って依存を注入
  • 学習リソース

  • Go Testing Package
  • Table Driven Tests
  • Code Coverage
  • Testify Package - アサーションライブラリ(オプション)