Goブログ

表紙記事

Rob Pike
2013年12月2日

はじめに

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

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

ツールサポート

まず、背景として、言語が優れたツールをサポートするとはどういう意味でしょうか?それは、言語が優れたツールを書きやすくし、そのエコシステムがあらゆる種類のツールの構築をサポートしていることを意味します。

Goには、ツール作成に適した多くの特性があります。まず、Goには解析しやすい規則的な構文があります。文法は、解析するために複雑な機構を必要とする特殊なケースがないようにすることを目的としています。

可能な限り、Goは字句および構文構造を使用して、意味的特性を理解しやすくしています。例としては、エクスポートされた名前を定義するための大文字の使用や、Cの伝統における他の言語と比較して大幅に簡略化されたスコープ規則などがあります。

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

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

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

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

もちろん、これらのパッケージが実現するのはこれらの大きなツールだけではありません。たとえば、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"
}

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

その注釈付きの代入はコストがかかるように見えるかもしれませんが、1つの「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 Playground の内部
前の記事:Go 1.2 がリリースされました
ブログインデックス