2016/10/25


mattn - Creators' Stickers

Operating Environment LINE for iOS or Android version 3.1.1 or higher, LINE for NOKIA Asha version 1...

https://store.line.me/stickershop/product/1337465

LINE スタンプ作りました。我慢しきれなかったオッサンギャグを言ってしまった後、なるはやでこのスタンプを投下して下さい。罪悪感が少し減るかもしれません。

LINEスタンプ


いい記事に感化されて僕も何か書きたくなった。

Golangにおけるinterfaceをつかったテスト技法 | SOTA

Golangにおけるinterfaceをつかったテスト技法 最近何度か聞かれたので自分がGolangでCLIツールやAPIサーバーを書くときに実践してるinterfaceを使ったテスト技法について簡単...

http://deeeet.com/writing/2016/10/25/go-interface-testing/

僕も1つ golang のテストの tips を。golang を書いていて良くあるのが「コマンドがステータス 0 で終了する事」のテスト。

package main

import "os"

func doSomething() {
    os.Exit(0)
}

func main() {
    doSomething()
}

でも os.Exit しちゃうとテスト自体が死んでしまうので「テスト出来ない」と思われがち。しかしラッパーを書いてあげる事でテストは出来る。まず os.Exit を関数リファレンスにしてしまう。

package main

import "os"

var exit = os.Exit

func doSomething() {
    exit(0)
}

func main() {
    doSomething()
}

次に以下の様なテストツールを書く。ファイル名は main_test.go

package main

import (
    "errors"
    "fmt"
    "testing"
)

type ExitError int

func (e ExitError) Error() string {
    return fmt.Sprintf("exited with code %d"int(e))
}

func init() {
    exit = func(n int) {
        panic(ExitError(n))
    }
}

func testExit(code int, f func()) (err error) {
    defer func() {
        e := recover()
        switch t := e.(type) {
        case ExitError:
            if int(t) == code {
                err = nil
            } else {
                err = fmt.Errorf("expected exit with %v but %v", code, e)
            }
        default:
            err = fmt.Errorf("expected exit with %v but %v", code, e)
        }
    }()

    f()
    return errors.New("expected exited but not")
}

第一引数は期待する終了コード。第二引数は関数を呼び出すためのクロージャ。後は以下の様なテストを書くだけ。

func TestExit(t *testing.T) {
    err := testExit(0func() {
        doSomething()
    })

    if err != nil {
        t.Fatal(err)
    }
}

テストを実行すると

$ go test
PASS
ok      _/C_/dev/go-sandbox/exittest    0.080s

今度はコードを修正して

package main

import "os"

var exit = os.Exit

func doSomething() {
    exit(1)
}

func main() {
    doSomething()
}
$ go test
--- FAIL: TestExit (0.00s)
        main_test.go:46: expected exit with 0 but exited with code 1
FAIL
FAIL    _/C_/dev/go-sandbox/exittest    0.080s

また log.Fatal で panic を起こさせても大丈夫。

package main

import (
    "log"
    "os"
)

var exit = os.Exit

func doSomething() {
    log.Fatal("unknown error")
}

func main() {
    doSomething()
}
$ go test
2016/10/25 11:34:03 unknown error
FAIL    _/C_/dev/go-sandbox/exittest    0.254s

出来ないと思ったらそこで勝負は終わりよ!

注意点としては、os.Exit の実行と同じスコープで recover する処理があると、そちらに拾われてしまうのでその辺が良く分かっている人だけお使い下さい。

あと、もちろんテストしやすい様に、以下の様に実装するのが正しいのです。(上記はそれが出来ない場合の得策です)

package main

import (
    "log"
    "os"
)

func doSomething() int {
    log.Print("unknown error")
    return 1
}

func main() {
    os.Exit(doSomething())
}

2016/10/19


golang のテストツールには標準でベンチマークツールが付属しています。例えば、引数 n を貰ってその数分だけメッセージの入ったスライスを返す関数 makeSlice が以下の実装だったとします。

foo.go

package foo

import "fmt"

func makeSlice(n int) []string {
    var r []string
    for i := 0; i < n; i++ {
        r = append(r, fmt.Sprintf("%03d だよーん", i))
    }
    return r
}

如何にも遅そうなコードですね。まずはこのコードを単品で計測するベンチマークを書きます。

foo_test.go

package foo

import "testing"

func BenchmarkMakeSlice(b *testing.B) {
    b.ResetTimer()
    makeSlice(b.N)
}

このベンチマークを実行する為には以下の様に実行します。

$ go test -test.bench BenchmarkMakeSlice

この時、気を付ける事は makeSlice に与える負荷量を固定値にしない事です。ベンチマークの実行は通常、b.N に対して 100, 10000, 1000000, 5000000 のベンチマークが実行されます。b.N の値の変化により処理がどの様に遅くなるかを検出する為には b.N に依存した処理を書く必要があります。

次に気を付けるべき事は、-count を指定する事です。b.N に対するテストを1回ずつ行ったとしても安定していないならば参考値にしかなり得ないからです。以下の様に -count を指定して実行します。

$ go test -count 10 -test.bench BenchmarkMakeSlice
BenchmarkMakeSlice-4     5000000               355 ns/op
BenchmarkMakeSlice-4     5000000               376 ns/op
BenchmarkMakeSlice-4     5000000               377 ns/op
BenchmarkMakeSlice-4     5000000               390 ns/op
BenchmarkMakeSlice-4     5000000               359 ns/op
BenchmarkMakeSlice-4     5000000               342 ns/op
BenchmarkMakeSlice-4     5000000               400 ns/op
BenchmarkMakeSlice-4     3000000               384 ns/op
BenchmarkMakeSlice-4     3000000               335 ns/op
BenchmarkMakeSlice-4     5000000               377 ns/op
PASS
ok      _/C_/dev/go-sandbox/bench       22.062s

ここまで出来たら改良したソースコードとの比較を始めましょう。改良された後のベンチマークだけを見たとしても本当に速くなったのかどうかは断言できませんよね。改良は上記の makeSlice を以下の様に改良しました。

package foo

import "fmt"

func makeSlice(n int) []string {
    r := make([]string, n)
    for i := 0; i < n; i++ {
        r[i] = fmt.Sprintf("%03d だよーん", i)
    }
    return r
}

make により予めスライスを確保する事で、メモリ確保の回数を減らしています。改良後のベンチマークを取ります。

$ go test -count 10 -test.bench BenchmarkMakeSlice
BenchmarkMakeSlice-4    10000000           162 ns/op
BenchmarkMakeSlice-4    10000000           161 ns/op
BenchmarkMakeSlice-4    10000000           160 ns/op
BenchmarkMakeSlice-4    10000000           161 ns/op
BenchmarkMakeSlice-4    10000000           163 ns/op
BenchmarkMakeSlice-4    10000000           165 ns/op
BenchmarkMakeSlice-4    10000000           166 ns/op
BenchmarkMakeSlice-4    10000000           159 ns/op
BenchmarkMakeSlice-4    10000000           162 ns/op
BenchmarkMakeSlice-4    10000000           157 ns/op
PASS
ok      _/C_/dev/go-sandbox/bench   18.430s

さて、確かに目に見えて速くなってはいるのですが、いったいどの程度速くなっているのでしょうか。-count を指定した事でこの処理には若干ながら揺らぎがある事が見えます。その揺らぎを纏めて、どの程度高速化されたのかを知りたいですよね。そこで便利なのが benchstat です。

benchstat - GoDoc

Command benchstat Benchstat computes and compares statistics about benchmarks. Usage: benchstat [-de...

https://godoc.org/rsc.io/benchstat

benchstat はベンチマーク結果の前後を比較し、揺らぎを計算した上でどの程度の速度差があるかを表示できるツールです。インストールは以下の様に行います。

$ go get rsc.io/benchstat

改良前のベンチマーク結果を bench1.log というファイルに、改良後のベンチマーク結果を bench2.log というファイルに出力させた後、以下の様に実行します。

$ benchstat bench1.log bench2.log
name         old time/op  new time/op  delta
MakeSlice-4   369ns ± 9%   162ns ± 3%  -56.26%  (p=0.000 n=9+10)

揺らぎが纏められ、この改良で処理時間が改良前の 56.26% にまで減っている事が分かりました。

  • 改修の前後で同じ条件である事
  • 改修内容に対して網羅的な入力パターン
  • 揺らぎが発生する程に多い回数

ベンチマークはこれらの条件を満たさないと本当に正しい結果は得られないと思います。golang のベンチマークツールはこういった条件を極力簡単に満たせる為の仕組みやツールが揃っています。ぜひ有効に活用して下さい。