Ginkgo v2 へ移行する

RSpec, Jest ライクな Go のテストフレームワーク Ginkgo が年末に v2.0.0 をリリースしたので個人プロジェクトで試してみた。

v1 から v2 への移行

基本的には公式が v2 の機能紹介兼移行ガイドを公開しているのでそれに従えばいい。

https://onsi.github.io/ginkgo/MIGRATING_TO_V2

なお Describe, It, BeforeEach くらいしか使っていないユースケースではテストコードの変更はほぼ無い。

バージョンアップ

まずはともかくバージョンを上げる。プロジェクトとして依存はもちろん、CLI も忘れずに上げること。

# プロジェクトとしての依存バージョンをアップデートする
$ go get github.com/onsi/ginkgo/v2@v2.0.0

$ go list -m github.com/onsi/ginkgo/v2
github.com/onsi/ginkgo/v2 v2.0.0

# ginkgo CLI をアップデートする(エラーが出る場合)
$ go install github.com/onsi/ginkgo/v2/ginkgo@v2.0.0
# エラーメッセージで指示された場合、以下も追加
$ go get github.com/onsi/ginkgo/v2/ginkgo/labels@v2.0.0
$ go get github.com/onsi/ginkgo/v2/ginkgo/generators@v2.0.0

$ ginkgo version
Ginkgo Version 2.0.0

コード修正

上でインストールしたパッケージ名からわかる通り、v2 で import path が変更されている(Go ではメジャーバージョン毎に import path を切るのを推奨している)。したがって、テストコード中の import 文は全て変える必要があるだろう。

 import (
-       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/ginkgo/v2"

多くの DSL はそのまま使えるので、これだけで殆ど動く場合もある。

筆者は DescribeTable/Entry による Table Driven Test にお世話になっていた。これは github.com/onsi/ginkgo/extensions/table にて提供されていたが、v2 では主要 DSL として昇格したため追加 import は不要になった。したがって、全体では以下の修正が必要となった。

 import (
-       . "github.com/onsi/ginkgo"
-       . "github.com/onsi/ginkgo/extensions/table"
+       . "github.com/onsi/ginkgo/v2"

筆者の場合、以上で全てのテストが動いた。他にも時間測定や非同期テスト、並列テスト、カスタムレポートなどの機能を使っている場合はコード修正が必要になる。コード修正が必要な変更は、その修正方針と共にまとまっている

便利(そう)なところ

DeferCleanup

「テストで必要なリソースを BeforeEach で作成 → AfterEach で削除」というのは鉄板のパターンだ。

Describe("TheResource", func() {
  var r TheResource
  BeforeEach(func() {
    r, _ = CreateTheResource() // エラー処理は省略
  })

  AfterEach(func() {
    if r != nil {
      DestroyTheResource(r)
      r = nil
    }
  })
})

このコード例には主に2つの問題がある*1:

  • コールバック間で r を共有するため、It 内で必要かに関係なくスコープが広くなっている
  • BeforeEach の成否を AfterEach でハンドリングしないといけない

AfterEach を使わずに BeforeEach 内で DeferCleanup を呼び出すことで、これらの問題を解決できる。DeferCleanup に渡したコールバックはテスト終了後に呼び出される。

Describe("TheResource", func() {
  BeforeEach(func() {
    r, err := CreateTheResource()

    if err != nil {
      // エラー処理は省略
      return
    }

    // リソース作成できたときだけクリーンアップ処理を登録
    DeferCleanup(func() error {
      DestroyResource(r)
    })
    // やりたいことが関数呼び出し1つだけなら、関数と渡したい引数を並べるだけでもいい
    DeferCleanup(DestoryTheResource, r)
  })
})

これにより r のスコープは狭くなり、クリーンアップ処理の実行判断も近いところで行えるようになった。この APIdefer でクローズ処理を行う Go のスタイルとも合致している。

DeferCleanupBeforeSuiteBeforeAll などでも利用可能であり、それぞれ適切なタイミングでコールバックを呼び出してくれる。

各種デコレータ

DescribeIt が可変長引数となり、デコレータを付与できるようになった。以下はデコレータを使った例。

// ラベル
Describe("TheResource", Serial, Label("unittest"), func() {
  ...
})

Serial デコレータを付与しているため、上記の suites 内のテストは並列実行されないことが保証される。また Label デコレータによって unittest というラベルが付いている。Ginkgo CLI--label-filter フラグを使うと、ラベルを用いた絞り込みが行える。

$ ginkgo --lable-filter="unittest"

この他に実行順やリトライに関するデコレータを提供している。

気になるところ

デコレータが導入された副作用として、エディタの補完が効きづらくなった。

v1 f:id:autopp:20220103225716p:plain

v2 f:id:autopp:20220103225743p:plain

v1 では Describe のボディに当たる部分を無名関数で補完できるが、v2 ではできない。これはデコレータ導入の影響によりシグネチャが以下のように変わってしまったことに起因すると思われる。

v1

func Describe(text string, body func()) bool

v2

func Describe(text string, args ...interface{}) bool

デコレータが引数に入り込む関係上、引数の型が interface{} 潰れてしまい、補完のしようがないように見える。デコレータがボディの後ろに来ればまだ補完のしようがあるように見えるが、見栄えを優先した形だろうか。

このようなコードには直接現れないような開発体験の増減はなかなか難しい……

まとめ

単純な使い方をしているのであれば、移行は簡単だ。v2 で失われたものもあるが、新機能もなかなか魅力的なので Ginkgo を使っているプロジェクトはちょっとずつ v2 に移行したいところ。

*1:古くからあるリソース管理の問題でもある