ことのはじまり
Vitest + testing-libraryでReactのテストを書いているので、eslint pluginとして @vitest/eslint-plugin を利用しています。
v1.5.0に更新されたなかで、eslint-plugin-jestと同等のいくつかのruleがrecommendedで有効になりました。
その中でも比較的影響度のあるvitest/no-conditional-tests が有効になったことにより、いくつかの良くないテストを修正する必要がありました。
ルールが有効になっていなくても違反するコードがなければよいのですが、すでに数千個のテストが含まれるリポジトリなので、いくつかのテストが違反していた状況です。
このルールは端的に言えば「テストコードの中で分岐かいたらだめだよ」というものです。
違反していたいくつかのコードのうち、大半は it.each で条件網羅テーブルを作り、そのなかで期待値が反転する場合は利用するテストマッチャーや値を反転させるという分岐を持っていました。
わかりやすいサンプルとしては以下のような感じです。
if (conditionFlag) {
expect(value).toBeDefined()
} else {
expect(value).toBeUndefined()
}※ recommendedにいくつかのruleを有効にしたPRはこちら。
なぜテストに分岐を書いてはいけないのか
もはや感覚的に「ダメだろ」とは認知しているものの、改めてちゃんと理由を説明できるかというと(理由を説明する立場であるので)伝え漏れがでてしまうかもしれないなぁと感じました。
なので理由をブログにしておいて説明するときはURLを渡せばいいや、と思ったので書いておこうと思います。
テスト品質としての理由
-
まず最初に思い浮かぶのは「テストができていなくても気づかないから」、ですね。
分岐のなかにassertが書かれていては分岐を通らないケースではテストになっていません。 -
次に思い浮かぶのは「偽陽性や偽陰性の可能性」です。
テスト内容に条件がついていることで、テスト結果が意図するものであるかを示すものがありません。テストのテストが必要になります。 -
最後に「無価値なテストをしている可能性」が考えられます。
いわゆるテストが通るだけのテストです。expect(value).toBe(value)のような無価値さを条件とit.eachがなんとなくそれっぽく見せてしまうことがまぁまぁあります。よく考えたら何のテストにもなってないじゃねーか、というやつです。
よりよい視点
テスト品質の向上も大事ですが、僕がいちばん大事にしているのは「テストは仕様書であってほしい」という点です。
その関数ないしメソッドないしコルーチン、なんでもいいのですが、その検査対象がどのような入出力を期待されているのかを表現するものがテストであってほしいのです。
この辺はTDDやテストファーストの本分なので詳細はそちらを理解してほしいとは思いますが、要するにテストを読んで振る舞いを理解できるテストが良いテストだと考えているということです。
分岐がないテストは、なぜよいテストになりえるのか
分岐があるテストは読みにくい。
テストの価値がテストそのものよりも仕様書としてはたらくことに重点をおいて考えると、これは非常に大事なことだと思います。
よいテストは読みやすく、テスト同士で補完しあい、作者の意図や対象の責務を伝えようとしてくれる。分岐のあるテストはどちらかというとカバレッジを取りたいだけにみえる。
「学問が好きな人」と「テストで良い点を取りたい人」のような違いがあると思うのです。この違いから生まれる将来は言わずもがなでしょう。
よみやすさとはなにか
つまり読みやすいテストとは何かを考えれば、よいテストに近づいていけそうです。
eslint-pluginのアップグレードのために分岐したテストを分解する作業をしていると、ふと「わかりやすいテストって逆からも読めるな?」と思ったのです。
アップグレードのための作業は分岐条件ごとに頭の中でわければいいだけなので、実装の内容は見なくても直せます。
直す箇所はテストの最後のほうなので、直し終えたときに無意識で下から上に読むようになるのですが、するとなぜか実装内容が見えてくる。
これはどういうことなんだろう、と思っていました。
ヒントっぽいもの
下から上に読めるテストはあきらかにわかりやすいなぁと思って2日ほどたったころ、ちょうど似たような話が目に止まりました。
腹落ちしたところ
内容は「一本道の長たらしいコードではなく、2次元で認知可能なコードの広がりの話(超訳)」なわけですが、これがストンと腹落ちしました。
テストは基本的に単一で短いコードで、それらの集合が全体のプロダクトコードの特性を表現します。つまりモジュールや関数分割でやっていることに似ている。
その過程でテストひとつひとつの結論を頭に溜めこんで全体を認知するわけですが、このときに結論同士を行き来することがあります。
ちょうど、仕様書を読むときのように。
この「結論間の行き来の時間」で上から読むのがしんどい。結論(assert)からはじめて、条件(arrange)を確認したい。
AAAパターンはこの可逆的な読み方ができることに仕様書としての価値があるのだと理解しました。
まとめ
下から読んでわかるテストを書こう。
