2025/06/02
はじめに
Goのドキュメントは、Godocを使って記述することで、OSSとして公開した時に pkg.go.devに自動的に公開される。
Godocでは、func ExampleFoor()
のようなコードを書くことで、ExampleをTestableに記述することができるが、これは、ドキュメントに含める範囲が広くなってしまったり、アサーションがstdoutであることなど、なにかと制約が多かった。
そこで、Testのの中にアノテーションコメントを入れ、そこから関数定義のGoDocCommentsに転記するツールを go generateで実行することで、Testの一部をExampleとして利用できるようにした。
ツール
generatorは、github.com/ysuzuki19/robustruct/tree/main/cmd/gen/testdocgen
に配置した。
ジェネレータのREADMEは、こちらに配置。
導入のサンプルは、こちら
ツールのセットアップ
Goのバージョンに合わせてツールを入れる。
tools.go
を使うケースでは、以下のように記載する。
//go:build tools
// +build tools
package tools
import (
_ "github.com/ysuzuki19/robustruct/cmd/gen/testdocgen"
)
$ go mod tidy
コード上の設定
コード内のどこかに(だいたい最上部か最下部?)に以下のコメントを追加する。
//go:generate go run github.com/ysuzuki19/robustruct/cmd/gen/testdocgen -file=$GOFILE
こんな感じで書く。
//go:generate go run github.com/ysuzuki19/robustruct/cmd/gen/testdocgen -file=$GOFILE
package lib
type User struct {
Name string
Age int
}
func NewUser(name string, age int) *User {
return &User{
Name: name,
Age: age,
}
}
func (u User) GetName() string {
return u.Name
}
テスト上の設定
上の設定をしたコードに対して、.go
を_test.go
としたテストファイルのみジェネレータの検出対象となる。
以下がtestdocgenのための埋め込み用のコメント例。
package lib_test
import (
"testing"
"github.com/stretchr/testify/require"
"example/for_testdocgen/lib"
)
func TestNewUser(t *testing.T) {
require := require.New(t)
// testdoc begin NewUser
u := lib.NewUser("Alice", 30)
require.Equal(lib.User{
Name: "Alice",
Age: 30,
}, *u)
// testdoc end
}
func TestGetName(t *testing.T) {
require := require.New(t)
// testdoc begin User.GetName
u := lib.NewUser("Alice", 30)
require.Equal("Alice", u.GetName())
// testdoc end
}
// testdoc
から始まるコメントがツールの設定行となり、begin
/end
で囲まれた部分が、Exampleとして転記される。
転記先は、begin
のあとに続けて、
FuncName
StructName.MethodName
のどちらかの形式を取る。
転記先が見つからない場合は testdocgen
はエラー終了する。
ジェネレータの実行
$ go generate ./...
実行結果
上のサンプルを実行すると、 lib.go
は以下のようになる。
//go:generate go run github.com/ysuzuki19/robustruct/cmd/gen/testdocgen -file=$GOFILE
package lib
type User struct {
Name string
Age int
}
// Example:
//
// u := lib.NewUser("Alice", 30)
// require.Equal(lib.User{
// Name: "Alice",
// Age: 30,
// }, *u)
func NewUser(name string, age int) *User {
return &User{
Name: name,
Age: age,
}
}
// Example:
//
// u := lib.NewUser("Alice", 30)
// require.Equal("Alice", u.GetName())
func (u User) GetName() string {
return u.Name
}
// Example:
が書かれていないときは、既存のコメントの下に追加され、// Example:
以降のコメントは、次の実行以降は自動で上書きされる。(手書きがあったとしてもそれは除去される)コメントがそもそもない場合は、// Example:
から追加される。
おわりに
このツールを使うことで、Exampleの更新漏れ、特に、コンパイルエラーや実装との乖離のリスクを減らせるようになった(かもしれない)。