Ginkgo
単体テストツール
Ginkgo
概要
Ginkgoは、Go言語用の成熟したBDD(行動駆動開発)テストフレームワークです。表現力豊かな仕様を書くために設計されており、Go の標準 testing パッケージの上に構築され、Gomega マッチャーライブラリと組み合わせることで真価を発揮します。単体テスト、統合テスト、受け入れテスト、パフォーマンステストなど、幅広いテストコンテキストで活用できる汎用的なテストフレームワークです。
詳細
Ginkgoは、Go標準のtestingパッケージと共存できるよう設計されており、既存のGoテストと混在させることができます。現在はV2が主流で、V1はサポート終了となっています。
Ginkgoの主な特徴:
- 表現力豊かなDSL:
Describe、Context、When、It等のネストしたコンテナによる整理 - 強力なセットアップ機能:
BeforeEach、AfterEach、BeforeSuite、AfterSuiteによる柔軟なセットアップ - 並列テスト実行: 再現可能なランダム順序とスペック並列化の高度なサポート
- コマンドラインツール:
ginkgoコマンドによる生成、実行、フィルタリング、プロファイリング - 自動ウォッチ機能:
ginkgo watchによる変更検知と自動実行 - Gomega統合: 豊富で成熟したアサーションとマッチャーファミリー
- 同期・非同期アサーション: 同期と非同期のアサーションの混在が可能
- フォーカス・ペンディング機能:
Fプレフィックス(FIt、FDescribe)によるフォーカステスト - タイムアウト管理: 独自のタイムアウト管理とグレースフルな割り込み処理
Ginkgoの哲学は「独立したスペック」にあり、これによりランダム化、フィルタリング、並列化が可能になります。
メリット・デメリット
メリット
- 高度な並列実行: 大規模テストスイートで大幅な時間短縮を実現
- 優れた可読性: 自然言語に近いBDD構文でテストの意図が明確
- 豊富なツール群: 包括的なコマンドラインツールとフィーチャー
- Gomega統合: 強力なマッチャーライブラリとの完璧な統合
- 標準ライブラリとの共存: 既存のGoテストとシームレスに統合
- 柔軟なテスト構成: フォーカス、ペンディング、ラベルによる柔軟なテスト制御
- 成熟度: 長年の開発により安定し、多くのプロジェクトで実績がある
- 活発な開発: 継続的な機能追加とバグ修正
デメリット
- 学習コスト: DSL特有の概念と記法の理解が必要
- Go専用: 他の言語では使用不可
- 設定の複雑さ: 大規模プロジェクトでは設定が複雑になる場合がある
- オーバーヘッド: 小規模なテストでは標準testingパッケージより重い
- 依存関係: Gomegaなどの外部ライブラリへの依存
参考ページ
使い方の例
基本セットアップ
インストール
# Ginkgo CLI ツールのインストール
go install github.com/onsi/ginkgo/v2/ginkgo@latest
# Gomega マッチャーライブラリのインストール
go get github.com/onsi/gomega@latest
プロジェクトの初期化
# テストスイートの初期化
ginkgo bootstrap
# 新しいテストファイルの生成
ginkgo generate calculator
基本的なテストスイート
// calculator_suite_test.go
package calculator_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestCalculator(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Calculator Suite")
}
基本的なテスト構造
describe、context、it の使用
// calculator_test.go
package calculator_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"myproject/calculator"
)
var _ = Describe("Calculator", func() {
var calc *calculator.Calculator
BeforeEach(func() {
calc = calculator.New()
})
Describe("Add method", func() {
Context("when adding positive numbers", func() {
It("should return the correct sum", func() {
result := calc.Add(2, 3)
Expect(result).To(Equal(5))
})
It("should handle multiple numbers", func() {
result := calc.Add(1, 2, 3, 4)
Expect(result).To(Equal(10))
})
})
Context("when adding negative numbers", func() {
It("should return the correct negative sum", func() {
result := calc.Add(-2, -3)
Expect(result).To(Equal(-5))
})
})
Context("when adding zero", func() {
It("should return the other number", func() {
Expect(calc.Add(5, 0)).To(Equal(5))
Expect(calc.Add(0, 7)).To(Equal(7))
})
})
})
Describe("Divide method", func() {
Context("when dividing by zero", func() {
It("should return an error", func() {
_, err := calc.Divide(10, 0)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("division by zero"))
})
})
Context("when dividing valid numbers", func() {
It("should return the correct quotient", func() {
result, err := calc.Divide(10, 2)
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(5.0))
})
})
})
})
セットアップとティアダウン
テストライフサイクル管理
var _ = Describe("Database Operations", func() {
var db *database.Connection
var testUser *models.User
// スイート全体のセットアップ(一度だけ実行)
BeforeSuite(func() {
db = database.Connect("test_database")
Expect(db).NotTo(BeNil())
})
// スイート全体のティアダウン(一度だけ実行)
AfterSuite(func() {
db.Close()
})
// 各spec実行前(外側から内側の順序)
BeforeEach(func() {
testUser = &models.User{
Name: "Test User",
Email: "[email protected]",
}
})
// JustBeforeEach(BeforeEachの後、Itの直前)
JustBeforeEach(func() {
err := db.Save(testUser)
Expect(err).NotTo(HaveOccurred())
})
// 各spec実行後(内側から外側の順序)
AfterEach(func() {
db.DeleteUser(testUser.ID)
})
// クリーンアップの登録
BeforeEach(func() {
DeferCleanup(func() {
// この関数は現在のspecまたはsuiteの後に実行される
cleanupTempFiles()
})
})
Describe("User Creation", func() {
It("should create a user successfully", func() {
users, err := db.GetAllUsers()
Expect(err).NotTo(HaveOccurred())
Expect(users).To(HaveLen(1))
Expect(users[0].Name).To(Equal("Test User"))
})
})
})
Gomega マッチャー
基本的なマッチャー
var _ = Describe("Gomega Matchers", func() {
It("demonstrates basic matchers", func() {
// 等価性
Expect(42).To(Equal(42))
Expect("hello").To(Equal("hello"))
// 真偽値
Expect(true).To(BeTrue())
Expect(false).To(BeFalse())
Expect(nil).To(BeNil())
// 数値比較
Expect(10).To(BeNumerically(">", 5))
Expect(10).To(BeNumerically(">=", 10))
Expect(5).To(BeNumerically("<", 10))
Expect(3.14).To(BeNumerically("~", 3.1, 0.1))
})
It("demonstrates string matchers", func() {
text := "Hello, Ginkgo World!"
Expect(text).To(ContainSubstring("Ginkgo"))
Expect(text).To(HavePrefix("Hello"))
Expect(text).To(HaveSuffix("World!"))
Expect(text).To(MatchRegexp(`Hello.*World`))
Expect(text).NotTo(BeEmpty())
})
It("demonstrates collection matchers", func() {
slice := []string{"apple", "banana", "cherry"}
Expect(slice).To(HaveLen(3))
Expect(slice).To(ContainElement("banana"))
Expect(slice).To(ContainElements("apple", "cherry"))
Expect(slice).To(ConsistOf("cherry", "apple", "banana"))
Expect(slice).NotTo(BeEmpty())
// マップ
m := map[string]int{"a": 1, "b": 2}
Expect(m).To(HaveKey("a"))
Expect(m).To(HaveKeyWithValue("a", 1))
})
})
非同期マッチャー
var _ = Describe("Asynchronous Testing", func() {
It("tests asynchronous operations", func() {
counter := 0
// Eventuallyは条件が満たされるまで待機
go func() {
time.Sleep(100 * time.Millisecond)
counter = 5
}()
Eventually(func() int {
return counter
}).Should(Equal(5))
// Consistentlyは期間中条件が維持されることを確認
Consistently(func() int {
return counter
}).Should(Equal(5))
})
It("tests channel operations", func() {
ch := make(chan string, 1)
go func() {
time.Sleep(50 * time.Millisecond)
ch <- "message"
}()
Eventually(ch).Should(Receive(Equal("message")))
})
})
並列テストとフォーカス
並列実行
// テストを並列で実行
var _ = Describe("Parallel Tests", func() {
It("runs in parallel", func() {
// 並列プロセスのインデックスを取得
processIndex := GinkgoParallelProcess()
// プロセス間でのリソース分散
testData := getTestDataForProcess(processIndex)
// テスト実行
result := processTestData(testData)
Expect(result).To(BeTrue())
})
})
// 並列プロセス間での同期セットアップ
var _ = SynchronizedBeforeSuite(func() []byte {
// 1つのプロセスでのみ実行(重いセットアップ作業)
setupDatabase()
return []byte("setup-complete")
}, func(data []byte) {
// 全プロセスで実行(軽量な初期化)
initializeClient()
})
var _ = SynchronizedAfterSuite(func() {
// 全プロセスで実行
cleanupClient()
}, func() {
// 1つのプロセスでのみ実行
teardownDatabase()
})
フォーカスとペンディング
var _ = Describe("Focus and Pending", func() {
// Fプレフィックスを付けるとこのテストのみ実行される
FDescribe("Focused describe block", func() {
It("will run", func() {
Expect(true).To(BeTrue())
})
})
Describe("Normal describe block", func() {
FIt("focused test", func() {
Expect(true).To(BeTrue())
})
It("normal test", func() {
Expect(true).To(BeTrue())
})
})
// Pプレフィックスまたは本体を空にするとペンディング
PIt("pending test", func() {
// 実装予定
})
It("another pending test") // 本体なしでペンディング
})
オーダード(順序保証)テスト
順序保証が必要なテスト
var _ = Describe("Integration Test", Ordered, func() {
var server *testServer
var client *httpClient
BeforeAll(func() {
server = startTestServer()
client = createClient(server.URL)
})
AfterAll(func() {
server.Stop()
})
It("should create a user", func() {
user := &User{Name: "John", Email: "[email protected]"}
err := client.CreateUser(user)
Expect(err).NotTo(HaveOccurred())
})
It("should get the created user", func() {
user, err := client.GetUser("[email protected]")
Expect(err).NotTo(HaveOccurred())
Expect(user.Name).To(Equal("John"))
})
It("should update the user", func() {
err := client.UpdateUser("[email protected]", &User{Name: "John Doe"})
Expect(err).NotTo(HaveOccurred())
})
It("should delete the user", func() {
err := client.DeleteUser("[email protected]")
Expect(err).NotTo(HaveOccurred())
})
})
ラベルとフィルタリング
ラベルによるテスト分類
var _ = Describe("API Tests", func() {
It("tests basic functionality", Label("smoke", "api"), func() {
// スモークテスト
Expect(apiClient.Ping()).To(Succeed())
})
It("tests advanced features", Label("integration", "api"), func() {
// 統合テスト
result := apiClient.ComplexOperation()
Expect(result).To(BeTrue())
})
It("tests performance", Label("performance", "slow"), func() {
// パフォーマンステスト
start := time.Now()
apiClient.HeavyOperation()
duration := time.Since(start)
Expect(duration).To(BeNumerically("<", time.Second))
})
})
モックとスタブ
Ginkgoでのモック使用
var _ = Describe("Service Tests", func() {
var (
mockRepo *MockRepository
service *UserService
)
BeforeEach(func() {
mockRepo = NewMockRepository()
service = NewUserService(mockRepo)
})
Describe("GetUser", func() {
Context("when user exists", func() {
BeforeEach(func() {
user := &User{ID: 1, Name: "John"}
mockRepo.EXPECT().FindByID(1).Return(user, nil)
})
It("should return the user", func() {
user, err := service.GetUser(1)
Expect(err).NotTo(HaveOccurred())
Expect(user.Name).To(Equal("John"))
})
})
Context("when user does not exist", func() {
BeforeEach(func() {
mockRepo.EXPECT().FindByID(999).Return(nil, errors.New("not found"))
})
It("should return an error", func() {
_, err := service.GetUser(999)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))
})
})
})
})
エラーハンドリングと復旧
ゴルーチンでのエラーハンドリング
var _ = Describe("Goroutine Error Handling", func() {
It("handles panics in goroutines", func() {
done := make(chan bool)
go func() {
defer GinkgoRecover() // 必須:ゴルーチンでのパニックをキャッチ
// 何かの処理
riskyOperation()
Expect(someCondition).To(BeTrue()) // 失敗時にパニックが発生
close(done)
}()
Eventually(done).Should(BeClosed())
})
})
コマンドライン操作
Ginkgoコマンドの使用
# 基本実行
ginkgo
# 並列実行
ginkgo -p
# ランダム化
ginkgo --randomize-all
# 特定のシードでの実行
ginkgo --seed=1234
# フォーカス実行
ginkgo --focus="Calculator.*Add"
# ラベルフィルタリング
ginkgo --label-filter="smoke && !slow"
# 監視モード
ginkgo watch
# カバレッジ付き実行
ginkgo --cover
# より詳細な出力
ginkgo -v
# 特定のディレクトリのテスト
ginkgo ./pkg/calculator
# テストバイナリのビルド
ginkgo build
# プロファイリング
ginkgo --cpuprofile=cpu.prof --memprofile=mem.prof
カスタムマッチャー
独自マッチャーの作成
// custom_matchers.go
package matchers
import (
"fmt"
"github.com/onsi/gomega/types"
)
func BeEvenNumber() types.GomegaMatcher {
return &evenNumberMatcher{}
}
type evenNumberMatcher struct{}
func (matcher *evenNumberMatcher) Match(actual interface{}) (success bool, err error) {
number, ok := actual.(int)
if !ok {
return false, fmt.Errorf("BeEvenNumber matcher expects an int")
}
return number%2 == 0, nil
}
func (matcher *evenNumberMatcher) FailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("Expected\n\t%v\nto be an even number", actual)
}
func (matcher *evenNumberMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("Expected\n\t%v\nnot to be an even number", actual)
}
// 使用例
var _ = Describe("Custom Matchers", func() {
It("uses custom even number matcher", func() {
Expect(4).To(BeEvenNumber())
Expect(3).NotTo(BeEvenNumber())
})
})