開発者は消しやすいテストコードを書こう

コード、ちゃんと消していますか?

普段の開発では「コードを書くこと」にスポットライトが当たりがちである。新機能を作る、バグを直す──どちらもコードを書いて価値を生み出す行為だ。 だからといって、「コードを消すこと」が不要になるわけではない。

コードを消すことは、コードを書くことよりも難しい問題もある。

ここで門戸を閉ざしてしまうのは些かもったいない。

ソフトウェアを構成するソースコードを消すことに関しては、著者の ズッ友であるGenki Sano氏の資料を参照されたい。

ソースコードを消すことは難しいが、テストコードを消すことはどうなのだろうか。

そんな疑問に対して、ど真ん中で向き合うために、今回は「消しやすいテストコード」について考えてみたい。

テストコードを消す必要性について

まずはここに切り込まねばならないだろう。書くことすら躊躇われることが稀によくあるテストコードをなぜ消す必要があるのか。

大前提として、コードは少なければ少ない方が良い。読む量が減れば、理解と変更にかかるコストも下がるからだ。 そういう意味でテストコードもソースコードも消したほうが良いだろう。

そして、テストコードはソースコードと比べて圧倒的に消しやすい。本番環境で動くプロダクトに影響がないため、気楽に消すことができる。 テストコードを減らすことで、残ったテストコードを物語る重要な仕様についての理解を深めることができる。

例えば、長く運用してきたサービスでは、

  • もう誰も触っていない画面のテスト
  • 仕様変更前の挙動を前提にしたままのテスト

が、そのまま生き残っていることが稀によくある。 リファクタリングやアーキテクチャ変更をしようとしたとき、こうしたテストが「本当に必要なのか」「誰に聞けばいいのか」が分からず、手を止めてしまうことが往々にしてある。

逆に、安心して消せるテストコード になっていれば、「これはもう役目を終えたから消そう」と判断しやすくなり、リファクタリングのスピードも上がるだろう。

消しやすいテストコードについて

消しやすい とはなにか。

筆者は 極めて独立しているテストコード と定義したい。

この定義には重要なポイントが2つある。

1つは、誰でも消せること。 2つ目は、他への影響が皆無であること。

1. 誰でも消せること

今日新卒として入ってきたClaudeCodeくんでも、若者でも幼稚園児でも構わない。 熟練の炭鉱夫だろうが、ドメインエキスパートだろうが、人に依存していないことが重要である。

消せる人間が多いということは、テストコードが消されるまでのスピードが早い(可能性がより高まる)ことを意味する。

要するに、「あのテストを書いた◯◯さんに聞かないと消していいか分からない」という状態を避けたい。 特定の人の頭の中にだけ前提があるテストコードは、その人がいなくなった瞬間から、誰も触れない聖域になってしまう。

2. 他への影響が皆無であること

削除対象のテストコードを消したことによって、他のテストコードやCI・CDに影響が一切無いことを指す。 テストコードがテストコードに影響することはザラにあって、

  • パラメータライズドテストのデータソースが共通化されており、2つ以上のテストコードが利用している
  • モックオブジェクトが共有されており、1つのテストコードを消すと他のテストコードが失敗する
  • テストコードの実行順序をずらすと、他のテストコードが失敗する

改めてまとめると、テストコードは依存していないことが最も重要である。ということが言える。

もちろん、「共有されているテスト用ユーティリティ」そのものが悪いわけではない。 問題なのは、テスト対象の仕様を暗黙に埋め込んだまま、複数のテストで共有してしまうことだ。

依存関係はテストの単位が大きいE2Eでよくありがちである。E2Eのテストの捨てやすさに関しては、著者のズッ友であるやまずん氏の記事を参照されたい。

テストも捨てやすく作ろう by やまずん

テストコードとアーキテクチャと開発手法について

消しやすいテストコードの真髄がわかったところで、 アーキテクチャに合わせて、テストコードも変えていこう という話を展開しよう。

アーキテクチャが変われば、責務の境界線も変わる。 つまり、テストコードもアーキテクチャに合わせて変化すべきだ。

この視点に立つと、「消しやすいテストコード」を育てていく方針が明確になる。

筆者は基本的に、コードのアーキテクチャはMVCから始めて、必要に応じてタイミングを見計らい、コードのアーキテクチャを組む。

そのタイミングで、APIの中規模テストを各コンポーネントの小規模テストに書き換えることにしている。

初期の段階では、API+DBの疎通をテストする中規模テストが主役になる。 しかし、後からアプリケーションの責務が整理されてくると、

  • ルール(バリデーション)
  • 計算ロジック
  • ドメイン知識

といった要素が独立し、小さいテストに切り出せるようになる

このタイミングで、中規模テストから積極的にテストを“消し”、より小さいテストへ置き換えることができる。

テストを小さくしていくことで、

  • テスト実行コストを下げる
  • 責任の所在が明らかになる

アーキテクチャの恩恵にテストコードも乗っかることができる。

例えば

  • APIのI/OとDBへの疎通を見るテストは、引き続きAPIの中規模テストコードとして残す。
  • 細かい入力値のバリデーションのテストは、責務の分割に伴い小規模なテストコードとして切り出す。

前者は「システムとしてつながっているか」を保証するためのテストであり、後者は「ロジックとして正しいか」を保証するためのテストだ。

この2つをきちんと分けておくと、「ロジック側の変更のたびにAPIテストを直す」といった不毛さから解放される。

こうすることで、テストコードの責任範囲が明確になり、消しやすくなる。

まとめ

テストコードは、「書くこと」以上に「消せること」が大事になる場面がある。

  • 役目を終えたテストを、誰でも・安全に消せること
  • そのために、テスト同士やテスト用ユーティリティとの依存を極力なくすこと
  • アーキテクチャの変化に合わせて、中規模のテストを小さなテストへと移譲していくこと

こうした工夫によって、テストコードは「ひたすら積み上がる資産」から 役目を終えたらちゃんと退場していく、健全なサイクルを持った仕組みに変わる。

テストは、永遠に残すためだけに書くのではない。 いつか安心して消せるようにしておくことも、開発者の大事な設計責任のひとつだ。