GoConvey
GoConvey
概要
GoConveyは、Go言語用のBDD(行動駆動開発)スタイルのテストフレームワークです。標準のgo testと完全統合しながら、読みやすいテスト記述、リアルタイムWebUI、豊富なアサーション機能を提供します。Convey関数によるネストした記述により、テストの意図を明確に表現し、開発者体験を大幅に向上させるモダンなテストソリューションです。
詳細
主な特徴
- BDDスタイル記述:
Convey関数によるネストした可読性の高いテスト - リアルタイムWebUI: ブラウザでのテスト結果とカバレッジの可視化
- go test統合: 標準テストツールとの完全互換性
- 豊富なアサーション:
So関数による表現力豊かなアサーション - 自動テスト実行: ファイル変更時の自動テスト再実行
- カラフルコンソール: 読みやすい色分けされたコンソール出力
- テストコードジェネレーター: 効率的なテストコード生成機能
- デスクトップ通知: オプションのテスト結果通知
アーキテクチャ
GoConveyは二つの主要コンポーネントで構成されています:
- テストフレームワーク:
ConveyとSoによるBDD記述システム - WebUI: リアルタイムテスト監視とレポート機能
核心機能
- Convey関数: テストスコープの宣言と階層化
- So関数: アサーションの実行
- Reset関数: テストクリーンアップの登録
- FailureMode: テスト失敗時の動作制御
メリット・デメリット
メリット
- 高い可読性: 自然言語に近いテスト記述
- 優れた開発体験: WebUIによる直感的なフィードバック
- 標準互換性: 既存のGoテストとの併用可能
- 効率的なワークフロー: 自動テスト実行で開発効率向上
- 包括的レポート: テストカバレッジとパフォーマンス分析
- 学習コストの低さ: 直感的なAPI設計
デメリット
- 外部依存: 標準ライブラリ以外の依存関係
- パフォーマンスオーバーヘッド: ネストした構造による軽微な実行コスト
- WebUI依存: フル機能にはWebブラウザが必要
- 複雑なテストでの冗長性: シンプルなテストには過剰な場合がある
参考ページ
書き方の例
基本的なテスト
package main
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
Convey("Given two integers", t, func() {
a := 2
b := 3
Convey("When they are added together", func() {
result := Add(a, b)
Convey("The result should be their sum", func() {
So(result, ShouldEqual, 5)
})
})
})
}
ネストしたテスト構造
func TestCalculator(t *testing.T) {
Convey("Calculator functionality", t, func() {
Convey("Addition operations", func() {
Convey("Adding positive numbers", func() {
So(Add(2, 3), ShouldEqual, 5)
So(Add(10, 15), ShouldEqual, 25)
})
Convey("Adding negative numbers", func() {
So(Add(-2, -3), ShouldEqual, -5)
So(Add(-10, 5), ShouldEqual, -5)
})
Convey("Adding zero", func() {
So(Add(0, 5), ShouldEqual, 5)
So(Add(5, 0), ShouldEqual, 5)
})
})
Convey("Multiplication operations", func() {
Convey("Multiplying positive numbers", func() {
So(Multiply(2, 3), ShouldEqual, 6)
So(Multiply(4, 5), ShouldEqual, 20)
})
Convey("Multiplying by zero", func() {
So(Multiply(5, 0), ShouldEqual, 0)
So(Multiply(0, 10), ShouldEqual, 0)
})
})
})
}
豊富なアサーション
func TestAssertions(t *testing.T) {
Convey("Various assertion types", t, func() {
Convey("Equality assertions", func() {
So(42, ShouldEqual, 42)
So(3.14, ShouldAlmostEqual, 3.14159, 0.01)
So("hello", ShouldEqual, "hello")
})
Convey("Boolean assertions", func() {
So(true, ShouldBeTrue)
So(false, ShouldBeFalse)
So(1 > 0, ShouldBeTrue)
})
Convey("Nil assertions", func() {
var ptr *int
So(ptr, ShouldBeNil)
value := 42
ptr = &value
So(ptr, ShouldNotBeNil)
})
Convey("Collection assertions", func() {
slice := []int{1, 2, 3, 4, 5}
So(slice, ShouldContain, 3)
So(slice, ShouldHaveLength, 5)
So(slice, ShouldNotBeEmpty)
})
Convey("Numeric assertions", func() {
So(10, ShouldBeGreaterThan, 5)
So(3, ShouldBeLessThan, 10)
So(5, ShouldBeBetween, 1, 10)
})
Convey("String assertions", func() {
So("hello world", ShouldContainSubstring, "world")
So("hello", ShouldStartWith, "hel")
So("world", ShouldEndWith, "rld")
So("[email protected]", ShouldMatchRegex, `\w+@\w+\.\w+`)
})
Convey("Type assertions", func() {
So(42, ShouldHaveSameTypeAs, 0)
So("string", ShouldHaveSameTypeAs, "")
})
})
}
セットアップとクリーンアップ
func TestWithSetup(t *testing.T) {
Convey("Database operations", t, func() {
// セットアップ
db := setupTestDatabase()
user := createTestUser()
// クリーンアップ関数の登録
Reset(func() {
cleanupTestUser(user)
cleanupTestDatabase(db)
})
Convey("Creating a user", func() {
err := db.CreateUser(user)
So(err, ShouldBeNil)
Convey("Should find the user by ID", func() {
foundUser, err := db.FindUserByID(user.ID)
So(err, ShouldBeNil)
So(foundUser.Name, ShouldEqual, user.Name)
})
Convey("Should find the user by email", func() {
foundUser, err := db.FindUserByEmail(user.Email)
So(err, ShouldBeNil)
So(foundUser.ID, ShouldEqual, user.ID)
})
})
Convey("Updating a user", func() {
// 既存ユーザーの作成
db.CreateUser(user)
Convey("Should update user name", func() {
newName := "Updated Name"
err := db.UpdateUserName(user.ID, newName)
So(err, ShouldBeNil)
updatedUser, _ := db.FindUserByID(user.ID)
So(updatedUser.Name, ShouldEqual, newName)
})
})
})
}
カスタムアサーション
// カスタムアサーションの作成
func ShouldBeValidEmail(actual interface{}, expected ...interface{}) string {
email, ok := actual.(string)
if !ok {
return "Expected string type for email validation"
}
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(email) {
return fmt.Sprintf("Expected '%s' to be a valid email address", email)
}
return "" // 成功
}
func TestCustomAssertion(t *testing.T) {
Convey("Email validation", t, func() {
Convey("Valid emails should pass", func() {
So("[email protected]", ShouldBeValidEmail)
So("[email protected]", ShouldBeValidEmail)
})
Convey("Invalid emails should fail", func() {
So("invalid-email", ShouldNotSatisfy, ShouldBeValidEmail)
So("@domain.com", ShouldNotSatisfy, ShouldBeValidEmail)
})
})
}
エラーハンドリング
func TestErrorHandling(t *testing.T) {
Convey("Error handling scenarios", t, func() {
Convey("Function that should return an error", func() {
result, err := functionThatShouldFail()
So(err, ShouldNotBeNil)
So(result, ShouldBeNil)
So(err.Error(), ShouldContainSubstring, "expected error message")
})
Convey("Function that should not return an error", func() {
result, err := functionThatShouldSucceed()
So(err, ShouldBeNil)
So(result, ShouldNotBeNil)
})
Convey("Specific error types", func() {
_, err := functionWithSpecificError()
So(err, ShouldHaveSameTypeAs, &CustomError{})
So(err, ShouldImplement, (*error)(nil))
})
})
}
高度なテスト技法
func TestAdvancedFeatures(t *testing.T) {
Convey("Advanced GoConvey features", t, func() {
// テストのスキップ
SkipConvey("This test is skipped", func() {
So(1, ShouldEqual, 2) // 実行されない
})
// フォーカステスト(他のテストをスキップ)
FocusConvey("Only this test runs", func() {
So(true, ShouldBeTrue)
})
Convey("Failure modes", func() {
// 失敗時の動作を制御
Convey("Continue on failure", func(c C) {
c.Convey("First assertion", FailureContinues, func() {
So(1, ShouldEqual, 2) // 失敗するが続行
})
c.Convey("Second assertion", func() {
So(true, ShouldBeTrue) // 実行される
})
})
})
Convey("Using context", func() {
ctx := context.WithValue(context.Background(), "key", "value")
So(ctx.Value("key"), ShouldEqual, "value")
So(ctx.Value("missing"), ShouldBeNil)
})
})
}