Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 1238] ファイルの概要

このコミットは、Go言語のテストインフラストラクチャにおける重要な変更を示しています。具体的には、テストファイルの配置をtest/ディレクトリから対応するライブラリのsrc/lib/ディレクトリ内へ移動・整理し、Goのテストツールであるgotestのテスト選択ロジックを洗練しています。これにより、テストコードがテスト対象のコードとより密接に配置されるようになり、テストの発見と実行がより効率的かつGoの慣習に沿った形に進化しました。

コミット

commit 0f83fa3a0c306d6eb56535540a4103104bb963ac
Author: Russ Cox <rsc@golang.org>
Date:   Mon Nov 24 15:17:47 2008 -0800

    convert tests.
    refine gotest's test selection criteria.
    
    R=r
    DELTA=1590  (745 added, 844 deleted, 1 changed)
    OCL=19903
    CL=19936

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0f83fa3a0c306d6eb56535540a4103104bb963ac

元コミット内容

convert tests.
refine gotest's test selection criteria.

変更の背景

このコミットが行われた2008年後半は、Go言語がまだ初期開発段階にあり、その設計と標準ライブラリの構造が活発に進化していた時期です。初期のGoプロジェクトでは、テストコードが独立したtest/ディレクトリに配置されることが一般的でした。しかし、これはテスト対象のコードとテストコードの間に物理的な距離を生み出し、プロジェクトが大規模になるにつれてテストの管理や関連性の把握を困難にする可能性がありました。

この変更の主な背景には、以下の点が挙げられます。

  1. テストのコ・ロケーション(Co-location): Go言語の設計思想として、関連するコードは同じパッケージ内に配置するという原則があります。テストコードも例外ではなく、テスト対象のパッケージと同じディレクトリに配置することで、コードの可読性、保守性、発見性を向上させることができます。このコミットは、この原則をテストにも適用し、_test.goという命名規則を導入することで、テストファイルを標準ライブラリの各パッケージに統合する第一歩となりました。
  2. gotestツールの進化: gotestはGo言語のテスト実行ツールであり、その機能はGoの進化とともに洗練されていきました。初期のgotestはテスト関数の選択ロジックが比較的単純でしたが、このコミットでは、より厳密な正規表現を用いてテスト関数を識別するように改善されています。これにより、意図しない関数がテストとして実行されることを防ぎ、テストの信頼性と効率性を高めることを目指しています。
  3. Go言語の標準ライブラリの成熟: Goの標準ライブラリが形成されていく過程で、各パッケージが自己完結的でテスト可能であることが重要視されました。テストコードを各パッケージに移動することで、パッケージごとのテストの独立性が高まり、CI/CDパイプラインでのテスト実行も容易になります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の初期の概念とツールに関する知識が役立ちます。

  • Go言語のパッケージシステム: Goのコードはパッケージ(packageキーワードで定義)に組織されます。関連する機能は同じパッケージにまとめられ、他のパッケージからインポートして利用されます。このコミットでは、テストコードも特定のパッケージに属するように変更されています。
  • _test.goファイル: Go言語のテストフレームワークの慣習として、テストコードはテスト対象のパッケージと同じディレクトリに配置され、ファイル名が_test.goで終わる必要があります。これにより、Goツールチェーンは自動的にこれらのファイルをテストとして認識し、ビルド時に通常の実行可能ファイルとは別に扱います。
  • gotestコマンド: Go言語の公式テストツールです。go testコマンドの前身にあたります。このツールは、指定されたパッケージ内の_test.goファイルを見つけ、その中のテスト関数(TestXxxという命名規則に従う関数)を実行します。
  • 6nmコマンド: Go言語の初期のツールチェーンの一部で、オブジェクトファイル(.6ファイルなど)からシンボルテーブルをダンプするために使用されました。C言語のnmコマンドに相当します。このコミットでは、gotestスクリプト内で6nmを使用して、コンパイルされたテストバイナリからテスト関数(T .*·Testパターンにマッチするシンボル)を抽出しています。·はGoの初期のツールチェーンでパッケージパスとシンボル名を区切るために使われていた特殊文字です。
  • panictesting.T: Goの初期のテストでは、テスト失敗時にpanicを発生させていました。しかし、Goのテストフレームワークが成熟するにつれて、testingパッケージが導入され、*testing.T型のメソッド(例: t.Errorf, t.Fatalf)を使用してテスト結果を報告するようになりました。これにより、テストの失敗がより詳細に、かつ制御された方法で扱えるようになりました。このコミットは、テストコードをtestingパッケージの慣習に合わせる移行の一部でもあります。

技術的詳細

このコミットは、主に以下の3つの技術的側面で変更を加えています。

  1. gotestスクリプトのテスト選択ロジックの改善:

    • 以前は、6nmの出力からT .*·Testというパターンでテスト関数を抽出していました。これは、Testで始まるすべてのシンボルをテスト関数と見なすものでした。
    • 変更後、パターンはT .*·Test[A-Z]となり、さらにgrep -v '·.*[.·]'が追加されました。
      • Test[A-Z]は、テスト関数名がTestの後に大文字で始まることを強制します。これにより、例えばTestHelperのようなヘルパー関数が誤ってテストとして認識されることを防ぎます。
      • grep -v '·.*[.·]'は、シンボル名に複数のドット(·)が含まれるものを除外します。これは、メソッド(例: MyType.TestSomething)やその他の特殊な名前空間を持つシンボルがテスト関数として誤って選択されるのを防ぐためのものです。Goの初期のシンボル名では、パッケージ名と関数名、あるいは型名とメソッド名が·で区切られていました。このフィルタリングにより、トップレベルのテスト関数のみが選択されるようになります。
  2. テストファイルの移動とリネーム:

    • test/bufiolib.gosrc/lib/bufio_test.goにリネームされ、移動されました。これは、bufioパッケージのテストが、そのパッケージのソースコードと同じsrc/lib/ディレクトリ内に配置されるようになったことを意味します。
    • 同様に、test/sorting.gotest/stringslib.gotest/timelib.goが削除され、それぞれsrc/lib/sort_test.gosrc/lib/strings_test.gosrc/lib/time/time_test.goとして新しく作成されています。これにより、Goの標準ライブラリの各パッケージが自身のテストを内包する構造へと移行しました。
  3. テストコード自体の変更:

    • パッケージ宣言の変更: 多くのテストファイルでpackage mainからテスト対象のパッケージ名(例: package bufio)に変更されています。これは、テストが独立した実行可能ファイルとしてではなく、テスト対象のパッケージの一部としてコンパイル・実行されることを示しています。
    • main()関数の削除: テストファイルからmain()関数が削除されています。これは、gotest(後のgo test)がテストのエントリポイントを自動的に生成するため、手動でmain関数を定義する必要がなくなったことを意味します。
    • panicからt.Errorfへの移行: テスト失敗時のエラー報告が、panicからtesting.T型のメソッド(t.Errorf)を使用する形式に変更されています。これにより、テストの実行が途中で中断されることなく、複数のテスト結果をまとめて報告できるようになります。また、t.Errorfはより詳細なエラーメッセージ(テスト名、期待値、実際の結果など)を提供できるようになります。
    • テスト関数のexportキーワード: export func TestXxxという形式でテスト関数が定義されています。これはGoの初期の構文で、関数がパッケージ外から参照可能であることを示していました。後のGoのバージョンでは、関数名が大文字で始まることで自動的にエクスポートされるため、exportキーワードは不要になります。

これらの変更は、Go言語のテストフレームワークが、より現代的でGoらしい慣習(コ・ロケーション、testingパッケージの利用、自動テスト発見)へと進化していく過程の重要な一歩でした。

コアとなるコードの変更箇所

src/cmd/gotest/gotestにおけるテスト選択ロジックの変更

--- a/src/cmd/gotest/gotest
+++ b/src/cmd/gotest/gotest
@@ -55,7 +55,10 @@ trap "rm -f _testmain.go _testmain.6" 0 1 2 3 14 15
  	# test array
  	echo
  	echo 'var tests = &[]testing.Test {'
- 	for i in $(6nm -s $ofiles | grep ' T .*·Test' | sed 's/.* //; s/·/./')
+ 	# test functions are named TestFoo
+ 	# the grep -v eliminates methods and other special names
+ 	# that have multiple dots.
+ 	for i in $(6nm -s $ofiles | grep ' T .*·Test[A-Z]' | grep -v '·.*[.·]' | sed 's/.* //; s/·/./')
  	do
  		echo '		testing.Test{ "'$i'", &'$i' },'
  	done

test/bufiolib.goからsrc/lib/bufio_test.goへの変更(一部抜粋)

--- a/test/bufiolib.go
+++ b/src/lib/bufio_test.go
@@ -2,16 +2,15 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// $G $F.go && $L $F.$A && ./$A.out
-
-package main
+package bufio
 
 import (
-	"os";
-	"io";
 	"bufio";
+	"fmt";
+	"io";
+	"os";
 	"syscall";
-	"rand"
+	"testing";
 )
 
 func StringToBytes(s string) *[]byte {
@@ -186,39 +182,19 @@ var bufsizes = []int {
  	23, 32, 46, 64, 93, 128, 1024, 4096
  }
  
-func TestBufRead() {
-	// work around 6g bug101
-	readmakers[0] = &NewByteReader;
-	readmakers[1] = &NewHalfByteReader;
-
-	bufreaders[0] = &Read1;
-	bufreaders[1] = &Read2;
-	bufreaders[2] = &Read3;
-	bufreaders[3] = &Read4;
-	bufreaders[4] = &Read5;
-	bufreaders[5] = &Read7;
-	bufreaders[6] = &ReadBytes;
-	bufreaders[7] = &ReadLines;
-
-	bufsizes[0] = 1;
-	bufsizes[1] = 2;
-	bufsizes[2] = 3;
-	bufsizes[3] = 4;
-	bufsizes[4] = 5;
-	bufsizes[5] = 6;
-	bufsizes[6] = 7;
-	bufsizes[7] = 8;
-	bufsizes[8] = 9;
-	bufsizes[9] = 10;
-	bufsizes[10] = 23;
-	bufsizes[11] = 32;
-	bufsizes[12] = 46;
-	bufsizes[13] = 64;
-	bufsizes[14] = 93;
-	bufsizes[15] = 128;
-	bufsizes[16] = 1024;
-	bufsizes[17] = 4096;
+export func TestBufReadSimple(t *testing.T) {
+	b, e := NewBufRead(NewByteReader(StringToBytes("hello world")));
+	if s := ReadBytes(b); s != "hello world" {
+		t.Errorf("simple hello world test failed: got %q", s);
+	}
+
+	b, e = NewBufRead(NewRot13Reader(NewByteReader(StringToBytes("hello world"))));
+	if s := ReadBytes(b); s != "uryyb jbeyq" {
+		t.Error("rot13 hello world test failed: got %q", s);
+	}
+}
  
+export func TestBufRead(t *testing.T) {
  	var texts [31]string;
  	str := "";
  	all := "";
@@ -229,33 +205,21 @@ func TestBufRead() {
  	}
  	texts[len(texts)-1] = all;
  
-	// BUG 6g should not need nbr temporary (bug099)
-	nbr := NewByteReader(StringToBytes("hello world"));
-	b, e := bufio.NewBufRead(nbr);
-	if ReadBytes(b) != "hello world" { panic("hello world") }
-
-	// BUG 6g should not need nbr nor nbr1 (bug009)
-	nbr = NewByteReader(StringToBytes("hello world"));
-	nbr1 := NewRot13Reader(nbr);
-	b, e = bufio.NewBufRead(nbr1);
-	if ReadBytes(b) != "uryyb jbeyq" { panic("hello world") }
-
  	for h := 0; h < len(texts); h++ {
  		text := texts[h];
  		textbytes := StringToBytes(text);
  		for i := 0; i < len(readmakers); i++ {
- 			readmaker := readmakers[i];
  			for j := 0; j < len(bufreaders); j++ {
- 				bufreader := bufreaders[j];
  				for k := 0; k < len(bufsizes); k++ {
+ 					readmaker := readmakers[i];
+ 					bufreader := bufreaders[j];
  					bufsize := bufsizes[k];
- 					read := readmaker(textbytes);
- 					buf, e := bufio.NewBufReadSize(read, bufsize);
- 					s := bufreader(buf);
+ 					read := readmaker.fn(textbytes);
+ 					buf, e := NewBufReadSize(read, bufsize);
+ 					s := bufreader.fn(buf);
  					if s != text {
- 						print("Failed: ", h, " ", i, " ", j, " ", k, " ", len(s), " ", len(text), "\n");
- 						print("<", s, ">\nshould be <", text, ">\n");
- 						panic("bufio result")
+ 						t.Errorf("reader=%s fn=%s bufsize=%d want=%q got=%q",
+ 							readmaker.name, bufreader.name, bufsize, text, s);
  					}
  				}
  			}
@@ -370,8 +324,3 @@ func TestBufWrite() {
  	}
  }
  
-
-func main() {
-	TestBufRead();
-	TestBufWrite()
-}

コアとなるコードの解説

src/cmd/gotest/gotestの変更

このシェルスクリプトは、Goのテストバイナリからテスト関数を抽出し、それらをtesting.Test構造体の配列として_testmain.goというファイルに書き出す役割を担っています。この_testmain.goが、最終的にテストを実行するメイン関数を含むバイナリとしてコンパイルされます。

変更前は、grep ' T .*·Test'という正規表現で、シンボル名にTestを含むすべてのシンボルをテスト関数として認識していました。これは、例えばTestという文字列を含むだけのヘルパー関数なども誤ってテストとして扱ってしまう可能性がありました。

変更後は、grep ' T .*·Test[A-Z]' | grep -v '·.*[.·]'というより厳密なフィルタリングが導入されました。

  • grep ' T .*·Test[A-Z]': Testの直後に大文字が続くシンボルのみを対象とします。これはGoのテスト関数の命名規則(例: TestMyFeature)に厳密に合致させ、Testで始まるがテスト関数ではないシンボル(例: TestUtilのようなヘルパー関数)を除外します。
  • grep -v '·.*[.·]': シンボル名に複数のドット(·)が含まれる行を除外します。Goの初期のシンボル名では、メソッドはType·Methodのように表現され、パッケージ内のグローバル関数とは異なる形式でした。このフィルタリングにより、gotestはトップレベルのテスト関数(例: package.TestFunction)のみを抽出し、メソッド(例: package.Type·Method)が誤ってテストとして実行されるのを防ぎます。

この変更により、gotestはより正確にテスト関数を識別できるようになり、テスト実行の信頼性と効率が向上しました。

test/bufiolib.goからsrc/lib/bufio_test.goへの変更

このファイルは、bufioパッケージのテストコードです。変更は、Goのテストフレームワークの進化と、テストコードの配置に関する慣習の変化を明確に示しています。

  1. パッケージ宣言の変更:

    • 変更前: package main
    • 変更後: package bufio これは最も重要な変更点の一つです。以前は、テストファイルが独立した実行可能ファイル(mainパッケージ)としてコンパイルされ、main()関数からテストが手動で呼び出されていました。変更後は、テストファイルがテスト対象のbufioパッケージの一部として扱われるようになり、gotest(またはgo test)が自動的にテスト関数を検出し、実行するようになりました。これにより、テストの記述と実行が大幅に簡素化されます。
  2. main()関数の削除:

    • 変更前は、ファイルの最後にmain()関数があり、その中でTestBufRead()TestBufWrite()といったテスト関数を明示的に呼び出していました。
    • 変更後は、main()関数が完全に削除されています。これは、Goのテストツールが自動的にテスト関数を検出し、実行するためのエントリポイントを生成するため、ユーザーが手動でmain関数を記述する必要がなくなったためです。
  3. エラー報告の変更(panicからt.Errorfへ):

    • 変更前は、テストが失敗した場合にpanic()を呼び出してプログラムを強制終了させていました。これは、テストが一つ失敗するとそれ以降のテストが実行されないという問題がありました。
    • 変更後、testingパッケージがインポートされ、テスト関数は*testing.T型の引数tを受け取るようになりました。テスト失敗時にはt.Errorf()(またはt.Error())が呼び出されます。
      • t.Errorf()は、エラーメッセージを記録しますが、テストの実行を即座に停止させません。これにより、一つのテスト関数内で複数のアサーションが失敗した場合でも、すべて報告されるようになります。
      • また、t.Errorf("reader=%s fn=%s bufsize=%d want=%q got=%q", ...)のように、より詳細なコンテキスト情報(どのリーダー、どの関数、バッファサイズ、期待値、実際の結果)を含むエラーメッセージを出力できるようになり、デバッグが容易になります。
  4. テスト関数の命名とexport:

    • TestBufReadSimpleTestBufReadTestBufWriteといった関数がexport funcとして定義されています。これはGoの初期の構文で、関数が外部から参照可能であることを示していました。Goのテストツールは、Testで始まり、その後に大文字が続く関数を自動的にテスト関数として認識します。

これらの変更は、Go言語のテストが、より堅牢で、情報量が多く、自動化されたフレームワークへと移行する過程を示しており、現代のGo開発におけるテストのベストプラクティスの基礎を築きました。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Go言語のtestingパッケージ: https://pkg.go.dev/testing (現在のドキュメントですが、基本的な概念は共通しています)
  • Go言語の初期の歴史に関する情報源 (例: Goのブログ記事、カンファレンストークなど)

参考にした情報源リンク

  • Go言語の公式Gitリポジトリ: https://github.com/golang/go
  • コミットハッシュ: 0f83fa3a0c306d6eb56535540a4103104bb963acのdiff情報
  • Go言語の初期のツールチェーンに関する情報 (例: 6nmコマンドの役割など) - これは一般的なWeb検索やGoの歴史に関する記事から得られる情報です。
  • Go言語のテストの進化に関する記事やドキュメント。

[インデックス 1238] ファイルの概要

このコミットは、Go言語のテストインフラストラクチャにおける重要な変更を示しています。具体的には、テストファイルの配置をtest/ディレクトリから対応するライブラリのsrc/lib/ディレクトリ内へ移動・整理し、Goのテストツールであるgotestのテスト選択ロジックを洗練しています。これにより、テストコードがテスト対象のコードとより密接に配置されるようになり、テストの発見と実行がより効率的かつGoの慣習に沿った形に進化しました。

コミット

commit 0f83fa3a0c306d6eb56535540a4103104bb963ac
Author: Russ Cox <rsc@golang.org>
Date:   Mon Nov 24 15:17:47 2008 -0800

    convert tests.
    refine gotest's test selection criteria.
    
    R=r
    DELTA=1590  (745 added, 844 deleted, 1 changed)
    OCL=19903
    CL=19936

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0f83fa3a0c306d6eb56535540a4103104bb963ac

元コミット内容

convert tests.
refine gotest's test selection criteria.

変更の背景

このコミットが行われた2008年後半は、Go言語がまだ初期開発段階にあり、その設計と標準ライブラリの構造が活発に進化していた時期です。初期のGoプロジェクトでは、テストコードが独立したtest/ディレクトリに配置されることが一般的でした。しかし、これはテスト対象のコードとテストコードの間に物理的な距離を生み出し、プロジェクトが大規模になるにつれてテストの管理や関連性の把握を困難にする可能性がありました。

この変更の主な背景には、以下の点が挙げられます。

  1. テストのコ・ロケーション(Co-location): Go言語の設計思想として、関連するコードは同じパッケージ内に配置するという原則があります。テストコードも例外ではなく、テスト対象のパッケージと同じディレクトリに配置することで、コードの可読性、保守性、発見性を向上させることができます。このコミットは、この原則をテストにも適用し、_test.goという命名規則を導入することで、テストファイルを標準ライブラリの各パッケージに統合する第一歩となりました。
  2. gotestツールの進化: gotestはGo言語のテスト実行ツールであり、その機能はGoの進化とともに洗練されていきました。初期のgotestはテスト関数の選択ロジックが比較的単純でしたが、このコミットでは、より厳密な正規表現を用いてテスト関数を識別するように改善されています。これにより、意図しない関数がテストとして実行されることを防ぎ、テストの信頼性と効率性を高めることを目指しています。
  3. Go言語の標準ライブラリの成熟: Goの標準ライブラリが形成されていく過程で、各パッケージが自己完結的でテスト可能であることが重要視されました。テストコードを各パッケージに移動することで、パッケージごとのテストの独立性が高まり、CI/CDパイプラインでのテスト実行も容易になります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の初期の概念とツールに関する知識が役立ちます。

  • Go言語のパッケージシステム: Goのコードはパッケージ(packageキーワードで定義)に組織されます。関連する機能は同じパッケージにまとめられ、他のパッケージからインポートして利用されます。このコミットでは、テストコードも特定のパッケージに属するように変更されています。
  • _test.goファイル: Go言語のテストフレームワークの慣習として、テストコードはテスト対象のパッケージと同じディレクトリに配置され、ファイル名が_test.goで終わる必要があります。これにより、Goツールチェーンは自動的にこれらのファイルをテストとして認識し、ビルド時に通常の実行可能ファイルとは別に扱います。
  • gotestコマンド: Go言語の公式テストツールです。go testコマンドの前身にあたります。このツールは、指定されたパッケージ内の_test.goファイルを見つけ、その中のテスト関数(TestXxxという命名規則に従う関数)を実行します。
  • 6nmコマンド: Go言語の初期のツールチェーンの一部で、オブジェクトファイル(.6ファイルなど)からシンボルテーブルをダンプするために使用されました。C言語のnmコマンドに相当します。このコミットでは、gotestスクリプト内で6nmを使用して、コンパイルされたテストバイナリからテスト関数(T .*·Testパターンにマッチするシンボル)を抽出しています。·はGoの初期のツールチェーンでパッケージパスとシンボル名を区切るために使われていた特殊文字です。
  • panictesting.T: Goの初期のテストでは、テスト失敗時にpanicを発生させていました。しかし、Goのテストフレームワークが成熟するにつれて、testingパッケージが導入され、*testing.T型のメソッド(例: t.Errorf, t.Fatalf)を使用してテスト結果を報告するようになりました。これにより、テストの失敗がより詳細に、かつ制御された方法で扱えるようになりました。このコミットは、テストコードをtestingパッケージの慣習に合わせる移行の一部でもあります。

技術的詳細

このコミットは、主に以下の3つの技術的側面で変更を加えています。

  1. gotestスクリプトのテスト選択ロジックの改善:

    • 以前は、6nmの出力からT .*·Testというパターンでテスト関数を抽出していました。これは、Testで始まるすべてのシンボルをテスト関数と見なすものでした。
    • 変更後、パターンはT .*·Test[A-Z]となり、さらにgrep -v '·.*[.·]'が追加されました。
      • Test[A-Z]は、テスト関数名がTestの後に大文字で始まることを強制します。これにより、例えばTestHelperのようなヘルパー関数が誤ってテストとして認識されることを防ぎます。
      • grep -v '·.*[.·]'は、シンボル名に複数のドット(·)が含まれるものを除外します。これは、メソッド(例: MyType.TestSomething)やその他の特殊な名前空間を持つシンボルがテスト関数として誤って選択されるのを防ぐためのものです。Goの初期のシンボル名では、パッケージ名と関数名、あるいは型名とメソッド名が·で区切られていました。このフィルタリングにより、トップレベルのテスト関数のみが選択されるようになります。
  2. テストファイルの移動とリネーム:

    • test/bufiolib.gosrc/lib/bufio_test.goにリネームされ、移動されました。これは、bufioパッケージのテストが、そのパッケージのソースコードと同じsrc/lib/ディレクトリ内に配置されるようになったことを意味します。
    • 同様に、test/sorting.gotest/stringslib.gotest/timelib.goが削除され、それぞれsrc/lib/sort_test.gosrc/lib/strings_test.gosrc/lib/time/time_test.goとして新しく作成されています。これにより、Goの標準ライブラリの各パッケージが自身のテストを内包する構造へと移行しました。
  3. テストコード自体の変更:

    • パッケージ宣言の変更: 多くのテストファイルでpackage mainからテスト対象のパッケージ名(例: package bufio)に変更されています。これは、テストが独立した実行可能ファイルとしてではなく、テスト対象のパッケージの一部としてコンパイル・実行されることを示しています。
    • main()関数の削除: テストファイルからmain()関数が削除されています。これは、gotest(後のgo test)がテストのエントリポイントを自動的に生成するため、手動でmain関数を定義する必要がなくなったことを意味します。
    • panicからt.Errorfへの移行: テスト失敗時のエラー報告が、panicからtesting.T型のメソッド(t.Errorf)を使用する形式に変更されています。これにより、テストの実行が途中で中断されることなく、複数のテスト結果をまとめて報告できるようになります。また、t.Errorfはより詳細なエラーメッセージ(テスト名、期待値、実際の結果など)を提供できるようになります。
    • テスト関数のexportキーワード: export func TestXxxという形式でテスト関数が定義されています。これはGoの初期の構文で、関数がパッケージ外から参照可能であることを示していました。後のGoのバージョンでは、関数名が大文字で始まることで自動的にエクスポートされるため、exportキーワードは不要になります。

これらの変更は、Go言語のテストフレームワークが、より現代的でGoらしい慣習(コ・ロケーション、testingパッケージの利用、自動テスト発見)へと進化していく過程の重要な一歩でした。

コアとなるコードの変更箇所

src/cmd/gotest/gotestにおけるテスト選択ロジックの変更

--- a/src/cmd/gotest/gotest
+++ b/src/cmd/gotest/gotest
@@ -55,7 +55,10 @@ trap "rm -f _testmain.go _testmain.6" 0 1 2 3 14 15
  	# test array
  	echo
  	echo 'var tests = &[]testing.Test {'
- 	for i in $(6nm -s $ofiles | grep ' T .*·Test' | sed 's/.* //; s/·/./')
+ 	# test functions are named TestFoo
+ 	# the grep -v eliminates methods and other special names
+ 	# that have multiple dots.
+ 	for i in $(6nm -s $ofiles | grep ' T .*·Test[A-Z]' | grep -v '·.*[.·]' | sed 's/.* //; s/·/./')
  	do
  		echo '		testing.Test{ "'$i'", &'$i' },'
  	done

test/bufiolib.goからsrc/lib/bufio_test.goへの変更(一部抜粋)

--- a/test/bufiolib.go
+++ b/src/lib/bufio_test.go
@@ -2,16 +2,15 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// $G $F.go && $L $F.$A && ./$A.out
-
-package main
+package bufio
 
 import (
-	"os";
-	"io";
 	"bufio";
+	"fmt";
+	"io";
+	"os";
 	"syscall";
-	"rand"
+	"testing";
 )
 
 func StringToBytes(s string) *[]byte {
@@ -186,39 +182,19 @@ var bufsizes = []int {
  	23, 32, 46, 64, 93, 128, 1024, 4096
  }
  
-func TestBufRead() {
-	// work around 6g bug101
-	readmakers[0] = &NewByteReader;
-	readmakers[1] = &NewHalfByteReader;
-
-	bufreaders[0] = &Read1;
-	bufreaders[1] = &Read2;
-	bufreaders[2] = &Read3;
-	bufreaders[3] = &Read4;
-	bufreaders[4] = &Read5;
-	bufreaders[5] = &Read7;
-	bufreaders[6] = &ReadBytes;
-	bufreaders[7] = &ReadLines;
-
-	bufsizes[0] = 1;
-	bufsizes[1] = 2;
-	bufsizes[2] = 3;
-	bufsizes[3] = 4;
-	bufsizes[4] = 5;
-	bufsizes[5] = 6;
-	bufsizes[6] = 7;
-	bufsizes[7] = 8;
-	bufsizes[8] = 9;
-	bufsizes[9] = 10;
-	bufsizes[10] = 23;
-	bufsizes[11] = 32;
-	bufsizes[12] = 46;
-	bufsizes[13] = 64;
-	bufsizes[14] = 93;
-	bufsizes[15] = 128;
-	bufsizes[16] = 1024;
-	bufsizes[17] = 4096;
+export func TestBufReadSimple(t *testing.T) {
+	b, e := NewBufRead(NewByteReader(StringToBytes("hello world")));
+	if s := ReadBytes(b); s != "hello world" {
+		t.Errorf("simple hello world test failed: got %q", s);
+	}
+
+	b, e = NewBufRead(NewRot13Reader(NewByteReader(StringToBytes("hello world"))));
+	if s := ReadBytes(b); s != "uryyb jbeyq" {
+		t.Error("rot13 hello world test failed: got %q", s);
+	}
+}
  
+export func TestBufRead(t *testing.T) {
  	var texts [31]string;
  	str := "";
  	all := "";
@@ -229,33 +205,21 @@ func TestBufRead() {
  	}
  	texts[len(texts)-1] = all;
  
-	// BUG 6g should not need nbr temporary (bug099)
-	nbr := NewByteReader(StringToBytes("hello world"));
-	b, e := bufio.NewBufRead(nbr);
-	if ReadBytes(b) != "hello world" { panic("hello world") }
-
-	// BUG 6g should not need nbr nor nbr1 (bug009)
-	nbr = NewByteReader(StringToBytes("hello world"));
-	nbr1 := NewRot13Reader(nbr);
-	b, e = bufio.NewBufRead(nbr1);
-	if ReadBytes(b) != "uryyb jbeyq" { panic("hello world") }
-
  	for h := 0; h < len(texts); h++ {
  		text := texts[h];
  		textbytes := StringToBytes(text);
  		for i := 0; i < len(readmakers); i++ {
- 			readmaker := readmakers[i];
  			for j := 0; j < len(bufreaders); j++ {
- 				bufreader := bufreaders[j];
  				for k := 0; k < len(bufsizes); k++ {
+ 					readmaker := readmakers[i];
+ 					bufreader := bufreaders[j];
  					bufsize := bufsizes[k];
- 					read := readmaker(textbytes);
- 					buf, e := bufio.NewBufReadSize(read, bufsize);
- 					s := bufreader(buf);
+ 					read := readmaker.fn(textbytes);
+ 					buf, e := NewBufReadSize(read, bufsize);
+ 					s := bufreader.fn(buf);
  					if s != text {
- 						print("Failed: ", h, " ", i, " ", j, " ", k, " ", len(s), " ", len(text), "\n");
- 						print("<", s, ">\nshould be <", text, ">\n");
- 						panic("bufio result")
+ 						t.Errorf("reader=%s fn=%s bufsize=%d want=%q got=%q",
+ 							readmaker.name, bufreader.name, bufsize, text, s);
  					}
  				}
  			}
@@ -370,8 +324,3 @@ func TestBufWrite() {
  	}
  }
  
-
-func main() {
-	TestBufRead();
-	TestBufWrite()
-}

コアとなるコードの解説

src/cmd/gotest/gotestの変更

このシェルスクリプトは、Goのテストバイナリからテスト関数を抽出し、それらをtesting.Test構造体の配列として_testmain.goというファイルに書き出す役割を担っています。この_testmain.goが、最終的にテストを実行するメイン関数を含むバイナリとしてコンパイルされます。

変更前は、grep ' T .*·Test'という正規表現で、シンボル名にTestを含むすべてのシンボルをテスト関数として認識していました。これは、例えばTestという文字列を含むだけのヘルパー関数なども誤ってテストとして扱ってしまう可能性がありました。

変更後は、grep ' T .*·Test[A-Z]' | grep -v '·.*[.·]'というより厳密なフィルタリングが導入されました。

  • grep ' T .*·Test[A-Z]': Testの直後に大文字が続くシンボルのみを対象とします。これはGoのテスト関数の命名規則(例: TestMyFeature)に厳密に合致させ、Testで始まるがテスト関数ではないシンボル(例: TestUtilのようなヘルパー関数)を除外します。
  • grep -v '·.*[.·]': シンボル名に複数のドット(·)が含まれる行を除外します。Goの初期のシンボル名では、メソッドはType·Methodのように表現され、パッケージ内のグローバル関数とは異なる形式でした。このフィルタリングにより、gotestはトップレベルのテスト関数(例: package.TestFunction)のみを抽出し、メソッド(例: package.Type·Method)が誤ってテストとして実行されるのを防ぎます。

この変更により、gotestはより正確にテスト関数を識別できるようになり、テスト実行の信頼性と効率が向上しました。

test/bufiolib.goからsrc/lib/bufio_test.goへの変更

このファイルは、bufioパッケージのテストコードです。変更は、Goのテストフレームワークの進化と、テストコードの配置に関する慣習の変化を明確に示しています。

  1. パッケージ宣言の変更:

    • 変更前: package main
    • 変更後: package bufio これは最も重要な変更点の一つです。以前は、テストファイルが独立した実行可能ファイル(mainパッケージ)としてコンパイルされ、main()関数からテストが手動で呼び出されていました。変更後は、テストファイルがテスト対象のbufioパッケージの一部として扱われるようになり、gotest(またはgo test)が自動的にテスト関数を検出し、実行するようになりました。これにより、テストの記述と実行が大幅に簡素化されます。
  2. main()関数の削除:

    • 変更前は、ファイルの最後にmain()関数があり、その中でTestBufRead()TestBufWrite()といったテスト関数を明示的に呼び出していました。
    • 変更後は、main()関数が完全に削除されています。これは、Goのテストツールが自動的にテスト関数を検出し、実行するためのエントリポイントを生成するため、ユーザーが手動でmain関数を記述する必要がなくなったためです。
  3. エラー報告の変更(panicからt.Errorfへ):

    • 変更前は、テストが失敗した場合にpanic()を呼び出してプログラムを強制終了させていました。これは、テストが一つ失敗するとそれ以降のテストが実行されないという問題がありました。
    • 変更後、testingパッケージがインポートされ、テスト関数は*testing.T型の引数tを受け取るようになりました。テスト失敗時にはt.Errorf()(またはt.Error())が呼び出されます。
      • t.Errorf()は、エラーメッセージを記録しますが、テストの実行を即座に停止させません。これにより、一つのテスト関数内で複数のアサーションが失敗した場合でも、すべて報告されるようになります。
      • また、t.Errorf("reader=%s fn=%s bufsize=%d want=%q got=%q", ...)のように、より詳細なコンテキスト情報(どのリーダー、どの関数、バッファサイズ、期待値、実際の結果)を含むエラーメッセージを出力できるようになり、デバッグが容易になります。
  4. テスト関数の命名とexport:

    • TestBufReadSimpleTestBufReadTestBufWriteといった関数がexport funcとして定義されています。これはGoの初期の構文で、関数が外部から参照可能であることを示していました。Goのテストツールは、Testで始まり、その後に大文字が続く関数を自動的にテスト関数として認識します。

これらの変更は、Go言語のテストが、より堅牢で、情報量が多く、自動化されたフレームワークへと移行する過程を示しており、現代のGo開発におけるテストのベストプラクティスの基礎を築きました。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Go言語のtestingパッケージ: https://pkg.go.dev/testing (現在のドキュメントですが、基本的な概念は共通しています)
  • Go言語の初期の歴史に関する情報源 (例: Goのブログ記事、カンファレンストークなど)

参考にした情報源リンク

  • Go言語の公式Gitリポジトリ: https://github.com/golang/go
  • コミットハッシュ: 0f83fa3a0c306d6eb56535540a4103104bb963acのdiff情報
  • Go言語の初期のツールチェーンに関する情報 (例: 6nmコマンドの役割など) - これは一般的なWeb検索やGoの歴史に関する記事から得られる情報です。
  • Go言語のテストの進化に関する記事やドキュメント。