開発者は消しやすいテストコードを書こう
コード、ちゃんと消していますか?
普段の開発では「コードを書くこと」にスポットライトが当たりがちである。新機能を作る、バグを直す──どちらもコードを書いて価値を生み出す行為だ。 だからといって、「コードを消すこと」が不要になるわけではない。
コードを消すことは、コードを書くことよりも難しい問題もある。
ここで門戸を閉ざしてしまうのは些かもったいない。
ソフトウェアを構成するソースコードを消すことに関しては、著者の ズッ友であるGenki Sano氏の資料を参照されたい。
ソースコードを消すことは難しいが、テストコードを消すことはどうなのだろうか。
そんな疑問に対して、ど真ん中で向き合うために、今回は「消しやすいテストコード」について考えてみたい。
テストコードを消す必要性について
まずはここに切り込まねばならないだろう。書くことすら躊躇われることが稀によくあるテストコードをなぜ消す必要があるのか。
大前提として、コードは少なければ少ない方が良い。読む量が減れば、理解と変更にかかるコストも下がるからだ。 そういう意味でテストコードもソースコードも消したほうが良いだろう。
そして、テストコードはソースコードと比べて圧倒的に消しやすい。本番環境で動くプロダクトに影響がないため、気楽に消すことができる。 テストコードを減らすことで、残ったテストコードを物語る重要な仕様についての理解を深めることができる。
例えば、長く運用してきたサービスでは、
- もう誰も触っていない画面のテスト
- 仕様変更前の挙動を前提にしたままのテスト
が、そのまま生き残っていることが稀によくある。 リファクタリングやアーキテクチャ変更をしようとしたとき、こうしたテストが「本当に必要なのか」「誰に聞けばいいのか」が分からず、手を止めてしまうことが往々にしてある。
逆に、安心して消せるテストコード になっていれば、「これはもう役目を終えたから消そう」と判断しやすくなり、リファクタリングのスピードも上がるだろう。
消しやすいテストコードについて
消しやすい とはなにか。
筆者は 極めて独立しているテストコード と定義したい。
この定義には重要なポイントが2つある。
1つは、誰でも消せること。 2つ目は、他への影響が皆無であること。
1. 誰でも消せること
今日新卒として入ってきたClaudeCodeくんでも、若者でも幼稚園児でも構わない。 熟練の炭鉱夫だろうが、ドメインエキスパートだろうが、人に依存していないことが重要である。
消せる人間が多いということは、テストコードが消されるまでのスピードが早い(可能性がより高まる)ことを意味する。
要するに、「あのテストを書いた◯◯さんに聞かないと消していいか分からない」という状態を避けたい。 特定の人の頭の中にだけ前提があるテストコードは、その人がいなくなった瞬間から、誰も触れない聖域になってしまう。
2. 他への影響が皆無であること
削除対象のテストコードを消したことによって、他のテストコードやCI・CDに影響が一切無いことを指す。 テストコードがテストコードに影響することはザラにあって、
- パラメータライズドテストのデータソースが共通化されており、2つ以上のテストコードが利用している
- モックオブジェクトが共有されており、1つのテストコードを消すと他のテストコードが失敗する
- テストコードの実行順序をずらすと、他のテストコードが失敗する
改めてまとめると、テストコードは依存していないことが最も重要である。ということが言える。
もちろん、「共有されているテスト用ユーティリティ」そのものが悪いわけではない。 問題なのは、テスト対象の仕様を暗黙に埋め込んだまま、複数のテストで共有してしまうことだ。
依存関係はテストの単位が大きいE2Eでよくありがちである。E2Eのテストの捨てやすさに関しては、著者のズッ友であるやまずん氏の記事を参照されたい。
テストコードとアーキテクチャと開発手法について
消しやすいテストコードの真髄がわかったところで、 アーキテクチャに合わせて、テストコードも変えていこう という話を展開しよう。
アーキテクチャが変われば、責務の境界線も変わる。 つまり、テストコードもアーキテクチャに合わせて変化すべきだ。
この視点に立つと、「消しやすいテストコード」を育てていく方針が明確になる。
筆者は基本的に、コードのアーキテクチャはMVCから始めて、必要に応じてタイミングを見計らい、コードのアーキテクチャを組む。
そのタイミングで、APIの中規模テストを各コンポーネントの小規模テストに書き換えることにしている。
初期の段階では、API+DBの疎通をテストする中規模テストが主役になる。 しかし、後からアプリケーションの責務が整理されてくると、
- ルール(バリデーション)
- 計算ロジック
- ドメイン知識
といった要素が独立し、小さいテストに切り出せるようになる。
このタイミングで、中規模テストから積極的にテストを“消し”、より小さいテストへ置き換えることができる。
テストを小さくしていくことで、
- テスト実行コストを下げる
- 責任の所在が明らかになる
アーキテクチャの恩恵にテストコードも乗っかることができる。
例えば
- APIのI/OとDBへの疎通を見るテストは、引き続きAPIの中規模テストコードとして残す。
- 細かい入力値のバリデーションのテストは、責務の分割に伴い小規模なテストコードとして切り出す。
前者は「システムとしてつながっているか」を保証するためのテストであり、後者は「ロジックとして正しいか」を保証するためのテストだ。
この2つをきちんと分けておくと、「ロジック側の変更のたびにAPIテストを直す」といった不毛さから解放される。
こうすることで、テストコードの責任範囲が明確になり、消しやすくなる。
まとめ
テストコードは、「書くこと」以上に「消せること」が大事になる場面がある。
- 役目を終えたテストを、誰でも・安全に消せること
- そのために、テスト同士やテスト用ユーティリティとの依存を極力なくすこと
- アーキテクチャの変化に合わせて、中規模のテストを小さなテストへと移譲していくこと
こうした工夫によって、テストコードは「ひたすら積み上がる資産」から 役目を終えたらちゃんと退場していく、健全なサイクルを持った仕組みに変わる。
テストは、永遠に残すためだけに書くのではない。 いつか安心して消せるようにしておくことも、開発者の大事な設計責任のひとつだ。