👨‍⚕️

go test時の環境変数の設定で使える静的解析ツールtenvを作りました

2021/10/01に公開

はじめまして、しぶちゃりです。
今回はGoの静的解析ツール tenv の紹介をさせていただきます。

Go1.17から使えるtesting.Setenv

Go1.17からtesting.Setenvというメソッドが追加されました。
Go1.17以前ではtest内で環境変数を使用する際にos.Setenvを用いていました
しかし、os.Setenvを用いた場合、テストごとの環境変数が引き継がれてしまいます。

例えば、このようなテストコードを実行してみます

package main_test

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

func TestMain(t *testing.T) {
	fmt.Println(os.Getenv("GO"))
	os.Setenv("GO", "HACKING GOPHER")
}

func TestMain2(t *testing.T) {
	fmt.Println(os.Getenv("GO"))
}
go test ./... -v

=== RUN   TestMain

--- PASS: TestMain (0.00s)
=== RUN   TestMain2
HACKING GOPHER
--- PASS: TestMain2 (0.00s)
PASS
ok  	a	0.154s

このようにTestMainで設定した環境変数がTestMain2でも引き継がれてしまっています。
上記のケースを回避するためには、事前にos.Getenvで取得した値をt.Cleanupなどで戻してあげる必要があります。

Go1.17から実装されたtesting.Setenvを用いた場合、テストが終了すると設定した環境変数が破棄されるようになります。

以下がサンプルコードです。vi

package main_test

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

func TestMain(t *testing.T) {
	fmt.Println(os.Getenv("GO"))
	t.Setenv("GO", "HACKING GOPHER")
}

func TestMain2(t *testing.T) {
	fmt.Println(os.Getenv("GO"))
}
go test ./... -v

=== RUN   TestMain

--- PASS: TestMain (0.00s)
=== RUN   TestMain2

--- PASS: TestMain2 (0.00s)
PASS
ok 

TestMain2ではTestMainで設定した値が引き継がれていないことがわかります。

tenv

僕が開発した静的解析ツールである tenv を利用することで、既存のテストコードでtesting.Setenvを用いていない箇所を検出してくれます。

デフォルトでは、 *testing.T *testing.B testing.TB を実装している箇所を全て検出します。

package main

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

func TestMain(t *testing.T) {
	fmt.Println(os.Getenv("GO"))
	os.Setenv("GO", "HACKING GOPHER")
}

func TestMain2(t *testing.T) {
	fmt.Println(os.Getenv("GO"))
}

func helper() {
	os.Setenv("GO", "HACKING GOPHER")
}

こちらのコードにtenvのチェックを入れるとこのようになります。

go vet -vettool=(which tenv) ./...

# a
./main_test.go:11:2: os.Setenv() can be replaced by `t.Setenv()` in TestMain

加えて -tenv.all というオプションをつけることで、 *_test.go のファイル内で os.Setenv を用いている箇所を全て検出します。

package main

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

func TestMain(t *testing.T) {
	fmt.Println(os.Getenv("GO"))
	os.Setenv("GO", "HACKING GOPHER")
}

func TestMain2(t *testing.T) {
	fmt.Println(os.Getenv("GO"))
}

func helper() {
	os.Setenv("GO", "HACKING GOPHER")
}
go vet -vettool=(which tenv) -tenv.all ./...

# a
./main_test.go:11:2: os.Setenv() can be replaced by `t.Setenv()` in TestMain
./main_test.go:19:2: os.Setenv() can be replaced by `testing.Setenv()` in helper

このツールはCIにも組み込むことが可能です。

CircleCI

- run:
    name: install tenv
    command: go get github.com/sivchari/tenv

- run:
    name: run tenv
    command: go vet -vettool=(which tenv) -tenv.all ./...

GitHub Actions

- name: install tenv
  run: go get github.com/sivchari/tenv

- name: run tenv
  run: go vet -vettool=(which tenv) -tenv.all ./...

golangci-lint経由でも利用可能

上記のlinterはgolangci-lintにも組み込まれているため、golangci-lint経由で利用することも可能です。

golangci-lintで実際にMergeされたPRが以下になります。
自分の作成したlinterを今後PRで出したい!と思う方の例にもなると思います。

https://github.com/golangci/golangci-lint/pull/2221

まとめ

今回ご紹介したlinterはGitHubで公開しています。

もしいいと思ったらスターをいただけるとモチベーションにつながるのでとても嬉しいです。

最後までお読みいただきありがとうございました。

https://github.com/sivchari/tenv

Discussion