Go:ファイルに依存するコードを対象としたテストでの No such file or directory を簡単に直す

2023/04/19に公開
5

go build で得たバイナリを実行する時に、例えば bin/foobar のように実行すると、current working directoryはもちろんbinの親ディレクトリになります。

一方で、 go testテストファイルのあるディレクトリをcurrent working directoryとして実行します。

そのため、テスト対象のコードにおいて、 os.ReadFile("foo/bar/data.csv")template.ParseFiles("templates/foo/bar.txt") など、ローカルのファイルを相対パスで読み込んでいると、バイナリ実行の時は問題ないのに、テストでは No such file or directory が起きます。

そんな時は、テストの先頭に下記を貼ると直せます。

cwd, err := os.Getwd()
if err != nil {
	t.Fatalf("failed to get current working directory: %v", err)
}
t.Cleanup(func() {
	err := os.Chdir(cwd)
	if err != nil {
		t.Fatalf("failed to get current working directory: %v", err)
	}
})
err := t, os.Chdir("../..")
if err != nil {
	t.Fatalf("failed to get current working directory: %v", err)
}

Goなのでerrチェックが長くなるので、 t.Helper() な関数に切り出して置くと良いかもしれません (deferを使っていないなので可能です)。下記のようにtestify/requireを使うのもおすすめです。

cwd, err := os.Getwd()
require.NoError(t, err)
t.Cleanup(func() {
	require.NoError(t, os.Chdir(cwd))
})
require.NoError(t, os.Chdir("../.."))

テストプロセスは同じディレクトリ (package?) の他のテストでも使いまわされるので、 Cleanup() 内でcurrent working directory元に戻しておきます。

Discussion

tenkohtenkoh

こんにちは。
元々どのようなテストを実行されたのか気になります。
実際のディレクトリ構成やテストコードを例示頂くことは可能ですか?

yprestoypresto

記事のタイトル・本文でその点が曖昧だったため少し修正しました🙏
テスト自体は、関数に引数を渡して返り値を確認するものです。
例えば下記のように、テスト対象がファイルに依存している場合を指していました。

  • CSVなどに固定のデータを入れていて、読み取った結果を使う関数をテストしたい
  • メールなどのテンプレートを読み込んでいて、レンダリング結果をテストしたい

ファイルの内容に当たる文字列をテストから引数として渡せば、ファイルへの依存をなくせますが、テンプレートファイルでそれをやるのは、テストのスコープを無理に狭める側面があると思っています。

tenkohtenkoh

ありがとうございます😆
これ以上は特に追いかける気はないのですが、以下参考情報です。

要旨は「テスト時の相対パス危ない」だと理解したのですが、何か特殊なことがないと記事中のエラーにはならないと思っており、どんなことをしているか具体的に知りたいなぁと思った次第でした。

  • 私も自分の環境でいろいろ試してみましたが、テスト時の相対パス扱いで問題は生じませんでした。
  • 私(N=1)では不安ですのでGo標準パッケージのテスト関係もざっと見てみましたが、特に工夫なく相対パス読み込みを使っています。(添付の図)

テストの仕組みから上手に説明できたら良いのですが、そこまでの余裕がないので、結果系だけとなってしまい恐縮です。

https://cs.opensource.google/go/go/+/master:src/image/png/reader_test.go;l=330;drc=b5a9459cd08ec2a74ee4fdabfa4bc1eba0e87e49

yprestoypresto

こちらのGoのPNG readerのテストですが、「ファイルの内容をテストで読み込んで、テスト対象の引数に渡す」ものに見受けられます。この場合、ファイル読み込みはテストからしか実行されないので、ワーキングディレクトリは常にテストファイルのあるディレクトリです。

加筆修正した通り、わたしがエラーとなっていたのは、「テスト対象自体がファイルにアクセスする場合」で、その場合、テストから呼び出される場合と、バイナリとして起動される場合とで、ワーキングディレクトリが一致しないことがあります。

tenkohtenkoh

なるほど、理解できました。ありがとうございます👍