Help us understand the problem. What is going on with this article?

golangのテストはじめ

More than 1 year has passed since last update.

はじめに

golangでのテストはとてもシンプルで、rubyのrspecのように新しくDSLを覚える必要もありません。

テストについての記事は沢山あるのですが、自分の中で特にこれは最初に覚えておいた方がいいなと思うことをピックアップしました。

基本的なtestの書き方

  • 例えばcalc.go のテストならば同じディレクトリ内にcalc_test.goという名前で作成する。
  • テストファイル内ではtestingパッケージをインポートする。
  • テストファイル内では、TestXXXという名前でテストメソッドを作成する。
  • DSLは特に無いので普通にテストコードを書く。
  • パラメータと期待値の組み合わせの配列を用意して、ループで検証していく形が推奨されている(Table Driven Test)
calc.go
package calc

func Add(a,b int) int {
    return a + b
}
calc_test.go
package calc

import (
    "testing"
)

func TestAdd(t *testing.T) {
    patterns := []struct {
        a        int
        b        int
        expected int
    }{
        {1, 2, 3},
        {10, -2, 8},
        {-10, -2, -12},
    }

    for idx, pattern := range patterns {
        actual := Add(pattern.a, pattern.b)
        if pattern.expected != actual {
            t.Errorf("pattern %d: want %d, actual %d", idx, pattern.expected, actual)
        }
    }
}

testの実行方法

  • カレントディレクトリ以下すべてを再帰的にテストgo test -v ./...
  • 特定のパッケージをテストgo test -v ./hogehoge(パッケージディレクトリを相対パスで指定する)
  • 特定のメソッドのみテストするgo test -run TestAdd ./...

※ -v オプションを付けると実行結果に詳細が付きますので、基本的にはつけておいたほうが良いです。

テストの実行前後に処理を入れるには

TestMainメソッドを定義します。

code := m.Run()を実行するとテストメソッドが走るので、その前後にDBの初期化処理等を入れることが出来ます。

calc_test.go
package calc

import (
    "fmt"
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    fmt.Println("before test")
    code := m.Run()
    fmt.Println("after test")
    os.Exit(code)
}

func TestAdd(t *testing.T) {
    // 以下省略
}

これを実行すると、以下のようになります。

テストの前後にfmt.Printlnが入っているのがわかります。

$ go test -v ./...
before test
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
after test
ok

テストでモックを使うには

インターフェースを使ったモック

インターフェースを使っているオブジェクトの場合、実際のコードとテストコードでインタフェースに定義するオブジェクトを変えることでテスト時の振る舞いを変えることが出来ます。
ここではsomefuncパッケージのClientオブジェクトのRunメソッド内で呼び出されるcallメソッドの振る舞いを、モックを使って切り替える方法を紹介します。

somefunc.go
package somefunc

type Caller interface {
    call(val int) int
}

type Client struct {
    FuncCaller Caller
}

type ExampleCaller struct{}

func (c *Client) Run(val int) int {
    return c.FuncCaller.call(val)
}

func (f *ExampleCaller) call(val int) int {
    return val
}

上記のコードを実行するには以下のように呼び出します。

main.go
c := somefunc.Client{&somefunc.ExampleCaller{}}
c.Run(1)

ここで、テスト時にExampleCallerのモックを作って、callメソッドの振る舞いを変えるにはテストコードを以下のようにします。

somefunc_test.go
package somefunc

import (
    "testing"
)

func TestRun(t *testing.T) {

    patterns := []struct {
        val      int
        expected int
    }{
        {2, 2},
        {8, 8},
        {-10, -10},
    }

    for idx, pattern := range patterns {
        // Clientのnewの際に、モックオブジェクトを引数にする
        c := Client{&mockCaller{}}
        actual := c.Run(pattern.val)
        if pattern.expected != actual {
            t.Errorf("pattern %d: want %d, actual %d", idx, pattern.expected, actual)
        }
    }
}

// callメソッドのレシーバをmockCallerとして宣言する。
type mockCaller struct{}

// 通常のコードではcallメソッドは引数の値をそのまま返却するが、
// モックでは、引数 + 10した値を返却するようにする。
func (s *mockCaller) call(val int) int {
    return val + 10
}

変数の再代入で行う方法

ここではsomeprocessパッケージのRun関数のテストを行っていますが、Run内でcallという関数を呼び出しています。

このcall関数の挙動をテストの時だけ切り替えるには、call関数を変数に入れ、テスト内で変数にモックを再代入すればOKです。

someprocess.go
package someprocess

func Run(val int) int {
    return call(val)
}

var call = func(val int) int {
    return val
}
someprocess_test.go
package someprocess

import (
    "testing"
)

func TestRun(t *testing.T) {
    call = func(val int) int {
        return val + 10
    }

    patterns := []struct {
        val      int
        expected int
    }{
        {2, 12},
        {8, 18},
        {-10, 0},
    }

    for idx, pattern := range patterns {
        actual := Run(pattern.val)
        if pattern.expected != actual {
            t.Errorf("pattern %d: want %d, actual %d", idx, pattern.expected, actual)
        }
    }
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away