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

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

このコミットは、Go言語の標準ライブラリにおけるテストファイルのパッケージ構造を変更し、可能な限りテストを対象パッケージと同じパッケージ("internal"テスト)に配置するように修正するものです。これにより、パッケージ内部の非公開(unexported)な詳細に対するテストが容易になります。

コミット

commit d137a2cb564c50ba104b5699c2a34ad393976564
Author: Robin Eklind <r.eklind.87@gmail.com>
Date:   Tue Feb 19 10:02:01 2013 -0500

    src: use internal tests if possible
    
    If a test can be placed in the same package ("internal"), it is placed
    there. This facilitates testing of package-private details. Because of
    dependency cycles some packages cannot be tested by internal tests.
    
    R=golang-dev, rsc, mikioh.mikioh
    CC=golang-dev, r
    https://golang.org/cl/7323044

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

https://github.com/golang/go/commit/d137a2cb564c50ba104b5699c2a34ad393976564

元コミット内容

src: use internal tests if possible

テストが同じパッケージ("internal")に配置できる場合、そこに配置されます。これにより、パッケージプライベートな詳細のテストが容易になります。依存関係の循環のため、一部のパッケージは内部テストでテストできません。

変更の背景

Go言語のテストでは、通常、テスト対象のパッケージとは異なるパッケージ名(例: package foo_test)を持つテストファイルを作成することが一般的です。これは、テストがパッケージの公開(exported)APIのみを対象とすることを意図しているためです。しかし、この方法では、パッケージ内部の非公開(unexported)な関数や変数、構造体フィールドなどを直接テストすることができません。

このコミットの背景には、Goのテストフレームワークにおけるこの制約を緩和し、パッケージ内部の実装詳細に対するテストカバレッジを向上させたいという意図があります。特に、複雑なロジックを持つ非公開関数や、内部状態の検証が必要な場合に、外部テストでは限界がありました。

この変更により、テストファイルが対象パッケージと同じパッケージ名(例: package foo)を持つ「内部テスト」として機能するようになります。これにより、テストコードが対象パッケージの非公開識別子にアクセスできるようになり、より詳細な単体テストが可能になります。

ただし、コミットメッセージにもあるように、依存関係の循環(dependency cycles)が存在する一部のパッケージでは、この内部テストの方式を適用できない場合があります。これは、テストファイルが対象パッケージと同じパッケージに属することで、新たな依存関係の循環が発生し、ビルドエラーを引き起こす可能性があるためです。

前提知識の解説

Go言語のパッケージと可視性

Go言語では、コードは「パッケージ」という単位で組織されます。パッケージは、関連する機能や型、関数などをまとめたものです。Goのソースファイルは、必ず package <パッケージ名> という宣言で始まります。

Goにおける識別子(変数、関数、型など)の可視性(スコープ)は、その識別子の名前の最初の文字が大文字か小文字かで決まります。

  • 大文字で始まる識別子: パッケージの外部からアクセス可能です(公開、exported)。
  • 小文字で始まる識別子: パッケージの内部からのみアクセス可能です(非公開、unexported)。

Go言語のテストファイルとパッケージ

Goのテストファイルは、慣習的に _test.go というサフィックスを持ちます。Goのテストフレームワークは、これらのファイルを自動的に認識し、テストを実行します。

Goのテストファイルには、主に2つの書き方があります。

  1. 外部テスト(External Tests):

    • テストファイルが package <対象パッケージ名>_test と宣言されます。
    • 例: package mypackage_test
    • この場合、テストコードは対象パッケージの外部にあるとみなされ、対象パッケージの公開(exported)識別子にのみアクセスできます。非公開(unexported)識別子にはアクセスできません。
    • これは、パッケージの公開APIが正しく機能するかどうかを検証するのに適しています。
  2. 内部テスト(Internal Tests):

    • テストファイルが package <対象パッケージ名> と宣言されます。
    • 例: package mypackage
    • この場合、テストコードは対象パッケージの一部であるとみなされ、対象パッケージの公開(exported)識別子と非公開(unexported)識別子の両方にアクセスできます。
    • これは、パッケージ内部の非公開な実装詳細や、複雑な内部状態を直接テストするのに適しています。

このコミットは、既存の外部テストの一部を内部テストに移行することで、より詳細なテストを可能にすることを目的としています。

技術的詳細

このコミットの技術的な詳細は、Goのテストファイルのパッケージ宣言と、それに伴うインポートパスの変更に集約されます。

1. package <pkg>_test から package <pkg> への変更

コミットの差分を見ると、多くのテストファイルの先頭にある package 宣言が、package <元のパッケージ名>_test から package <元のパッケージ名> へと変更されています。

変更前(外部テスト)の例:

// src/pkg/container/heap/heap_test.go
package heap_test

import (
	. "container/heap" // ドットインポート
	"testing"
)

この場合、heap_test パッケージは container/heap パッケージの外部にあるため、container/heap パッケージの公開識別子にのみアクセスできます。container/heap パッケージの識別子を直接参照するために、import . "container/heap" というドットインポートが使用されています。

変更後(内部テスト)の例:

// src/pkg/container/heap/heap_test.go
package heap

import (
	"testing"
)

package heap と宣言することで、heap_test.gocontainer/heap パッケージの一部とみなされます。これにより、heap_test.go 内のコードは、container/heap パッケージ内の公開および非公開の識別子に直接アクセスできるようになります。

2. ドットインポートの削除と直接参照

テストファイルが対象パッケージと同じパッケージに属するようになったため、以前は外部テストとして対象パッケージの識別子にアクセスするために使用されていたドットインポート(例: . "container/heap")は不要になります。

差分を見ると、多くのファイルでこのドットインポートが削除されています。そして、以前は md5.New() のようにパッケージ名をプレフィックスとして使用していた呼び出しが、New() のように直接関数名を呼び出す形に変更されています。これは、テストファイルが対象パッケージと同じパッケージに属しているため、パッケージ名を省略して識別子を直接参照できるようになったためです。

変更前(外部テスト)の例:

// src/pkg/crypto/md5/md5_test.go
package md5_test

import (
	"crypto/md5" // md5パッケージをインポート
	// ...
)

func TestGolden(t *testing.T) {
	// ...
	c := md5.New() // パッケージ名をプレフィックスとして使用
	// ...
}

変更後(内部テスト)の例:

// src/pkg/crypto/md5/md5_test.go
package md5 // パッケージ名をmd5に変更

import (
	// "crypto/md5" は不要になる
	// ...
)

func TestGolden(t *testing.T) {
	// ...
	c := New() // 直接New()を呼び出す
	// ...
}

3. 依存関係の循環と例外

コミットメッセージにある「Because of dependency cycles some packages cannot be tested by internal tests.(依存関係の循環のため、一部のパッケージは内部テストでテストできません。)」という点も重要です。

Goのパッケージシステムでは、依存関係の循環は許容されません。もし package Apackage B に依存し、同時に package Bpackage A に依存する場合、コンパイルエラーとなります。

内部テストに移行することで、テストファイルが対象パッケージの一部となるため、テストコードが対象パッケージ内の他のファイルに依存する形になります。もし、対象パッケージが既に別のパッケージに依存しており、その別のパッケージが間接的にテストファイルに依存するような構造になっている場合、新たな依存関係の循環が発生する可能性があります。このようなケースでは、内部テストへの移行は不可能であり、引き続き外部テストとして維持する必要があります。

このコミットでは、多くのパッケージで内部テストへの移行が試みられていますが、すべてのパッケージに適用されているわけではないのは、このような依存関係の制約によるものと考えられます。

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

このコミットで変更されたファイルは以下の通りです。

  • src/pkg/container/heap/heap_test.go
  • src/pkg/crypto/md5/md5_test.go
  • src/pkg/crypto/sha1/sha1_test.go
  • src/pkg/go/build/deps_test.go
  • src/pkg/io/ioutil/ioutil_test.go
  • src/pkg/io/ioutil/tempfile_test.go
  • src/pkg/net/conn_test.go
  • src/pkg/net/http/filetransport_test.go
  • src/pkg/net/packetconn_test.go
  • src/pkg/net/protoconn_test.go
  • src/pkg/path/filepath/match_test.go
  • src/pkg/runtime/debug/garbage_test.go
  • src/pkg/runtime/debug/stack_test.go

これらのファイルすべてにおいて、共通して以下の変更が行われています。

  1. テストファイルの package 宣言が、package <元のパッケージ名>_test から package <元のパッケージ名> へと変更されています。
  2. 対象パッケージをドットインポートしていた行(例: . "container/heap""crypto/md5" など)が削除されています。
  3. 対象パッケージの識別子を呼び出す際に、パッケージ名のプレフィックス(例: md5.New())が削除され、直接識別子名(例: New())が呼び出されるように変更されています。

コアとなるコードの解説

ここでは、src/pkg/crypto/md5/md5_test.go の変更を例に、コアとなるコードの解説を行います。

変更前 (md5_test.go の一部):

--- a/src/pkg/crypto/md5/md5_test.go
+++ b/src/pkg/crypto/md5/md5_test.go
@@ -2,10 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package md5_test
+package md5
 
 import (
-\t"crypto/md5"
 	"fmt"
 	"io"
 	"testing"
@@ -54,7 +53,7 @@ var golden = []md5Test{\n func TestGolden(t *testing.T) {\n \tfor i := 0; i < len(golden); i++ {\n \t\tg := golden[i]\n-\t\tc := md5.New()\n+\t\tc := New()\n \t\tbuf := make([]byte, len(g.in)+4)\n \t\tfor j := 0; j < 3+4; j++ {\n \t\t\tif j < 2 {\
@@ -79,14 +78,14 @@ func TestGolden(t *testing.T) {\n }\n \n func ExampleNew() {\n-\th := md5.New()\n+\th := New()\n \tio.WriteString(h, \"The fog is getting thicker!\")\n \tio.WriteString(h, \"And Leon\'s getting laaarger!\")\n \tfmt.Printf(\"%x\", h.Sum(nil))\n \t// Output: e2c569be17396eca2a2e3c11578123ed\n }\n \n-var bench = md5.New()\n+var bench = New()\
 var buf = make([]byte, 8192+1)\
 var sum = make([]byte, bench.Size())\

変更点:

  1. package 宣言の変更:

    • - package md5_test+ package md5 に変更されています。
    • これにより、md5_test.go ファイルは crypto/md5 パッケージの一部として扱われるようになります。
  2. インポートの削除:

    • - "crypto/md5" の行が削除されています。
    • テストファイルが md5 パッケージ自体に属するため、自身のパッケージをインポートする必要がなくなります。
  3. 識別子の直接参照:

    • - c := md5.New()+ c := New() に変更されています。
    • - h := md5.New()+ h := New() に変更されています。
    • - var bench = md5.New()+ var bench = New() に変更されています。
    • 以前は md5_test パッケージから md5 パッケージの New 関数を呼び出すために md5.New() のようにパッケージ名をプレフィックスとして使用していましたが、テストファイルが md5 パッケージの一部となったため、New() のように直接関数名を呼び出すことができるようになりました。

この一連の変更により、md5_test.go 内のテストコードは、crypto/md5 パッケージ内の非公開な関数や変数にもアクセスできるようになり、より包括的なテストが可能になります。他のファイルでも同様のパターンで変更が適用されています。

関連リンク

参考にした情報源リンク

  • Go言語のパッケージと可視性に関する一般的な情報
  • Go言語のテストに関するドキュメント(特に外部テストと内部テストの違いについて)