Series: go-testing
Tags: Go Golang

Go でテストを書いてみる

今まで Go で色々やってきたがテストを書いてこなかったのでテストを書く方法について調べた。

開発環境

  • go version go1.16.5 linux/amd64

本文

Go のテストについて誰かがまとめた情報はたくさん出てくるのだが公式サイトのどこに書いてあるか調べたところ、普通に公式チュートリアルに書いてあった。

Go ではテストフレームワークが同梱されているので特にインストール作業は必要ない。

もっとも基本的なテストとしては、単にテストしたいファイルと同じディレクトリに、テストしたいファイル名に _test をつけたファイルを作って、そこにテストを書いていけばいい。

題材として今禁則処理を考慮した文字列改行を行うプログラムを書いているので、それのテストを書くことにした。
コードの抜粋が以下のとおりである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Package kinsoku は禁則処理を行うパッケージです。
package kinsoku

import (
	"bytes"

	"github.com/mattn/go-runewidth"
)

// ExecKinsoku は禁則処理を行いながら半角文字相当の文字幅 width を超えないように文字列 text を改行します。
// 改行した結果は配列で返します。
func ExecKinsoku(width int, text string) (words []string) {

	var tmpbuf bytes.Buffer
	for _, r := range text {

		tmpbuf.WriteString(string(r))
		for {
			if runewidth.StringWidth(tmpbuf.String()) > width {
				tmpr := []rune(tmpbuf.String())
				splitidx := determineSplitIndex(tmpr, len(tmpr)-1)
				words = append(words, string(tmpr[:splitidx]))
				tmpbuf.Reset()
				tmpbuf.WriteString(string(tmpr[splitidx:]))
			}
			// 改行位置が確定後、残った文字列が文字幅以下なら次の文字へ進む
			if runewidth.StringWidth(tmpbuf.String()) <= width {
				break
			}
		}
	}
	if tmpbuf.Len() > 0 {
		words = append(words, tmpbuf.String())
	}
	return words
}

// (以下略)

このコードは <プロジェクトルート>/internal/pkg/kinsoku/kinsoku.go なので、テストを書くために <プロジェクトルート>/internal/pkg/kinsoku/kinsoku_test.go というファイルを作成する。

テスト用の関数の名前は Test+テストしたい関数 となる。
よって上記の ExecKinsoku をテストする場合、TestExecKinsoku という関数を作る。

公式チュートリアルを参考に入力値と結果を構造体で定義し、ループでテストを回してみた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package kinsoku

import (
	"reflect"
	"strings"
	"testing"
)

func TestExecKinsoku(t *testing.T) {
	width := 10

	cases := []struct {
		in   string
		want []string
	}{
		{
			"あお、〕:,゠ㇷ!.。かきくけこ",
			[]string{"あ", "お、〕:,゠", "ㇷ!.。かき", "くけこ"},
		},
		{
			"Go + Vue.js + FirebaseUI で「少人数リモートチームのためのYWT振り返り用ツール」を試作した感想",
			[]string{"Go + Vue.j", "s + Fireba", "seUI で「", "少人数リ", "モートチー", "ムのための", "YWT振り返", "り用ツー", "ル」を試作", "した感想"},
		},
	}

	for _, c := range cases {
		got := ExecKinsoku(width, c.in)
		if !reflect.DeepEqual(got, c.want) {
			t.Errorf("got:\n%v\n\nwant:\n%v", strings.Join(got[:], "\n"), strings.Join(c.want[:], "\n"))
		}
	}
}

※元のコードがまだ行頭禁則文字しか考慮できていないので変なところで改行される結果になっている

これをテストするには、プロジェクトルートでコマンドを打つなら

go test ./internal/pkg/kinsoku/kinsoku.go ./internal/pkg/kinsoku/kinsoku_test.go

<プロジェクトルート>/internal/pkg/kinsoku でコマンドを打つなら単に

go test

でいい。

結果は OK の場合は

PASS
ok      github.com/k1350/ogpgen/internal/pkg/kinsoku    0.002s

のようになる。

入力値を変更してわざと失敗させると下記のようになる。

--- FAIL: TestExecKinsoku (0.00s)
    kinsoku_test.go:29: got:
        aあ
        お、〕:,゠
        ㇷ!.。かき
        くけこ
        
        want:
        あ
        お、〕:,゠
        ㇷ!.。かき
        くけこ
FAIL
exit status 1
FAIL    github.com/k1350/ogpgen/internal/pkg/kinsoku    0.002s

この他にもベンチマーク用の関数が書けたりするが、詳しくは testing パッケージのドキュメント参照。

Package testing

感想

特にフレームワークを追加で入れなくてもテストが書けるのは便利だし、(PHP と比べたら)新しい言語だなという感触だった。

テストフレームワークの特徴としてはアサーションが無いところ。
ただ個人的には元々「アサーションいるか?」と思ってたところがあるので、無いほうがいいと思う。ただ値を比較して違ったら記録したいだけなのにアサーション用の関数を覚えるほうが面倒なので……。

あとテスト対象と同じパッケージにテストコードを置くのももしかしたら特徴的かもしれない。(別に置くことも可能。)
今回はプライベート関数のテストは書いていないが、Go では元々同一パッケージ内ならファイルが違ってもプライベート関数を呼び出せるため、プライベート関数をテストするために特殊なことはしなくていいというメリットがある。
他の言語だとプライベート関数のテストが面倒くさいところがあるので、この点はいいなと思った。

以上。