The Go Blog

巻頭記事

ロブ・パイク
2013年12月2日

はじめに

Goはプロジェクトの当初からツールを念頭に置いて設計されました。これらのツールには、ドキュメント表示ツールgodoc、コード書式設定ツールgofmt、APIリライタgofixなど、Goテクノロジーの最も象徴的なものも含まれます。おそらく最も重要なのは、ビルド仕様としてソースコード以外の何も使用せずに、Goプログラムを自動的にインストール、ビルド、およびテストするプログラムであるgoコマンドです。

Go 1.2のリリースでは、テストカバレッジの新しいツールが導入されました。これは、カバレッジ統計の生成方法に通常とは異なるアプローチを採用しており、godocとその仲間によって確立されたテクノロジーを基盤としています。

ツールのサポート

まず、背景から説明します。言語が良いツールをサポートするとはどういう意味でしょうか?それは、言語が優れたツールを簡単に記述できるようにし、そのエコシステムがあらゆる種類のツールの構築をサポートすることを意味します。

Goには、ツールに適した多くの特性があります。まず、Goには解析しやすい規則的な構文があります。文法は、複雑なメカニズムを必要とする特殊なケースがないことを目指しています。

Goは、可能な限り語彙的および構文的構造を使用して、セマンティックプロパティを理解しやすくしています。例としては、エクスポートされた名前を定義するための大文字の使用や、Cの伝統を持つ他の言語と比較して大幅に簡素化されたスコープ規則などがあります。

最後に、標準ライブラリには、Goソースコードを字句解析および構文解析するための本番品質のパッケージが付属しています。また、より珍しいことですが、Go構文ツリーをきれいに整形出力するための本番品質のパッケージも含まれています。

これらのパッケージは組み合わされてgofmtツールの核を形成しますが、特にプリティプリンタは注目に値します。任意のGo構文ツリーを受け取り、標準形式の人間が読める正しいコードを出力できるため、構文ツリーを変換し、変更されたが正しく読みやすいコードを出力するツールを構築する可能性を生み出します。

その一例がgofixツールです。これは、新しい言語機能や更新されたライブラリを使用するようにコードの書き換えを自動化します。Gofixにより、Go 1.0への準備段階で言語とライブラリに根本的な変更を加えることができ、ユーザーがツールを実行するだけでソースを最新バージョンに更新できるという確信がありました。

Google社内では、gofixを使用して、他の使用している言語ではほとんど考えられないような大規模なコードリポジトリで抜本的な変更を行ってきました。もはや、一部のAPIの複数のバージョンをサポートする必要はありません。gofixを使用して、会社全体を一度の操作で更新できます。

もちろん、これらのパッケージが実現するのは、これら大規模なツールだけではありません。たとえば、IDEプラグインのようなより控えめなプログラムを簡単に記述することもできます。これらすべての項目は互いに連携し、多くのタスクを自動化することでGo環境をより生産的にします。

テストカバレッジ

テストカバレッジとは、パッケージのテストを実行することで、そのパッケージのコードのどれだけの部分が実行されるかを記述する用語です。テストスイートの実行によってパッケージのソースステートメントの80%が実行された場合、テストカバレッジは80%であると言います。

Go 1.2でテストカバレッジを提供するプログラムは、Goエコシステムのツールサポートを活用する最新のものです。

テストカバレッジを計算する通常の​​方法は、バイナリをインストルメントすることです。たとえば、GNU gcovプログラムは、バイナリによって実行されるブランチにブレークポイントを設定します。各ブランチが実行されると、ブレークポイントが解除され、ブランチのターゲットステートメントは「カバレッジ済み」とマークされます。

このアプローチは成功しており、広く使用されています。Goの初期のテストカバレッジツールも同じ方法で機能しました。しかし、問題があります。バイナリの実行分析は困難であるため、実装が難しいのです。また、実行トレースをソースコードに確実に紐付ける方法も必要ですが、ソースレベルデバッガのユーザーが証言するように、これも難しい場合があります。そこには、不正確なデバッグ情報や、インライン化された関数などの問題が分析を複雑にするなどの問題があります。最も重要なことは、このアプローチは移植性が非常に低いことです。デバッグサポートはシステムによって大きく異なるため、アーキテクチャごと、そしてある程度はオペレーティングシステムごとに新たに作成する必要があります。

しかし、それは機能します。たとえば、gccgoのユーザーであれば、gcovツールはテストカバレッジ情報を提供できます。しかし、より一般的に使用されているGoコンパイラスイートであるgcのユーザーの場合、Go 1.2までは運が悪かったでしょう。

Goのテストカバレッジ

Goの新しいテストカバレッジツールでは、動的デバッグを回避する別のアプローチを採用しました。アイデアはシンプルです。コンパイル前にパッケージのソースコードを書き換えてインストルメンテーションを追加し、変更されたソースをコンパイルして実行し、統計をダンプします。goコマンドがソースからテスト、実行までの流れを制御するため、書き換えは簡単に手配できます。

例を挙げます。次のようなシンプルな1ファイルパッケージがあるとします。

package size

func Size(a int) string {
    switch {
    case a < 0:
        return "negative"
    case a == 0:
        return "zero"
    case a < 10:
        return "small"
    case a < 100:
        return "big"
    case a < 1000:
        return "huge"
    }
    return "enormous"
}

そしてこのテスト

package size

import "testing"

type Test struct {
    in  int
    out string
}

var tests = []Test{
    {-1, "negative"},
    {5, "small"},
}

func TestSize(t *testing.T) {
    for i, test := range tests {
        size := Size(test.in)
        if size != test.out {
            t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
        }
    }
}

パッケージのテストカバレッジを取得するには、go test-coverフラグを指定して、カバレッジを有効にしてテストを実行します。

% go test -cover
PASS
coverage: 42.9% of statements
ok      size    0.026s
%

カバレッジが42.9%であることに注目してください。これはあまり良くありません。この数値を上げる方法を尋ねる前に、どのように計算されたのかを見てみましょう。

テストカバレッジが有効になっている場合、go testは、コンパイル前にソースコードを書き換えるために、ディストリビューションに含まれる別のプログラムである「cover」ツールを実行します。書き換えられたSize関数は次のようになります。

func Size(a int) string {
    GoCover.Count[0] = 1
    switch {
    case a < 0:
        GoCover.Count[2] = 1
        return "negative"
    case a == 0:
        GoCover.Count[3] = 1
        return "zero"
    case a < 10:
        GoCover.Count[4] = 1
        return "small"
    case a < 100:
        GoCover.Count[5] = 1
        return "big"
    case a < 1000:
        GoCover.Count[6] = 1
        return "huge"
    }
    GoCover.Count[1] = 1
    return "enormous"
}

プログラムの各実行可能セクションには、実行時にそのセクションが実行されたことを記録する代入ステートメントが注釈付けされています。カウンターは、coverツールによって生成される2番目の読み取り専用データ構造を介して、それがカウントするステートメントの元のソース位置に紐付けられています。テスト実行が完了すると、カウンターが収集され、いくつのカウンターが設定されたかを見てパーセンテージが計算されます。

この注釈付けの代入はコストがかかるように見えるかもしれませんが、単一の「move」命令にコンパイルされます。したがって、その実行時オーバーヘッドは控えめで、典型的な(より現実的な)テストを実行しても約3%しか増加しません。これにより、テストカバレッジを標準の開発パイプラインの一部に含めることが合理的になります。

結果の表示

私たちの例のテストカバレッジは良くありませんでした。その理由を調べるために、go testに「カバレッジプロファイル」を書き出すように依頼します。これは、収集された統計を保持するファイルで、より詳細に調べることができます。これは簡単です。-coverprofileフラグを使用して、出力ファイルを指定します。

% go test -coverprofile=coverage.out
PASS
coverage: 42.9% of statements
ok      size    0.030s
%

-coverprofileフラグは自動的に-coverを設定してカバレッジ分析を有効にします。)テストは以前と同じように実行されますが、結果はファイルに保存されます。それらを調べるには、go testなしでテストカバレッジツール自体を実行します。まず、関数ごとにカバレッジを分割するように要求できますが、この場合は関数が1つしかないため、あまり明確になりません。

% go tool cover -func=coverage.out
size.go:    Size          42.9%
total:      (statements)  42.9%
%

データを表示するはるかに興味深い方法は、カバレッジ情報で装飾されたソースコードのHTMLプレゼンテーションを取得することです。この表示は-htmlフラグによって呼び出されます。

$ go tool cover -html=coverage.out

このコマンドが実行されると、ブラウザウィンドウがポップアップし、カバレッジ済み(緑)、未カバレッジ(赤)、および未インストルメント(灰色)のソースが表示されます。以下はスクリーンショットです。

このプレゼンテーションでは、何が問題なのかが明らかです。いくつかのケースをテストし忘れていました!そして、それらがどれであるかを正確に確認できるため、テストカバレッジを簡単に改善できます。

ヒートマップ

このソースレベルのアプローチによるテストカバレッジの大きな利点は、さまざまな方法でコードを簡単にインストルメントできることです。たとえば、ステートメントが実行されたかどうかだけでなく、何回実行されたかを知ることができます。

go testコマンドは、カバレッジモードを3つの設定のいずれかに設定する-covermodeフラグを受け入れます。

  • set:各ステートメントが実行されましたか?
  • count:各ステートメントは何回実行されましたか?
  • atomic:countと同様ですが、並列プログラムで正確にカウントします。

デフォルトは「set」で、これはすでに見てきました。atomic設定は、並列アルゴリズムの実行時に正確なカウントが必要な場合にのみ必要です。これは、sync/atomicパッケージのアトミック操作を使用するため、非常に高価になる可能性があります。ただし、ほとんどの場合、countモードは問題なく機能し、デフォルトのsetモードと同様に非常に安価です。

標準パッケージであるfmt書式設定パッケージのステートメント実行をカウントしてみましょう。テストを実行し、カバレッジプロファイルを書き出すことで、後で情報をきれいに表示できます。

% go test -covermode=count -coverprofile=count.out fmt
ok      fmt 0.056s  coverage: 91.7% of statements
%

これは、以前の例よりもはるかに良いテストカバレッジ比率です。(カバレッジ比率はカバレッジモードの影響を受けません。)関数内訳を表示できます。

% go tool cover -func=count.out
fmt/format.go: init              100.0%
fmt/format.go: clearflags        100.0%
fmt/format.go: init              100.0%
fmt/format.go: computePadding     84.6%
fmt/format.go: writePadding      100.0%
fmt/format.go: pad               100.0%
...
fmt/scan.go:   advance            96.2%
fmt/scan.go:   doScanf            96.8%
total:         (statements)       91.7%

大きな効果はHTML出力で得られます。

% go tool cover -html=count.out

このプレゼンテーションでpad関数は次のようになります。

緑の濃淡がどのように変化するかに注目してください。明るい緑色のステートメントは実行回数が多く、彩度の低い緑色は実行回数が少ないことを表します。ステートメントにマウスを合わせると、ツールチップに実際のカウントが表示されます。執筆時点では、カウントは次のようになります(ツールチップから行頭マーカーにカウントを移動して、表示しやすくしました)。

2933    if !f.widPresent || f.wid == 0 {
2985        f.buf.Write(b)
2985        return
2985    }
  56    padding, left, right := f.computePadding(len(b))
  56    if left > 0 {
  37        f.writePadding(left, padding)
  37    }
  56    f.buf.Write(b)
  56    if right > 0 {
  13        f.writePadding(right, padding)
  13    }

これは関数の実行に関する多くの情報であり、プロファイリングに役立つ可能性があります。

基本ブロック

前の例のカウントが、閉じ括弧のある行で期待どおりではなかったことに気づいたかもしれません。それは、いつものことですが、テストカバレッジは不正確な科学だからです。

しかし、ここで何が起こっているのかを説明する価値はあります。カバレッジの注釈は、従来のメソッドでバイナリがインストルメントされる方法で、プログラムのブランチによって区切られるようにしたいと思います。ただし、ブランチはソースに明示的に表示されないため、ソースを書き換えることでこれを行うのは困難です。

カバレッジの注釈が行うことは、通常、括弧で区切られたブロックをインストルメントすることです。これを一般的に正しく行うのは非常に困難です。使用されているアルゴリズムの結果として、閉じ括弧は閉じているブロックに属しているように見えますが、開き括弧はブロックの外に属しているように見えます。より興味深い結果は、次のような式で

f() && g()

fgの呼び出しを個別にインストルメントする試みはありません。事実にかかわらず、それらは常に同じ回数実行されたように見えます。それはfが実行された回数です。

公平に言えば、gcovでさえここで問題を抱えています。そのツールはインストルメンテーションを正しく行いますが、表示は行ベースであるため、一部のニュアンスを見逃す可能性があります。

全体像

Go 1.2のテストカバレッジに関する物語は以上です。興味深い実装を持つ新しいツールは、テストカバレッジ統計だけでなく、それらの解釈しやすいプレゼンテーション、さらにはプロファイリング情報を抽出する可能性も可能にします。

テストはソフトウェア開発の重要な部分であり、テストカバレッジはテスト戦略に規律を加える簡単な方法です。さあ、テストして、カバーしましょう。

次の記事: Goプレイグラウンドの内側
前の記事: Go 1.2がリリースされました
ブログインデックス