2016/10/25

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. unsafe.StringData、unsafe.String、unsafe.SliceData が入った。
  4. Re: Go言語で画像ファイルか確認してみる
  5. net/url に JoinPath が入った。

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

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())
}
Posted at by