【Go】Goのテストに入門してみた! ~環境変数編~
はじめに
前回は「httptest」を見ていきました。
今回は「環境変数のセット」に入門します!
前回の記事はこちら!
この記事でわかること
- Goのユニットテストにおける環境変数のセットの仕方と
T.Setenvの特徴 - Goのベンチマークテストにおける環境変数のセットの仕方と
B.Setenvの特徴
ユニットテストで環境変数を設定するには?
テストをする際に、環境変数に依存した処理をテストすることがあるかと思います。
ユニットテストで一時的な環境変数を設定するには、Go 1.17から導入された T.Setenv を使います。
T.Setenv のドキュメントには以下の文言が記載されています。
Setenv calls os.Setenv(key, value) and uses Cleanup to restore the environment variable to its original value after the test.
Because Setenv affects the whole process, it cannot be used in parallel tests or tests with parallel ancestors.
ざっくり要約すると、
Setenvメソッドでは内部でos.Setenv(key, value)を呼んでいる- 同じく内部で
Cleanupメソッドを内部で呼び、環境変数を元に値に戻せる -
並行テストでは用いることができない
このような特徴があることがわかります。
T.Setenvを使って環境変数を設定しよう!
今回は、テーブルテストにおいて特定のテストだけ実施し、それ以外はスキップするというケースで実装します。
package main
import (
"os"
"testing"
"unicode/utf8"
)
func Length(s string) int {
return utf8.RuneCountInString(s)
}
func TestLength(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{"ascii", "Hello", 5},
{"full-width-japanese", "こんにちは", 5},
{"half-width-japanese", "コンニチハ", 5},
{"emoji", "😃😃😃", 3},
{"empty", "", 0},
}
t.Setenv("TEST_TARGET", "emoji")
target := os.Getenv("TEST_TARGET")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.name != target {
t.Skipf("Skipping test %s because TARGET_TEST=%s", tt.name, target)
}
if got := Length(tt.input); got != tt.want {
t.Errorf("Length = %d, want %d", got, tt.want)
}
})
}
}
「emoji」という名前のテストだけ実施するようにしています。
実行すると、以下のような結果が得られます。
# go test -v
=== RUN TestLength
=== RUN TestLength/ascii
main_test.go:30: Skipping test ascii because TARGET_TEST=emoji
=== RUN TestLength/full-width-japanese
main_test.go:30: Skipping test full-width-japanese because TARGET_TEST=emoji
=== RUN TestLength/half-width-japanese
main_test.go:30: Skipping test half-width-japanese because TARGET_TEST=emoji
=== RUN TestLength/emoji
=== RUN TestLength/empty
main_test.go:30: Skipping test empty because TARGET_TEST=emoji
--- PASS: TestLength (0.00s)
--- SKIP: TestLength/ascii (0.00s)
--- SKIP: TestLength/full-width-japanese (0.00s)
--- SKIP: TestLength/half-width-japanese (0.00s)
--- PASS: TestLength/emoji (0.00s)
--- SKIP: TestLength/empty (0.00s)
PASS
ok go-test-practice 0.296s
「emoji」という名前のテストだけ実行され、それ以外はスキップされていることがわかります。
環境変数が一時的に読み込まれ、使用されていることがわかります。
ベンチマークテストで環境変数を設定するには?
ベンチマークテストで一時的な環境変数を設定するには、Go 1.17から導入された B.Setenv を使います。
(T.Setenv はユニットテストで一時的な環境変数を設定するメソッドです)
B.Setenv のドキュメントには以下の文言が記載されています。
Setenv calls os.Setenv and uses Cleanup to restore the environment variable to its original value after the test.
Because Setenv affects the whole process, it cannot be used in parallel tests or tests with parallel ancestors.
ざっくり要約すると、
-
Setenvメソッドでは内部でos.Setenv(key, value)を呼んでいる - 同じく内部で
Cleanupメソッドを内部で呼び、環境変数を元に値に戻せる - 並行テストでは用いることができない
このような特徴があることがわかります。T.Setenv と同じですね。
B.Setenvを使って環境変数を設定しよう!
今回は、特定のベンチマークテストだけ実施し、それ以外はスキップするというケースで実装します。
package main
import (
"fmt"
"os"
"strings"
"testing"
)
const N = 100000
func generateStrings() []string {
s := make([]string, N)
for i := 0; i < N; i++ {
s[i] = "Hello"
}
return s
}
func BenchmarkStringConcatTable(b *testing.B) {
s := generateStrings()
benchmarks := []struct {
name string
fn func([]string)
}{
{
name: "Plus",
fn: func(s []string) {
result := ""
for _, v := range s {
result += v + " "
}
_ = result
},
},
{
name: "Sprintf",
fn: func(s []string) {
result := ""
for _, v := range s {
result = fmt.Sprintf("%s %s", result, v)
}
_ = result
},
},
{
name: "Join",
fn: func(s []string) {
_ = strings.Join(s, " ")
},
},
{
name: "Builder",
fn: func(s []string) {
var sb strings.Builder
for _, v := range s {
sb.WriteString(v)
sb.WriteString(" ")
}
_ = sb.String()
},
},
}
b.Setenv("BENCH_TARGET", "Join")
target := os.Getenv("BENCH_TARGET")
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
if bm.name != target {
b.Skipf("Skipping benchmark %s because BENCH_TARGET=%s", bm.name, target)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
bm.fn(s)
}
})
}
}
「Join」という名前のテストだけ実施するようにしています。
実行すると、以下のような結果が得られます。
# go test -v -benchmem -bench .
goos: darwin
goarch: arm64
pkg: go-test-practice
cpu: Apple M1
BenchmarkStringConcatTable
BenchmarkStringConcatTable/Plus
main_test.go:72: Skipping benchmark Plus because BENCH_TARGET=Join
--- SKIP: BenchmarkStringConcatTable/Plus
BenchmarkStringConcatTable/Sprintf
main_test.go:72: Skipping benchmark Sprintf because BENCH_TARGET=Join
--- SKIP: BenchmarkStringConcatTable/Sprintf
BenchmarkStringConcatTable/Join
BenchmarkStringConcatTable/Join-8 1653 712172 ns/op 606208 B/op 1 allocs/op
BenchmarkStringConcatTable/Builder
main_test.go:72: Skipping benchmark Builder because BENCH_TARGET=Join
--- SKIP: BenchmarkStringConcatTable/Builder
PASS
ok go-test-practice 2.328s
「Join」という名前のベンチマークテストだけ実行され、それ以外はスキップされていることがわかります。
環境変数が一時的に読み込まれ、使用されていることがわかります。
まとめ
今回は「環境変数のセット」について学びました。
- Go 1.17で追加された
T.Setenvを使うことで、簡単にユニットテストで扱う環境変数を設定することができる - Go 1.17で追加された
B.Setenvを使うことで、簡単にベンチマークテストで扱う環境変数を設定することができる - どちらもテスト終了後に自動で元の値へ戻るため、安全に一時的な環境変数を設定できる
- どちらも並行実行されるテストでは利用できない点には注意が必要
次回は「一時ディレクトリの作成」に入門します!
参考
Discussion