いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-167 (テストとカバレッジとベンチマークとプロファイルのやり方)

概要

よく忘れるので、ここにメモメモ。。。

Goには、標準でテスト、ベンチマーク、プロファイルを行う機能が付属しています。

どれもコマンドラインで実行できるので便利です。

ただ、ベンチマークとプロファイルに関しては、コマンドラインオプションなども付与しないといけないのでよく忘れがち。

以下、自分用ですがメモです。

今回のサンプル

今回のサンプルは以下のURLにアップしています。よろしければご参考まで。

github.com

決まりごと

テスト、ベンチマーク、プロファイルは、いずれも go test コマンドから行います。

なので、ファイル名は xxx_test.go としておく必要があります。

テスト対象

以下の関数を lib.go として保存して、テスト対象とします。

package main

func Fib(n int) int {
    if n < 2 {
        return n
    }

    return Fib(n-1) + Fib(n-2)
}

テスト

以下のようなテストコードを lib_test.go に記載したとします。

package main

import (
   "fmt"
   "testing"
)

// TestFib -- fib() の ユニットテスト
func TestFib(t *testing.T) {
    tests := []struct {
        in, want int
    }{
        {1, 1},
        {2, 1},
        {3, 2},
        {10, 55},
        {40, 102334155},
        {41, 165580141},
        {42, 267914296},
        {43, 433494437},
    }

    for _, test := range tests {
        test := test
        title := fmt.Sprintf("fib:%d", test.in)

        t.Run(title, func(t *testing.T) {
            t.Parallel()

            got := Fib(test.in)
            if test.want != got {
                t.Errorf("[want] %v\t[got] %v", test.want, got)
            }
        })
    }
}

テストの実行は以下のようにします。

$ go test .

実際の実行結果は以下のようになります。

gitpod /workspace/try-golang/cmd/test_bench_profile $ go test .
ok      github.com/devlights/try-golang/cmd/test_bench_profile  2.216s

デフォルトでは結果だけ表示されますが、詳細表示したい場合は -v オプションを付与します。

gitpod /workspace/try-golang/cmd/test_bench_profile $ go test -v .
=== RUN   TestFib
=== RUN   TestFib/fib:1
=== PAUSE TestFib/fib:1
=== RUN   TestFib/fib:2
=== PAUSE TestFib/fib:2
=== RUN   TestFib/fib:3
=== PAUSE TestFib/fib:3
=== RUN   TestFib/fib:10
=== PAUSE TestFib/fib:10
=== RUN   TestFib/fib:40
=== PAUSE TestFib/fib:40
=== RUN   TestFib/fib:41
=== PAUSE TestFib/fib:41
=== RUN   TestFib/fib:42
=== PAUSE TestFib/fib:42
=== RUN   TestFib/fib:43
=== PAUSE TestFib/fib:43
=== CONT  TestFib/fib:1
=== CONT  TestFib/fib:10
=== CONT  TestFib/fib:40
=== CONT  TestFib/fib:42
=== CONT  TestFib/fib:3
=== CONT  TestFib/fib:2
=== CONT  TestFib/fib:43
=== CONT  TestFib/fib:41
--- PASS: TestFib (0.00s)
    --- PASS: TestFib/fib:1 (0.00s)
    --- PASS: TestFib/fib:10 (0.00s)
    --- PASS: TestFib/fib:3 (0.00s)
    --- PASS: TestFib/fib:2 (0.00s)
    --- PASS: TestFib/fib:40 (0.52s)
    --- PASS: TestFib/fib:41 (0.85s)
    --- PASS: TestFib/fib:42 (1.38s)
    --- PASS: TestFib/fib:43 (2.21s)
PASS
ok      github.com/devlights/try-golang/cmd/test_bench_profile  2.216s

t.Parallel() を指定しているので、平行でテストが実行されています。

t.Parallel() は平行でテストを実施したい場合に呼び出しますので、普段は付けなくて良いですね。

Exampleテスト

Goには ExampleXxx と記載することでもテストを記載できます。

ExampleXxx の場合は、結果をコメントで記載しておきます。

以下のようになります。

package main

import (
    "fmt"
    "testing"
)

// Examplefib -- fib() の Exampleテスト
func Examplefib() {
    fmt.Println(fib(40))

    // Output:
    // 102334155
}

これも通常のテストとして扱われます。このテストを用意しておくと go doc でドキュメント作成した際に Example ブロックに表示されるようになります。

関数の使い方を説明するコードとして使えるので便利です。

gitpod /workspace/try-golang/cmd/test_bench_profile $ go test -v . -run ^Example.*$
=== RUN   ExampleFib
--- PASS: ExampleFib (0.52s)
PASS
ok      github.com/devlights/try-golang/cmd/test_bench_profile  0.524s

カバレッジ

カバレッジは go test に以下のようにオプションを指定することで実行できます。

$ go test . -coverprofile=coverage.out

上記で指定したファイルにカバレッジ結果が出力されます。

そのファイルを今度は go tool cover に食べさせることで結果を見ることが出来ます。

$ go tool cover -func=coverage.out

実際にやってみると以下のようになります。

gitpod /workspace/try-golang/cmd/test_bench_profile $ go test -coverprofile=coverage.out
PASS
coverage: 100.0% of statements
ok      github.com/devlights/try-golang/cmd/test_bench_profile  14.928s

gitpod /workspace/try-golang/cmd/test_bench_profile $ go tool cover -func=coverage.out
github.com/devlights/try-golang/cmd/test_bench_profile/lib.go:3:        Fib             100.0%
total:                                                                  (statements)    100.0%

ベンチマーク

ベンチマークはテストと同じ xxx_test.go に記載します。

通常のテスト関数は

func TestFib(t *testing.T) {}

という様に TestXxx という命名規則で、引数に *testing.T を受け取るようにしますが、ベンチマークの場合は

func BenchmarkFib(b *testing.B) {}

という様に BenchmarkXxx という命名規則で、引数に *testing.B を受け取るようにします。

例えば、以下のようになります。

// BenchmarkAppend_WithoutLen -- ベンチマークのサンプル
func BenchmarkAppend_WithoutLen(b *testing.B) {
    s := make([]string, 0)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        //lint:ignore SA4010 It's ok because this is just a example.
        s = append(s, fmt.Sprintf("%d", i))
    }
}

// BenchmarkAppend_WithLen -- ベンチマークのサンプル
func BenchmarkAppend_WithLen(b *testing.B) {
    s := make([]string, 0, b.N)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        //lint:ignore SA4010 It's ok because this is just a example.
        s = append(s, fmt.Sprintf("%d", i))
    }
}

ベンチマークではループを回しますが、ループの上限は b.N で指定するようにします。

実行は go test に対して -bench というオプションを指定します。

gitpod /workspace/try-golang/cmd/test_bench_profile $ go test . -bench .
goos: linux
goarch: amd64
pkg: github.com/devlights/try-golang/cmd/test_bench_profile
cpu: Intel(R) Xeon(R) CPU @ 2.80GHz
BenchmarkAppend_WithoutLen-16            5636902               210.7 ns/op
BenchmarkAppend_WithLen-16              10482872               107.8 ns/op
PASS
ok      github.com/devlights/try-golang/cmd/test_bench_profile  5.442s

ベンチマーク時にメモリの状況も取得する場合は、さらに -benchmem を付与します。

gitpod /workspace/try-golang/cmd/test_bench_profile $ go test . -bench . -benchmem
goos: linux
goarch: amd64
pkg: github.com/devlights/try-golang/cmd/test_bench_profile
cpu: Intel(R) Xeon(R) CPU @ 2.80GHz
BenchmarkAppend_WithoutLen-16            5524316               214.2 ns/op           111 B/op          1 allocs/op
BenchmarkAppend_WithLen-16              10607025               108.3 ns/op            16 B/op          1 allocs/op
PASS
ok      github.com/devlights/try-golang/cmd/test_bench_profile  5.459s

上の出力ですが、実はベンチマークを実行する際にテストも実行されています。

通常はこれで良いのですが、時間が掛かるテストがあったりするとベンチマークが出るまでに時間がかかるので

以下のようにすると、テストの実行をせずにベンチマークだけ実行できます。

$ go test . -bench . -benchmem -run ^$
gitpod /workspace/try-golang/cmd/test_bench_profile $ go test . -bench . -benchmem -run ^$
goos: linux
goarch: amd64
pkg: github.com/devlights/try-golang/cmd/test_bench_profile
cpu: Intel(R) Xeon(R) CPU @ 2.80GHz
BenchmarkAppend_WithoutLen-16            6408944               196.0 ns/op            98 B/op          1 allocs/op
BenchmarkAppend_WithLen-16               9027681               115.4 ns/op            16 B/op          1 allocs/op
PASS
ok      github.com/devlights/try-golang/cmd/test_bench_profile  3.723s

トータルの実行時間が減っていますね。

プロファイル

実際のコードに直接 runtime/pprof パッケージを使ってプロファイルを採取する方法はここでは記載しません。

以下はテストコードを使ってプロファイルを採取するやり方です。

プロファイルはテストかベンチマーク関数があれば実行できます。

メモリのプロファイルを取得する場合は -memprofile mem.pprof と CPU時間のプロファイルを取得する場合は -cpuprofile cpu.pprof を付与します。

ファイル名は好きに指定できます。

実際の実行は例えば以下のようにします。

gitpod /workspace/try-golang/cmd/test_bench_profile $ go test . -bench . -benchmem -run ^$ -memprofile mem.pprof -cpuprofile cpu.pprof
goos: linux
goarch: amd64
pkg: github.com/devlights/try-golang/cmd/test_bench_profile
cpu: Intel(R) Xeon(R) CPU @ 2.80GHz
BenchmarkAppend_WithoutLen-16            5921113               200.3 ns/op           105 B/op          1 allocs/op
BenchmarkAppend_WithLen-16              11097564               121.5 ns/op            16 B/op          1 allocs/op
PASS
ok      github.com/devlights/try-golang/cmd/test_bench_profile  3.061s

これで、結果が出力されました。

次にこの結果を go tool pprof に食べさせます。

gitpod /workspace/try-golang/cmd/test_bench_profile $ go tool pprof mem.pprof 
File: test_bench_profile.test
Type: alloc_space
Time: Dec 27, 2021 at 6:54am (UTC)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 

gdbみたいな感じでインタラクティブなCUIが起動するので、ここでコマンドを実行して値を見ることが出来ます。

一番良く使うのが top です。

(pprof) top
Showing nodes accounting for 1054.44MB, 99.72% of 1057.41MB total
Dropped 30 nodes (cum <= 5.29MB)
Showing top 10 nodes out of 11
      flat  flat%   sum%        cum   cum%
  672.02MB 63.55% 63.55%   700.52MB 66.25%  github.com/devlights/try-golang/cmd/test_bench_profile.BenchmarkAppend_WithoutLen
  345.42MB 32.67% 96.22%   348.42MB 32.95%  github.com/devlights/try-golang/cmd/test_bench_profile.BenchmarkAppend_WithLen
   31.50MB  2.98% 99.20%    31.50MB  2.98%  fmt.Sprintf
    5.51MB  0.52% 99.72%     5.51MB  0.52%  runtime.allocm
         0     0% 99.72%     5.51MB  0.52%  runtime.newm
         0     0% 99.72%     5.51MB  0.52%  runtime.resetspinning
         0     0% 99.72%     5.51MB  0.52%  runtime.schedule
         0     0% 99.72%     5.51MB  0.52%  runtime.startm
         0     0% 99.72%     5.51MB  0.52%  runtime.wakep
         0     0% 99.72%  1048.94MB 99.20%  testing.(*B).launch

結果をpngで画像出力することも出来たりします。その場合は Graphviz が必要になります。

こんな感じ

f:id:gsf_zero1:20211227155705p:plain

インストールされていない場合は

$ sudo apt update -q ; sudo apt install -y graphviz

とかしてインストールするようにしましょう。

参考情報


過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com