KubernetesからGoのテストを学ぶ
Takashi Yamamoto
Infrastructure Engineer · Leapcell

テストを行う理由
優れたユニットテストは、より洗練されたコード設計につながり、それによってコードの理解度、再利用性、および保守性が向上します。変更を加える際に、プログラム全体を再テストする必要はありません。変更された部分の入出力が一貫していることを確認するだけで、プログラムに問題がないかどうかを迅速に検証できます。
さらに、バグが発生するたびに、そのバグの入力をテストケースとして追加できます。これにより、同じ間違いを繰り返すことはなく、テストを毎回1回実行するだけで、過去の同様の問題が新しい変更によって再発していないかどうかを確認できます。これは、ソフトウェアの品質を大幅に向上させます。
モックを容易にするためのパラメーターとしてのメソッドの受け渡し
Kubernetesのグレースフルシャットダウンロジックでは、ハンドラーパラメーターを直接呼び出す代わりにメソッドとして宣言することで、ハンドラー自体の正確性を気にせずに、flushList
のロジックのみをテストできます。
ただし、gomonkeyのリフレクションを使用して、メソッドの戻り値を直接モックして、同じ効果を実現することもできます。
レースコンディションをテストする必要がある場合は、goルーチンを起動することでそれを行うことができます。
type gracefulTerminationManager struct { rsList graceTerminateRSList } func newGracefulTerminationManager() *gracefulTerminationManager { return &gracefulTerminationManager{ rsList: graceTerminateRSList{ list: make(map[string]*item), }, } } type item struct { VirtualServer string RealServer string } type graceTerminateRSList struct { lock sync.Mutex list map[string]*item } func (g *graceTerminateRSList) flushList(handler func(rsToDelete *item) (bool, error)) bool { g.lock.Lock() defer g.lock.Unlock() success := true for _, rs := range g.list { if ok, err := handler(rs); !ok || err != nil { success = false } } return success } func (g *graceTerminateRSList) add(rs *item) { g.lock.Lock() defer g.lock.Unlock() g.list[rs.RealServer] = rs } func (g *graceTerminateRSList) len() int { g.lock.Lock() defer g.lock.Unlock() return len(g.list) }
ここでは、レースコンディション下でflushList
とadd
をテストする必要があります。
func Test_raceGraceTerminateRSList_flushList(t *testing.T) { manager := newGracefulTerminationManager() go func() { for i := 0; i < 100; i++ { manager.rsList.add(&item{ VirtualServer: "virtualServer", RealServer: fmt.Sprint(i), }) } }() // Wait until a certain number of elements are added before proceeding for manager.rsList.len() < 20 { } // Pass in the handler for mocking success := manager.rsList.flushList(func(rsToDelete *item) (bool, error) { return true, nil }) assert.True(t, success) }
https://github.com/agiledragon/gomonkeyを使用してプログラムの一部をモックすると、テスト対象のメソッドを外部呼び出しの影響から分離できます。
プライベートメソッドをスタブする必要がある場合は、より新しいバージョンのgomonkeyを使用すると、テストする必要のあるメソッドに集中できます。
テストファイル内でいくつかの統合テストを行う必要がある場合、データベースやキャッシュなど、最初に多くのリソースを初期化する必要があるという問題に直面する可能性があります。この場合、各モジュールのディレクトリにこれらのリソースを初期化するためのメソッドを追加できます。次に例を示します。
func InitTestSuite(opts ...TestSuiteConfigOpt) { config := &TestSuiteConfig{} for _, opt := range opts { opt(config) } dsn := config.GetDSN() err := NewOrmClient(&Config{ Config: &gorm.Config{ //Logger: logger.Default.LogMode(logger.Info), }, SourceConfig: &SourceDBConfig{}, Dial: postgres.Open(dsn), }) }
次に、それらを使用する必要があるテストファイルで、TestMain
メソッドを介して初期化します。
これのもう1つの利点は、モジュールが完全に分離されているかどうかを早期に発見できることです。 たとえば、テストスイートの設定時に多くのコンポーネントを初期化している場合は、モジュールの設計が正しいか、または必要かどうかを再検討する価値があります。
並行性の問題をテストする方法
並行プログラムのテストを記述する方法は?
分散システムでは、最も一般的な問題は多数の競合状態です。 多くのケースはごくわずかな確率でしか発生しませんが、発生すると深刻な事故につながる可能性があります。 したがって、可能な限り同時実行レースのシナリオをシミュレートし、すべての操作が完了した後に結果を確認する必要があります。 ただし、1回の実行でテストが正常に合格することがあるため、複数回実行した後でも結果が一貫していることを確認する必要があります。 これには、次のサンプルコードに示すように、コードを複数回実行する必要があります。
var ( counter int ) func increment() { counter++ } func TestIncrement(t *testing.T) { count := 100 var wg sync.WaitGroup for i := 0; i < count; i++ { wg.Add(1) go func() { increment() wg.Done() }() } assert.Equal(t, count, counter) }
メソッドを操作するために複数のgoroutineを起動すると、結果が期待どおりにならない場合があります。 この時点で、コードを確認して変更する必要があります。
TDD(テスト駆動開発)
テストを記述した後、テストに合格するために必要な最小限のコードのみを記述します。 ステートマシンコードの実装を例にとると、まずメソッドを定義します。 読みやすくするために、簡単な実装を次に示します。
func GetOrder(orderId string) Order { return Order{} } func UpdateOrder(originalOrder, order Order) error { return nil } func UpdateOrderStateByEvent(ctx context.Context, orderId string, event Event) (err error) { order := GetOrder(orderId) stateMap, ok := orderEventStateMap[event] if !ok { return errors.New("event not exists") } if !stateMap.currentStateSet.Contains(order.OrderState) { return errors.New("current OrderState error") } updateOrder := Order{ OrderId: order.OrderId, OrderState: order.OrderState, } err = UpdateOrder(order, updateOrder) if err != nil { return err } return nil }
次に、UpdateOrderStateByEvent
をテストします。 ユニットテストは、このメソッドを単独でテストすることを目的としていることを明確にする必要があります。 他のメソッドはgomonkeyでモックして、テストの再現性を確保できます。
func TestOrderStateByEvent(t *testing.T) { type args struct { ctx context.Context orderId string event Event } tests := []struct { name string args args wantErr error initStubs func() (reset func()) }{ { name: "", args: args{ ctx: context.Background(), orderId: "orderId1", event: onHoldEvent, }, wantErr: nil, initStubs: func() (reset func()) { patches := gomonkey.ApplyFunc(GetOrder, func(orderId string) Order { return Order{ OrderId: orderId, OrderState: delivering, } }) return func() { patches.Reset() } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 1. Mock the required methods reset := tt.initStubs() defer reset() // 2. Call the method to be tested err := UpdateOrderStateByEvent(tt.args.ctx, tt.args.orderId, tt.args.event) assert.Nil(t, err) }) } }
テスト駆動開発の概念は、1990年代初頭に提唱されました。 この例ではGoを使用していますが、TDDは最初に他の言語で実践されました。 著者はテストを記述し、次にテストに合格するために必要な最小限のコードを記述し、これらのステップを交互に繰り返します。 プログラムが完了すると、すでにテスト可能な状態になっています。
テストコードから始めることで、実際のコードを記述した後に大幅な変更を加えたくないという気持ちを回避できます。 これにより、関数が長くなりすぎるのを防ぎ、将来の変更や再テストをはるかに管理しやすくします。 ビジネスロジックを開発する場合、ビジネスロジックを事前に分解し、コンポーネントをグルーコードと組み合わせると、すべてのコードを一度に記述して後でテストするよりもバグが発生する可能性が低くなります。
テストの記述に時間がかかりすぎると考える人もいるかもしれませんが、ツールを使用してテスト効率を向上させることができます。 たとえば、IDEを使用してテストスケルトンを生成します。AIコパイロットの登場により、テストケースでの反復作業を手動で行う必要はなくなりました。 現在、ケースを1つ記述し、テストメソッドのロジックを実装するだけで、AIは多くのエッジケースの例を生成できます。これは、自分で考えるよりも徹底している場合もあります。 さらに、メソッド名が適切に選択されている限り、生成されたサンプルは非常に使いやすくなっています。 AIによって生成されたテストケースが適切でない場合は、メソッド名自体に問題がないかどうかを検討し、コードを継続的に改善できます。
結論
最初にエレガントなコードを記述する必要はありませんが、常に優れたコードを記述し、自分の作業を継続的に反省し、ツールを使用して自分自身を絶えず改善することを目指すべきです。 そうすれば、私たちが生み出す結果もより優れたものになるでしょう。
Leapcellをご紹介します。Goプロジェクトをホストするための最適な選択肢です。
Leapcellは、Webホスティング、非同期タスク、およびRedis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い、リクエストや料金は発生しません。
比類のない費用対効果
- アイドル料金なしで、従量課金制です。
- 例:25ドルで、平均応答時間60ミリ秒で694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察を得るためのリアルタイムのメトリックとロギング。
簡単なスケーラビリティと高パフォーマンス
- 簡単な同時実行処理のための自動スケーリング。
- 運用上のオーバーヘッドはゼロで、構築に集中できます。
詳細については、ドキュメントをご覧ください。
Xでフォローしてください:@LeapcellHQ