Testify
Testify
概要
Testifyは、Goの標準ライブラリと親和性の高い、共通のアサーションとモック機能を提供するツールキットです。Goの標準的なtestingパッケージを拡張し、より読みやすく保守しやすいテストコードの記述を可能にします。軽量でありながら強力な機能を持ち、Go開発者の間で広く採用されているテストフレームワークです。
詳細
主要な特徴
シンプルなアサーション
- 直感的で読みやすいアサーションメソッド
assert.Equal(),assert.NotNil(),assert.True()などの豊富なアサーション- カスタムエラーメッセージのサポート
強力なモック機能
mock.Mockによるモックオブジェクトの作成- メソッド呼び出しの期待値設定
- 引数のマッチングとレスポンスの設定
- 期待値の検証機能
テストスイート機能
suite.Suiteによる構造化されたテスト- SetupとTeardownの自動実行
- テストメソッドの自動検出と実行
標準ライブラリとの統合
- Goの標準
testingパッケージとの完全な互換性 - 既存のテストコードへの段階的な導入が可能
メリット・デメリット
メリット
- 学習コストの低さ: Goの標準testingパッケージの延長として使用可能
- 豊富なアサーション: 多様なデータ型に対応した専用アサーション
- モック機能: 外部依存をモック化して単体テストを分離
- 軽量性: 最小限の依存関係で高速な実行
- コミュニティサポート: Go界隈での広い採用と豊富な事例
デメリット
- 機能の制限: 他のフレームワークと比較すると高度な機能は限定的
- エラーメッセージ: 標準的なエラーメッセージが時として不十分
- 並列実行: 複雑な並列テストシナリオでは制約がある
- モックの複雑さ: 複雑なモックシナリオでは冗長になる場合がある
参考ページ
- GitHub: stretchr/testify
- Go Doc: pkg.go.dev/github.com/stretchr/testify
- Assert Package: pkg.go.dev/github.com/stretchr/testify/assert
- Mock Package: pkg.go.dev/github.com/stretchr/testify/mock
- Suite Package: pkg.go.dev/github.com/stretchr/testify/suite
書き方の例
基本的なアサーションテスト
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBasicAssertions(t *testing.T) {
// 等価性のテスト
assert.Equal(t, 123, 123, "値は等しくなければなりません")
// 非等価性のテスト
assert.NotEqual(t, 123, 456, "値は異なるべきです")
// nil チェック
assert.Nil(t, nil)
// 非nil チェック
var obj interface{} = "something"
if assert.NotNil(t, obj) {
// objがnilでないことが確認できたので、
// 安全にさらなるアサーションを実行
assert.Equal(t, "something", obj)
}
}
func TestCalculator(t *testing.T) {
calc := NewCalculator()
// 加算のテスト
result := calc.Add(5, 3)
assert.Equal(t, 8, result, "5 + 3 は 8 になるべきです")
// 減算のテスト
result = calc.Subtract(10, 4)
assert.Equal(t, 6, result, "10 - 4 は 6 になるべきです")
// 除算のテスト(エラーケース)
_, err := calc.Divide(10, 0)
assert.Error(t, err, "0での除算はエラーになるべきです")
assert.Contains(t, err.Error(), "division by zero")
}
アサーションインスタンスの使用
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWithAssertInstance(t *testing.T) {
assert := assert.New(t)
// assertインスタンスを使用することで、
// 各アサーションでtパラメータを渡す必要がなくなります
assert.Equal(123, 123, "値は等しくなければなりません")
assert.NotEqual(123, 456, "値は異なるべきです")
assert.Nil(nil)
var obj interface{} = "something"
if assert.NotNil(obj) {
assert.Equal("something", obj)
}
}
func TestStringOperations(t *testing.T) {
assert := assert.New(t)
text := "Hello, World!"
assert.Contains(text, "World")
assert.NotContains(text, "xyz")
assert.Len(text, 13)
assert.True(len(text) > 10)
}
モック機能の使用
package main
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
// テスト対象のインターフェース
type DataService interface {
GetData(id int) (string, error)
SaveData(id int, data string) error
}
// モックオブジェクト
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) GetData(id int) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
}
func (m *MockDataService) SaveData(id int, data string) error {
args := m.Called(id, data)
return args.Error(0)
}
// テスト対象のビジネスロジック
type UserService struct {
dataService DataService
}
func (s *UserService) ProcessUser(userID int) (string, error) {
data, err := s.dataService.GetData(userID)
if err != nil {
return "", err
}
processedData := "Processed: " + data
err = s.dataService.SaveData(userID, processedData)
if err != nil {
return "", err
}
return processedData, nil
}
func TestUserService(t *testing.T) {
// モックオブジェクトの作成
mockService := new(MockDataService)
// 期待値の設定
mockService.On("GetData", 123).Return("user data", nil)
mockService.On("SaveData", 123, "Processed: user data").Return(nil)
// テスト対象のコード実行
userService := &UserService{dataService: mockService}
result, err := userService.ProcessUser(123)
// アサーション
assert.NoError(t, err)
assert.Equal(t, "Processed: user data", result)
// 期待値が満たされたことを確認
mockService.AssertExpectations(t)
}
func TestUserServiceWithError(t *testing.T) {
mockService := new(MockDataService)
// エラーケースの期待値設定
mockService.On("GetData", 456).Return("", assert.AnError)
userService := &UserService{dataService: mockService}
result, err := userService.ProcessUser(456)
// エラーケースのアサーション
assert.Error(t, err)
assert.Empty(t, result)
mockService.AssertExpectations(t)
}
プレースホルダーを使用したモック
func TestWithPlaceholders(t *testing.T) {
mockService := new(MockDataService)
// mock.Anythingを使用して任意の引数を受け入れ
mockService.On("GetData", mock.Anything).Return("default data", nil)
mockService.On("SaveData", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil)
userService := &UserService{dataService: mockService}
// 複数の異なるIDでテスト
result1, err1 := userService.ProcessUser(100)
result2, err2 := userService.ProcessUser(200)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, "Processed: default data", result1)
assert.Equal(t, "Processed: default data", result2)
mockService.AssertExpectations(t)
}
func TestMockWithCallbacks(t *testing.T) {
mockService := new(MockDataService)
// コールバック関数を使用した動的なレスポンス
mockService.On("GetData", mock.AnythingOfType("int")).Return("", nil).Run(func(args mock.Arguments) {
id := args.Get(0).(int)
// IDに基づいて何らかの処理を実行
assert.True(t, id > 0, "IDは正の値であるべきです")
})
userService := &UserService{dataService: mockService}
_, err := userService.ProcessUser(123)
assert.NoError(t, err)
mockService.AssertExpectations(t)
}
テストスイートの使用
package main
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/stretchr/testify/assert"
)
// テストスイートの定義
type CalculatorTestSuite struct {
suite.Suite
calculator *Calculator
}
// テストスイートの初期化(各テストの前に実行)
func (suite *CalculatorTestSuite) SetupTest() {
suite.calculator = NewCalculator()
}
// テストメソッド(Testで始まる名前)
func (suite *CalculatorTestSuite) TestAddition() {
result := suite.calculator.Add(5, 3)
suite.Equal(8, result, "5 + 3 は 8 になるべきです")
}
func (suite *CalculatorTestSuite) TestSubtraction() {
result := suite.calculator.Subtract(10, 4)
suite.Equal(6, result, "10 - 4 は 6 になるべきです")
}
func (suite *CalculatorTestSuite) TestDivision() {
result, err := suite.calculator.Divide(15, 3)
suite.NoError(err, "有効な除算でエラーが発生すべきではありません")
suite.Equal(5, result, "15 / 3 は 5 になるべきです")
}
func (suite *CalculatorTestSuite) TestDivisionByZero() {
_, err := suite.calculator.Divide(10, 0)
suite.Error(err, "0での除算はエラーになるべきです")
suite.Contains(err.Error(), "division by zero")
}
// スイートを実行するためのテスト関数
func TestCalculatorTestSuite(t *testing.T) {
suite.Run(t, new(CalculatorTestSuite))
}
組み込みアサーションを使用したスイート
type DatabaseTestSuite struct {
suite.Suite
db *sql.DB
userRepo *UserRepository
}
func (suite *DatabaseTestSuite) SetupSuite() {
// スイート全体の初期化(一度だけ実行)
var err error
suite.db, err = sql.Open("sqlite3", ":memory:")
suite.Require().NoError(err, "データベース接続に失敗しました")
// テーブル作成
_, err = suite.db.Exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`)
suite.Require().NoError(err, "テーブル作成に失敗しました")
}
func (suite *DatabaseTestSuite) SetupTest() {
// 各テストの前に実行
suite.userRepo = NewUserRepository(suite.db)
// テストデータのクリーンアップ
_, err := suite.db.Exec("DELETE FROM users")
suite.Require().NoError(err)
}
func (suite *DatabaseTestSuite) TestCreateUser() {
user := &User{
Name: "John Doe",
Email: "[email protected]",
}
err := suite.userRepo.Create(user)
suite.NoError(err)
suite.NotEqual(0, user.ID, "ユーザーIDが設定されるべきです")
}
func (suite *DatabaseTestSuite) TestGetUser() {
// テストデータの挿入
result, err := suite.db.Exec(
"INSERT INTO users (name, email) VALUES (?, ?)",
"Jane Doe", "[email protected]",
)
suite.Require().NoError(err)
id, err := result.LastInsertId()
suite.Require().NoError(err)
// ユーザーの取得
user, err := suite.userRepo.GetByID(int(id))
suite.NoError(err)
suite.NotNil(user)
suite.Equal("Jane Doe", user.Name)
suite.Equal("[email protected]", user.Email)
}
func (suite *DatabaseTestSuite) TearDownSuite() {
// スイート全体のクリーンアップ(一度だけ実行)
if suite.db != nil {
suite.db.Close()
}
}
func TestDatabaseTestSuite(t *testing.T) {
suite.Run(t, new(DatabaseTestSuite))
}
高度なアサーション例
func TestAdvancedAssertions(t *testing.T) {
assert := assert.New(t)
// スライスのテスト
numbers := []int{1, 2, 3, 4, 5}
assert.Len(numbers, 5)
assert.Contains(numbers, 3)
assert.NotContains(numbers, 10)
assert.ElementsMatch([]int{5, 4, 3, 2, 1}, numbers) // 順序を無視した比較
// マップのテスト
userMap := map[string]int{
"alice": 25,
"bob": 30,
"carol": 28,
}
assert.Len(userMap, 3)
assert.Equal(25, userMap["alice"])
assert.NotContains(userMap, "david")
// 構造体のテスト
user1 := User{ID: 1, Name: "Alice", Email: "[email protected]"}
user2 := User{ID: 1, Name: "Alice", Email: "[email protected]"}
user3 := User{ID: 2, Name: "Bob", Email: "[email protected]"}
assert.Equal(user1, user2)
assert.NotEqual(user1, user3)
// JSONとの比較
jsonString := `{"id":1,"name":"Alice","email":"[email protected]"}`
assert.JSONEq(jsonString, `{"name":"Alice","id":1,"email":"[email protected]"}`)
// 正規表現のテスト
email := "[email protected]"
assert.Regexp(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, email)
}