TL;DR
テストコードであっても腐るものは腐る
システムの表層にあたる画面の要素にとらわれず、業務のドメインを掘り下げて理解を深めるのが大事。
大体の話は
Twitterに書いておいたので先に読んでもらえると分かりやすいと思います。
End-to-endのテストを書く時、CypressでもGaugeでもどんなFWでも「"2016"年"4"月のデータが表示されている」みたいな特定の年月に対してテスト書きたい時は注意が必要だよみたいな話がしたい。
— わみ (@i_whammy_) 2022年9月15日
そもそもその年月は固定値なのか?
— わみ (@i_whammy_) 2022年9月15日
それとも現在の日付から最も近い年の4月なのか?
はたまた現在の日付から見た最新年度の年度初めなのか?
同じ年月のよっても、仕様によって表現したい内容が変わってくる。
何でここまで気にしないといけないかと言えば、年月を経て失敗するテストケースになる可能性があるから。そして年月を経た時に、その事情をチームの誰も知らないという恐ろしい事態に陥る可能性があるから。
— わみ (@i_whammy_) 2022年9月15日
そうならないように、表現したい仕様に沿った適切なアサーションにしておく必要があると思う。
— わみ (@i_whammy_) 2022年9月15日
例えばその日付から見て最も年の近い4月、まで分かってるならアサーションにもそのように書いておくべき。
仕様をもう一段階掘り下げて、隠れてる仕様を見つけ出して分かるように表現する必要がある。
対象読者
CypressとかSeleniumみたいなエンドツーエンドで画面の自動テストが実行できるフレームワークを使って自動テストを書いてる人
- と思ったけど画面に限った話ではない気もするのでそれ以外の人も読んでもらえると何か学びがあるかもしれない
画面の自動テストを書く時に起こること
例えば画面内に、特定の日付が表れることをテストしたくなったとします。
その時、あなたはどんな表現でテストを書きますか?
どんなフレームワークでも、実装レベルでは「特定のCSSセレクタに該当するHTML要素の値が"2022年9月1日"になっていることをアサーションする」みたいな実装になると思います。
じゃあこれで終わりでいいのか、って言ったらそんなことはなくて、テストの保守性を高めるにはもう少しやれることがあるんじゃないかなーと思って筆を取りました。
以下サンプルコードはCypressとjavascriptを使って書きますが、言語やFWは本論じゃないので雰囲気で読み流して下さい。
愚直に実装する
「id="date"がついたdivタグの値が"2022/9/1"である」というテストを書きたいなーと思うと、Cypressだと以下のようなコードになります。
describe('テスト', () => { it('日付が2022年9月1日である', () => { cy.visit('/') // 中略 cy.get('#date') .should('contain', '2022/9/1') }) })
今回話の対象にしたいのは、以上の例で言う「テスト」及び「日付が2022年9月1日である」の部分です。
これって何の日付なんだっけ?
「日付が2022年9月1日である」というアサーションの文言は、テストのアサーション表現としては必要最低限でしかありません。
まず「何の日付なのか?」という情報が欠けています。例えば受注データが記録された日なのか、荷物の発送予定日なのか、それともただの固定された文字列なのか*1、アプリケーションによってコンテキストは異なるはずです。
この「何の日付なんだっけ?」という問いへの答えをテストにも残しておくことで、後からテストを読んだ開発者がアプリケーションの仕様を理解しやすくなるのではないでしょうか。
以下の例は「受注日付が2022年9月1日であること」をわかりやすくしたコードのサンプルです。(あえてセレクタはそのままにしておきます)
describe('受注管理画面のテスト', () => { it('受注日付が2022年9月1日である', () => { cy.visit('/') // 中略 cy.get('#date').should('contain', '2022/9/1') }) })
この日付っていつ変わるんだっけ?
次に、この日付がいつ変更される可能性があるのかという点も気にする必要があります。固定値であれば気にしなくても良いのですが、例えば月をまたいだり、年をまたいだりしたタイミングでこのセレクタで取得される日付が変わるとしたら?
もしこの自動テストがデプロイメントパイプライン上で定期的に実行されていたら、「コードを何も変更していないのにテストが失敗した!」という事態に陥ってしまうのではないでしょうか。*2
いつ日付が変わるのかに着目することで、より安定した、かつ仕様に忠実な表現に直すことができるのではないでしょうか。
以下の例は「id='date'に表示される日付が、アプリケーションを開いた月の初めの日であること」に読み替えたコードのサンプルです。
describe('受注管理画面のテスト', () => { it('表示される受注日付はその月の初めの日である', () => { cy.visit('/') // 中略 const today = new Date() const firstDateOfMonth = [today.getFullYear(), today.getMonth()+1, 1].join('/') cy.get('#date').should('contain', firstDateOfMonth) }) })
何故こんな話がしたかったか
一見シンプルに見えるここまでの議論ですが、何故わざわざ取り上げようと思ったか。それはこのプロセスの中に、「システムの表層である画面」から「システムで取り扱うドメイン」への追究が発生していることに気がついたからです。
CypressやSeleniumのような自動テストフレームワークでテストを記述する時、開発者が一番注目するのは画面です。ともすれば、画面で起こっている表面的なことにばかり目が向いて、上で挙げたような愚直な実装に陥ってしまうこともあると思います。愚直な実装はその場では問題が無かったとしても、ドメイン知識を持つ開発者がチームを離れた時に仕様に対する知識が失われたり、突発的に失敗するテストに変貌するリスクを抱えています。
自動テストを書く時に、目線を画面からより深いドメインの部分に向ける、そしてそこで得た知識をコード上に反映させることで、テストコードの保守性をより高められるのではないでしょうか。
......というところで、先人(自分含む)が数年前に残したテストが突如失敗する様を目にした結果、勢いで書き始めた記事を締めようと思います。