GoConvey

GoテストフレームワークBDDWeb UIアサーション行動駆動開発

GoConvey

概要

GoConveyは、Go言語用のBDD(行動駆動開発)スタイルのテストフレームワークです。標準のgo testと完全統合しながら、読みやすいテスト記述、リアルタイムWebUI、豊富なアサーション機能を提供します。Convey関数によるネストした記述により、テストの意図を明確に表現し、開発者体験を大幅に向上させるモダンなテストソリューションです。

詳細

主な特徴

  • BDDスタイル記述: Convey関数によるネストした可読性の高いテスト
  • リアルタイムWebUI: ブラウザでのテスト結果とカバレッジの可視化
  • go test統合: 標準テストツールとの完全互換性
  • 豊富なアサーション: So関数による表現力豊かなアサーション
  • 自動テスト実行: ファイル変更時の自動テスト再実行
  • カラフルコンソール: 読みやすい色分けされたコンソール出力
  • テストコードジェネレーター: 効率的なテストコード生成機能
  • デスクトップ通知: オプションのテスト結果通知

アーキテクチャ

GoConveyは二つの主要コンポーネントで構成されています:

  1. テストフレームワーク: ConveySoによるBDD記述システム
  2. 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)
        })
    })
}