🧪

【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.

ざっくり要約すると、

  1. Setenv メソッドでは内部で os.Setenv(key, value) を呼んでいる
  2. 同じく内部で Cleanup メソッドを内部で呼び、環境変数を元に値に戻せる
  3. 並行テストでは用いることができない
    このような特徴があることがわかります。

T.Setenvを使って環境変数を設定しよう!

今回は、テーブルテストにおいて特定のテストだけ実施し、それ以外はスキップするというケースで実装します。

main_test.go
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.

ざっくり要約すると、

  1. Setenv メソッドでは内部で os.Setenv(key, value) を呼んでいる
  2. 同じく内部で Cleanup メソッドを内部で呼び、環境変数を元に値に戻せる
  3. 並行テストでは用いることができない

このような特徴があることがわかります。T.Setenv と同じですね。

B.Setenvを使って環境変数を設定しよう!

今回は、特定のベンチマークテストだけ実施し、それ以外はスキップするというケースで実装します。

main_test.go
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 を使うことで、簡単にベンチマークテストで扱う環境変数を設定することができる
  • どちらもテスト終了後に自動で元の値へ戻るため、安全に一時的な環境変数を設定できる
  • どちらも並行実行されるテストでは利用できない点には注意が必要

次回は「一時ディレクトリの作成」に入門します!

参考

https://pkg.go.dev/testing#T.Setenv
https://pkg.go.dev/testing#B.Setenv
https://go.dev/doc/go1.17#testingpkgtesting
https://future-architect.github.io/articles/20250509a/#環境変数を設定したい
https://blog.mmmcorp.co.jp/2021/09/09/golang1-17/

Discussion