テストは通常どのようなものですか?
非常に概略的に、各ユニットテストは通常、次の手順で構成されます。
- 入力データの初期化。
- ビジネスロジックの実行と結果の取得。
- 結果と標準との比較。
多くの場合、入力と出力はコード自体の中にあります。 コードの変更により出力に予想される変更が導入された場合、参照結果を手動で編集する必要があります。 場合によっては、テストのデータが膨大な場合、それらは別々のファイルに取り出されますが、参照データと比較ロジックのサポートは開発者の肩に残ります。
しかし、これはすべて統一することができます!
単体テストの本体では、一般に結果と標準との比較がないことを想像してください。 テスト自体が参照データを作成できると想像してください。 すべての入力データと出力データが構造化された形式であり、テストコードがよりコンパクトで統一され、読みやすくなることを想像してください。 提示?
アジェンダのテスト-テスト
私はこのアプローチをアジェンダテストと呼んでいます。なぜなら、私は略語が大好きだからです。実際、アジェンダは固有のデータです。 その本質は何ですか?
- テストの入力と出力はファイルに保存されます(JSONまたはその他-重要ではありません)。
- テストは2つのモードで機能します。
- 初期化モード:テストは出力データを計算し、このデータを標準ファイルに保存します。
- テストモード:テストは出力データを計算し、以前に保存された参照データを読み取り、それらを比較します。 データが異なる-テストは失敗します。
- 初期化モード:テストは出力データを計算し、このデータを標準ファイルに保存します。
- データの読み取り、書き込み、比較などのすべての補助コードは、補助ライブラリ/関数/クラスに転送され、個々のテストではその本質のみが残ります。
それだけですか?..そしてそれだけです! Goの例でこれがどのように機能するかを見てみましょう。Goについては、 小さなライブラリを公開しており、他の言語に簡単に移植できます。
まず、「ビジネスロジック」ファイルを作成します。テストするコードは次のとおりです。
ファイルexample.go
package example import "errors" type Movie struct { TotalTime int `json:"total_time"` CurrentTime int `json:"current_time"` IsPlaying bool `json:"is_playing"` } func (m *Movie) Rewind() { m.CurrentTime = 0 } func (m *Movie) Play() error { if m.IsPlaying { return errors.New("Movie is already playing") } m.IsPlaying = true return nil }
次にテストを作成します。
ファイルexample_test.go
package example import ( "encoding/json" "testing" "github.com/iafan/agenda" ) func TestMovie(t *testing.T) { agenda.Run(t, ".", func(path string, data []byte) ([]byte, error) { type MovieTestResult struct { M *Movie `json:"movie"` Err interface{} `json:"play_error"` } in := make([]*Movie, 0) // data , // if err := json.Unmarshal(data, &in); err != nil { return nil, err } out := make([]*MovieTestResult, len(in)) for i, m := range in { // , "-" // Rewind() m.Rewind() // Play() nil err := m.Play() // "" // 1) Movie // 2) out[i] = &MovieTestResult{m, agenda.SerializableError(err)} } // // return json.MarshalIndent(out, "", "\t") }) }
この行のアジェンダテストのすべての魔法:
agenda.Run(t, ".", func(...){...}}
これは、現在のディレクトリ内のすべてのテストファイル(デフォルトでは、これらは.json拡張子を持つファイル)を取得し、それぞれに対してパラメーターとして渡された関数を実行します。
次に、テストデータを含むファイルを作成します。
ファイルtest_data.json
[ {"total_time":100,"current_time":0,"is_playing":false}, {"total_time":150,"current_time":35,"is_playing":true}, {"total_time":95,"current_time":4,"is_playing":true}, {"total_time":125,"current_time":110,"is_playing":false} ]
初期化モードでテストを実行できます。
$ go test -args init
同時に、参照データを含むファイルが入力ファイルの隣に作成されます。
ファイルtest_data.json.result
[ { "movie": { "total_time": 100, "current_time": 0, "is_playing": true }, "play_error": null }, { "movie": { "total_time": 150, "current_time": 0, "is_playing": true }, "play_error": "Movie is already playing" }, { "movie": { "total_time": 95, "current_time": 0, "is_playing": true }, "play_error": "Movie is already playing" }, { "movie": { "total_time": 125, "current_time": 0, "is_playing": true }, "play_error": null } ]
このファイルを分析し、出力が期待どおりであることを確認する必要があります。 すべてが正常な場合、そのような生成されたファイルは、テストデータとともにリポジトリにコミットされます。
これで、通常モードでテストを実行できます。
$ go test
もちろん、テストはエラーなしで合格するはずです。
ここで、プロジェクトの存続期間中にコードに変更を加えた場合、そのようなテストで作業するために2つのシナリオを使用します。
- コードの変更がデータの変更につながらないことが予想される場合:
go test
を実行し、テストが壊れていないことを確認します。 - コードの変更がデータの変更につながることが予想される場合:
go test -args init
実行してから、たとえばgit diff
使用して、すべてのデータの変更が予想されることを確認します。
コードとテストデータの分離には、長所と短所の両方があります。
欠点は、コミットに存在するファイルの数が多いことです。 制限されたサイズの単純なデータを使用した単純な単体テストには 、 表形式のテストがより適しています。
はるかに多くの利点があります:テスト(コードとデータの両方)の読みやすさ、特にテストされたデータの複雑な構造の場合、結果をチェックするときに何かを見落とす可能性が低く、コードを再コンパイルする必要なくテスターがテストデータを補充および検証する可能性があります。