テストの抽象化を増やしすぎてはいけない理由(翻訳)
はじめに
コードには「プロダクションコード」と「テストコード」があります。プロダクションコードとは、コードベースのうち本番環境で実行される部分を指し、同様にテストコードはテスト環境でのみ実行されるコードベースの部分を指します。プロダクションコードの予行演習はテストコードを介して行われます。
テストコードは「明快で」「読みやすく」「何が行われているのか理解しやすい」ものでなければなりません。shared exampleのようなテストの抽象化は、必ずしもテストコードで不可ではありません。
テストコードをDRYにしない理由
プロダクションコードのテストをむやみに抽象化すると、リファクタリングが難しくなります。その理由は、1個のテスト抽象化がテストコードの複数の場所で再利用されるからです。そのようなテストを変更すると、テストの他のどこかが壊れる可能性があります。しまいには、面倒な抽象化部分でテストのリファクタリングが発生したり、下手をすると抽象化が合わなくなって失敗するようになってしまったテストを更新して、抽象化を消し去るはめになったりします。
失敗を含むテストは読みにくいものです。抽象化を用いるテストが抽象化の内部で失敗すると、失敗した場所がレポートの冒頭に表示されますが、どのテストが実際に失敗しているかを突き止め、どのテストが抽象化を呼び出したかを確認するには、開発者がスタックトレースの隅々まで目を通す必要があります。
以下はRSpecのShared Exampleの出力です。
Finished in 3.17 seconds (files took 2.49 seconds to load)
47 examples, 5 failures
Failed examples:
rspec ./spec/requests/api/v1/users_spec.rb[1:2:2:2:1:1:1] # Api::V1::Users PATCH #update behaves like ...
rspec ./spec/requests/api/v1/users_spec.rb[1:2:4:1:1:1] # Api::V1::Users PATCH #reset_change_email behaves like ...
rspec ./spec/requests/api/v1/users_spec.rb[1:2:5:1:1:1:1] # Api::V1::Users PATCH #request_remove_user behaves like ...
rspec ./spec/requests/api/v1/users_spec.rb[1:2:1:1:1:1] # Api::V1::Users GET #show behaves like ...
rspec ./spec/requests/api/v1/users_spec.rb[1:2:3:1:1:1] # Api::V1::Users PATCH #reset_api_key behaves like ...
失敗の発生箇所をずばり当てたらビールおごりたい
どうか誤解なさらないよう
テストコードに多数の抽象化を導入するというのは、ちょうど読んでいる本の一字一句すべてにカラーマーカーで線を引くようなものです。マーカーで強調したかったのは最も重要な部分だけのはずですよね。
テストコードも同様です。私たちがやりたいのは、テストを隅々まで抽象化することではなく、基本的にできる限り明快かつ読みやすいベタなテストコードを書き、抽象化はテストケースのうち重要性が低い部分で行うことです。たとえば、テストコード用のオブジェクトを作成するFactoryパターンは(訳注: 適切な抽象化の)例として申し分ありません(FactoryBot)。カスタムマッチャーも同様に、テストコードを明快かつ宣言的に保ちつつテストに抽象化を導入するまっとうな手法です。
要するに、テストコードのうちで重要性の低い部分を抽象化するのは「あり」ですし、実際にとても有用です。逆に、システムの実際の振る舞いを記述するテストコードに抽象化を持ち込むと決してよい結果を生みません1。
関連記事
- 私が先月shared exampleを1本書いたときに、コードレビューで指摘を受けたおかげで命拾いしました。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。