Goにおけるテスト可能なExample

Testable Examples in Go by Andrew Gerrand

はじめに

GodocのExampleは、パッケージのドキュメントとして表示され、かつテストとして実行することで検証されるGoコードの断片です。 ユーザーがそのパッケージのgodocのWebページを訪れて、関連付けられた「Run」ボタンをクリックすることでも実行できます。

パッケージに実行可能なドキュメントを持たせることで、APIが変更されても情報が古くならないことを保証できます。

標準ライブラリには、こういったExampleが数多く含まれています(たとえばstrings パッケージを見てください)。

この記事では、自分自身のExample関数を書く方法を説明します。

Exampleはテストである

Exampleはパッケージのテストスイートの一部としてコンパイルされます(そしてオプションで実行されます)。

典型的なテストと同様に、Exampleはパッケージの _test.go ファイルに置かれる関数です。 しかし通常のテスト関数と異なり、Example関数は引数を取らず、 Test の代わりに Example という単語から始まります。

reverse パッケージGoのexampleリポジトリの一部です。 ここでは、その String 関数を示すExampleを紹介します。

package reverse_test

import (
    "fmt"

    "golang.org/x/example/hello/reverse"
)

func ExampleString() {
    fmt.Println(reverse.String("hello"))
    // Output: olleh
}

このコードは reverse ディレクトリ内の example_test.go に置かれるでしょう。

Goのパッケージドキュメントサーバーであるpkg.go.devは、このExampleをString 関数のドキュメントと並べて表示します。

reverse.String関数のドキュメントに表示されたExample

パッケージのテストスイートを実行すると、これ以上何もしなくてもExample関数が実行されることがわかります。

$ go test -v
=== RUN   TestString
--- PASS: TestString (0.00s)
=== RUN   ExampleString
--- PASS: ExampleString (0.00s)
PASS
ok      golang.org/x/example/hello/reverse  0.209s

Outputコメント

ExampleString 関数が「パスする」とはどういうことでしょうか。

Exampleを実行する際、テストフレームワークは標準出力に書き込まれたデータを取得し、それをExampleの Output: コメントと比較します。 テストの出力がoutputコメントと一致すれば、テストはパスします。

失敗するExampleを見るために、outputコメントの文字列を明らかに間違ったものに変更してみましょう。

func ExampleString() {
    fmt.Println(reverse.String("hello"))
    // Output: golly
}

そしてもう一度テストを実行します。

$ go test
--- FAIL: ExampleString (0.00s)
got:
olleh
want:
golly
FAIL

outputコメントを完全に削除すると

func ExampleString() {
    fmt.Println(reverse.String("hello"))
}

Example関数はコンパイルはされますが、実行はされません。

$ go test -v
=== RUN   TestString
--- PASS: TestString (0.00s)
PASS
ok      golang.org/x/example/hello/reverse  0.110s

outputコメントのないExampleは、ネットワークにアクセスするコードのようにユニットテストとして実行できないコードを示すのに便利です。 それでいて、Exampleが少なくともコンパイルされることは保証されます。

Example関数の命名

Godocは、Example関数をパッケージレベルの識別子に関連付けるために命名規則を使用しています。

func ExampleFoo()     // Foo関数またはFoo型のドキュメントになる
func ExampleBar_Qux() // Bar型のQuxメソッドのドキュメントになる
func Example()        // パッケージ全体のドキュメントになる

この規則に従うことで、godocは ExampleString というExampleを String 関数のドキュメントと並べて表示します。

アンダースコアに続けて小文字のアルファベットで始まるサフィックスを使うことで、1つの識別子に対して複数のExampleを用意できます。 次のExampleはすべて String 関数のドキュメントになります。

func ExampleString()
func ExampleString_second()
func ExampleString_third()

より大きなExample

良いExampleを書くには、関数だけでは足りないことがあります。

たとえばsort パッケージを示すには、 sort.Interface の実装を見せる必要があります。 メソッドは関数本体の中では宣言できないため、Exampleには、Example関数に加えていくらか文脈を含める必要があります。

これを実現するために、「ファイル全体のExample」を使用できます。 ファイル全体のExampleとは、 _test.go で終わるファイルで、ちょうど1つのExample関数を含み、テスト関数やベンチマーク関数を含まず、 かつ少なくとも1つの他のパッケージレベルの宣言を含むものです。このようなExampleを表示する際、godocはファイル全体を表示します。

こちらは sort パッケージにあるファイル全体のExampleです。

package sort_test

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s: %d", p.Name, p.Age)
}

// ByAge は Age フィールドに基づいて []Person に対する sort.Interface を実装します。
type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }

func Example() {
    people := []Person{
        {"Bob", 31},
        {"John", 42},
        {"Michael", 17},
        {"Jenny", 26},
    }

    fmt.Println(people)
    sort.Sort(ByAge(people))
    fmt.Println(people)

    // Output:
    // [Bob: 31 John: 42 Michael: 17 Jenny: 26]
    // [Michael: 17 Jenny: 26 Bob: 31 John: 42]
}

1つのパッケージは複数のファイル全体のExampleを持てますが、1ファイルにつき1つのExampleです。 実際にどうなっているか、sort パッケージのソースコードを見てみてください。

まとめ

GodocのExampleは、コードをドキュメントとして書き、維持するための優れた方法です。 また、ユーザーが土台として使える、編集可能で動作する実行可能なExampleを提供します。ぜひ活用してください!

By Andrew Gerrand