[ testing ] Go言語入門~ テスト ~

この記事は約12分で読めます。

Go言語では、標準でシンプルなテストパッケージが存在します。

どんなプログラムでも、品質を担保するためにテストを書く必要があります。

今回は、Go言語で作成したコードで テスト を行います。

シンプルな テスト

まず最初に基本的なテストを書いてみます。

ここでは、int型のスライスを引数にとって、スライスの中にあるint型の最大値を返すmax関数を作成します。これに対して、テストを作成します。

ファイル名 max.go

package main

func max(inputs []int) int {
	maxVal := 0
	for _, v := range inputs {
		if v > maxVal {
			maxVal = v
		}
	}
	return maxVal
}

 

testファイルは xx_test.go という命名で作成します。

そして、テストの関数名はTestXxx()とします。

引数には、*testing.Tを入れましょう。(ベンチマークの場合は、*testing.Bです。)

ファイル名 max_test.go

package main

import (
	"testing"
)

func TestMax(t *testing.T) {
	testcases := []struct {
		inputs   []int
		expected int
	}{
		{[]int{1, 4, 6}, 6},
		{[]int{0, -1, -3}, 0},
		{[]int{1}, 1},
		{[]int{-1, -2, -3}, -1},
		{[]int{}, 0},
	}
	for _, testcase := range testcases {
                actual := max(testcase.inputs)
		if actual != testcase.expected {
			t.Errorf("invalid actual:%d,expected:%d", actual, testcase.expected)
		}
	}
}

書き方は基本的に、テストケースを用意して、実際にmax関数を呼び出し、期待値通りになるか比較していきます。

実行する

$ go test (テスト関数を表示したければ、-v)

 

実行結果

--- FAIL: TestMax (0.00s)
    max_test.go:21: invalid actual:0,expected:-1
FAIL
exit status 1
FAIL	<ファイルディレクトリ>	0.006s

 

テストが落ちましたね。

max_test.go:21: invalid actual:0,expected:-1

から、テストが通っていないため、max変数を修正します。

package main

func max(inputs []int) int {
        if len(inputs)==0{
             return 0
        }
	maxVal := inputs[0]
	for i := 1; i < len(inputs); i++ { 
                if inputs[i] > maxVal {
			maxVal = inputs[i]
		}
	}
	return maxVal
}

 

実行結果

PASS
ok  	<ファイルディレクトリ>	0.005s

 

これで通りました。

こんな感じで、テストを書くことで関数の予想外のエラーを無くしていきます。

 

  • テストはxx_test.go
  • テストメソッドはTestXxx(t *testing.T)
  • 予想外のエラーを無くすためにテストを書く

コントローラーのテスト

コントローラのテストを書きます。

ファイル名:server.go

package main

import (
  "fmt"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello World")
}

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

 

ファイル名:server_test.go

package main

import(
	"testing"
	"net/http"
	"net/http/httptest"
)

func TestHelloHandler(t *testing.T) {

	req, err := http.NewRequest("GET", "/", nil)
	if err != nil{
		panic(err)
	}

	response := httptest.NewRecorder()

	http.HandlerFunc(handler).ServeHTTP(response, req)

	if response.Code != http.StatusOK {
		t.Errorf("Status code differs. Expected %d .\n Got %d instead", http.StatusOK, response.Code)
	}

	expected := "Hello World"
	if response.Body.String() != expected {
		t.Errorf("body differs expected:%v,actual:%v",expected,response.Body.String())
	}
}

モックを使ったテスト(全コード配布)

$ go get github.com/golang/mock/gomock
$ go get github.com/golang/mock/mockgen

 

今回は以下の記事でも紹介した、BMI計算のメソッドに対して、mockを用います。

 

[ Go ] Go言語で DI してみる (テストコード付き)
今回はGo言語で、DIされているコードとされていないコードを比較することで、DIについて解説します。DIのメリットのテストコードも書いているので、是非みてみてください。

 

mockgenを使って、インターフェースに対してmockの処理を記述したコードを出力します。

今回はBMI計算機(ICalculator)をmock化します。
ファイル名:calculator.go

 
package main

type ICalculator interface {
	calcBMI(height, weight float64) float64
}

 

以下のコマンドを実行するとmock処理のコードが生成されます。

$ mockgen -source calculator.go

すると、以下のようなファイルが生成されます。

mock/mock_calculator.goができますが、

packageをmainに変えて、user.goと同じディレクトリにします。

// Code generated by MockGen. DO NOT EDIT.
// Source: calculator.go

// Package mock_main is a generated GoMock package.
package main

import (
	gomock "github.com/golang/mock/gomock"
	reflect "reflect"
)

// MockICalculator is a mock of ICalculator interface
type MockICalculator struct {
	ctrl     *gomock.Controller
	recorder *MockICalculatorMockRecorder
}

// MockICalculatorMockRecorder is the mock recorder for MockICalculator
type MockICalculatorMockRecorder struct {
	mock *MockICalculator
}

// NewMockICalculator creates a new mock instance
func NewMockICalculator(ctrl *gomock.Controller) *MockICalculator {
	mock := &MockICalculator{ctrl: ctrl}
	mock.recorder = &MockICalculatorMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockICalculator) EXPECT() *MockICalculatorMockRecorder {
	return m.recorder
}

// calcBMI mocks base method
func (m *MockICalculator) calcBMI(height, weight float64) float64 {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "calcBMI", height, weight)
	ret0, _ := ret[0].(float64)
	return ret0
}

// calcBMI indicates an expected call of calcBMI
func (mr *MockICalculatorMockRecorder) calcBMI(height, weight interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "calcBMI", reflect.TypeOf((*MockICalculator)(nil).calcBMI), height, weight)
}

 

次にBMI計算機を実行するUserです。
ファイル名:user.go

package main

import (
	"fmt"
)

type user struct {
	name   string
	height float64
	weight float64
}

func (u user) getBMI2(ic ICalculator) string {
	return fmt.Sprintf("%s's BMI is %.1f", u.name, ic.calcBMI(u.height, u.weight))
}

 

ファイル名:user_test.go

package main

import (
	"github.com/golang/mock/gomock"
	"log"
	"testing"
)
func TestGetBMI2(t *testing.T) {
	tCases := []struct {
		username string
		userBmi  float64
		expected string
	}{
		{"user1", 18.477, "user1's BMI is 18.5"},
		{"user2", 18.922, "user2's BMI is 18.9"},
	}

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	for _, tc := range tCases {
		calclator := NewMockICalculator(ctrl)
		calclator.EXPECT().calcBMI(gomock.Any(), gomock.Any()).Return(tc.userBmi)
		user := user{
			name:   tc.username,
			height: 0,
			weight: 0,
		}
		acutual := user.getBMI2(calclator)
		if acutual != tc.expected {
			t.Errorf("expected:%v ,acutual:%v", tc.expected, acutual)
		}
		log.Println(acutual)
	}
}

 

これによって、mockの処理を記述することで、getBMIに対してテストを記述できます。

ソースコード

calculator.zip

参考文献

Go Mockでインタフェースのモックを作ってテストする #golang

応用:カバレッジ

Goにはテストのカバレッジを取得してくれます。

$ go test -cover

-coverオプションでカバレッジを取得します。
実行結果

ok  	<ファイルディレクトリ>	0.015s	
coverage: 33.3% of statements

カバレッジをhtml出力したい場合は、

$ go test  -coverprofile=<出力ファイル名:(ex)cover.out>
$ go tool cover -html=<出力ファイル名:(ex)cover.out> -o cover.html

こうするとcover.htmlが出力されるので、ブラウザで見てみましょう。

カバレッジ

 

まとめ

今回はGo言語でテストを記述しました。

Go言語のテストはかなりシンプルだと思います。

assertionはないですが、コンポーネントテスト書きやすいと思います。

それでは。

 

タイトルとURLをコピーしました