課題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 - アサーションライブラリ(オプション)