テストを活用して、まあまあ良いコードを書く
テストをきっかけにコードを改善することで、ビジネス上の機会損失を減らすことを説明したいと思います。長いのでタイトルだけでも読んで頂けたら嬉しいです🙏
この記事を読むと以下の問題に答えられるようになります。
Q.次のうち、最も「まあまあ品質が良いコード」に近いのはどれか。
- 自動テストが書いてあるコード
- 十分にテストされたコード
- テストがしやすいコード
- 実装者以外が変更できないくらい最適化されたコード
答え
3
コードの品質を上げたら、ビジネス上の価値も上がって欲しい
「コードを良くすれば、利益を上げやすくなり、給料・評価として還元される」状態を目指したいです。この状態を達成できるコードを「高品質のコード」と仮定します。以下のような機会損失を減らすことでコストを下げ、ビジネスとしての価値を上げる方法を考えます。
- なかなかリリースができない
- 新機能追加までが遅い
- 修正に時間がかかる
完成してもコードは書き続ける
「一度書いたら二度と修正しないで済むようにする」のは、エンジニアにとっては理想だと言えます。しかしこれは現実的でないです。「レガシーコードからの脱却」には、以下のような一文があります。
“ソフトウェアは変更の必要はないという思い込みを開発者は信じて、長いあいだ開発をしてきた。ソフトウェア開発は書いたらそれっきりの仕事だと思っていたのだ。だが、真実は異なる。使われているソフトウェアには必ず変更が必要となるのだ。それ自体は良いことでもある。ユーザーがソフトウェアから価値を引き出す新たな方法を見つけたことを意味するからだ。開発者は、ソフトウェアを変更しやすくすることで、ユーザーに応えたいと思うだろう。”
この一文からわかるのは、「ユーザーのニーズは常に増え、応え続けるために変更は必要不可欠」ということです。仮に無視した場合、そのユーザーは他のサービスやプロダクトに乗り換えます。ニーズを満たす時間が想定よりかかった分だけ、機会損失が発生します。
市場は変化し続けるが、コードは不変
市場は常に変化します。しかしコードは一度書いたら勝手に変更されることはありません。コードは不変なところがメリットですが、デメリットでもあるのです。特にコードは意識的に変更していく必要があります。
市場にコードを依存させる
機会損失を限りなくゼロに近づけるには、なるべくコードが市場(ユーザーのニーズ)にとり残されないように変更し続けなくてはなりません。常に変更し続けるには、
- コードの変更が不要になるように工夫する
- コードの変更が簡単になるように工夫する
これらを考える必要があります。今回は話をシンプルにするために、「コードの変更が不要になるように工夫する」に関しては述べません。
コードの変更が簡単になるように工夫する
市場に合わせてコードを簡単に変更できるようになると、提供までの時間が短くなり、機会損失が減ります。「コードの変更が簡単になるような工夫」ができれば、開発・ビジネスともに改善したと言えます。
読みやすく使いやすいコードを書くと、変更がしやすくなる
「読みやすく使いやすいコードは、変更を簡単にし機会損失を減らすため、ビジネスの上の価値が高くなるので、まあまあ高品質なコードと言える」と仮説しました。「読みやすい・使いやすい」以外も品質に影響しますが、今回はそれらについて述べません。そのため「まあまあ品質が良くなる」としました。
テストで不安なふるまいを検知し、実装で改善する
「読みやすく使いやすいコード」がなぜ変更を簡単にし、そのようなコードにどうやって近づけるのでしょうか?私はテストを活用すると、近づけるのではと考えました。まずは「読みやすい」「使いやすい」がなぜ重要なのか、次になぜテストが「読みやすく使いやすいコード」に繋がるのかを説明します。
コードの品質、速い・読みやすい・使いやすい
コードの品質には、「読みやすい」「使いやすい」以外にも「速い」もあります。もちろん他にも観点はあると思いますが、ほぼこの3つに集約すると思います。
読むコストは、書く10倍
「レガシーコードからの脱却」によれば、「平均してコードは書かれる回数の10倍読まれている」とあり、「コードは読むためのもの」と言えると思います。立ち上げ初期は書く回数の方が多いかもしれませんが、少なくともリリース後からは読む回数の方が多いという実感があります。つまり読みやすさを改善することは、コードに関わる業務時間を減らし、機能提供や改善までのスピードを間接的に早くします。機会損失の低減につながります。
「使いやすいコード」は、ソフトウェアの劣化を防ぐ
「使いやすいコード」は「読みやすいコード」と似ていますが、ここでは特に「変更が簡単で保守がしやすい」性質を持ったコードのことを指します。ソフトウェアは通常、規模が大きくなるにつれ複雑性が増し、修正や変更が難しくなります。
成長とともに修正・変更に費やす時間は増える(単体テストの考え方/使い方)
開発スピードを下げずに、変更を絶えず加えていくためには、使いやすさに目を向けることが大切です。使いにくいコードは改善活動を阻害し、機能を継ぎ足すようになり、次第に変更や拡張ができなくなります。そして次第に「読みづらいコード」が作られていきます。
「速いコード」は、マシンに依存する
実行自体が高速なコードも品質が高いです。動作が早ければユーザーの離脱を防いだり、実行負荷が減り、金銭的にメリットがあります。しかし、ソフトウェアにおいてはマシンの性能も実行速度に影響します。そしてハードウェアの発展はある意味ソフトウェアより速いです。「UNIXという考え方」には以下のように書かれています。
現在のハードウェア上で性能を向上させようと、何日も、あるいは何週間もかけてアプリケーションの動作を調整した経験は、おそらく多くのソフトウェア開発者に共通のものだろう。ところが、次世代のハードウェアは10倍の性能向上を「ただ」で実現してしまうかもしれないのだ。
今から5年前の2020年はm1 mac が登場したばかりでした。その頃はまだ業務でもintel mac を使っていた記憶があります。そこから振り返ると、当時重かったコードも今は「ただ」できびきびと、パフォーマンス良く動作する予感がします。「実行が速い」ということはもちろん大事ですが、外部要因で解決できる可能性も高いので、「読みやすさ」「使いやすさ」の方が大切になるのではと考えました。
テストしやすいコードは、読みやすい・使いやすい
ここまで「読みやすい」「使いやすい」コードがどのように業務にメリットをもたらすかを述べました。次に、このような特性を持ったコードを生み出すためにテストが関連深いことを説明します。
テストがしやすいコードは凝集度が高く、結合度が低い
「テスト駆動開発」のまえがきでは、「凝集度が高く結合度が低いたくさんの部品で構成された設計はテストが書きやすい」と述べています。
凝集度
凝集度とは、「レガシーコードからの脱却」では「単一の責任を持ち、1つのものだけを扱うこと」と説明しています。具体的には「1つのクラスやメソッドまたは関数で色んなことをやろうとせず、1つの目的(ふるまい)だけを叶えようとしている度合い」と表現できそうです。大雑把に言えば、凝集度を高めると実装がシンプルになります。結果、読みさすさにつながります。中でも論理的凝集を解消して凝集度を高めると、条件分岐が減り、テストが書きやすくなります。凝集度についてのさらに詳しい情報は、「オブジェクト指向のその前に 凝集度と結合度」というスライドがわかりやすいです。
結合度
結合度とは、「初めての自動テスト」では「2つのオブジェクトのつながりの強さを表す度合い」と説明しています。結合度が強い状態(密結合)だと、一方に変更を加えたくても、もう一方に影響するので変更を加えるのが困難になります。「単体テストの考え方/使い方」で「コードを分離して個別にテストすることが難しくなる」と説明している通り、密結合な実装は独立したテストをすることが困難です。つまり、結合度が低いとテストがしやすくなります。また疎結合なコードは、変更が加えやすいので、使いやすいとも言えそうです。
たくさんの部品で構成された設計
「たくさんの部品で構成された設計」は、「コードを小さく保つ」という意味ではテストが書きやすくなります。なぜなら、適度に小さいコードはテストする際、前準備が少なくなる傾向になるからです。
高凝集・疎結合・適切に分割されたコードは読みやすい
以上により、テストしやすいコードとは、凝集度が高く疎結合であり、適切に分割されたコードだと言えます。そしてこれらの特性は、「読みやすく、使いやすいコード」にも当てはまります。つまり、テストしやすいコードは読みやすく使いやすいと言えます。
「テストコード」だけがテストではない
ところで、「テスト」とはなんでしょうか?何気なく「テストを書く」と普段、口にしますが、本来テストは「する」ものだと思います。「テストを書く」は、ITエンジニアの方言です。「テストを書く」とは多くの場合、自動テストのことを指していると考えています。意識から消えがちですが、テストは「自動テスト」だけでは無いです。「テストをする」の文脈で考えれば、他の様々な要素もテストであることに気づくと思います。「テスト駆動開発」には以下のように書かれています。
テストは「評価する」という意味の動詞だ。何か変更したら、どんな些細な変更であろうが、なにかしらのテストをしないエンジニアはいないだろう
動作確認もテストだと言えますし、マークアップ後、都度localhostで確認するのもテストです。私が強調したいのは、「テストしやすい」とは、必ずしも「テストコードが書きやすい」ことを意味しないということです。テストしやすいコードを書くことは、テストコードを書くことと別です。もちろん、自動テストを書くこととも別です。
テストは質を上げない
t-wadaさんのスライドより拝借
ryuzeeさんのブログより拝借
1つ注意点があります。それはテストをいくらしてもコードの品質は上がらないということです。大事なのはテストをきっかけに、コードの改善活動を行うことです。改善につなげずに行うテストには意味がありません。視力検査を何度やっても視力は回復しません。
特にテストコードについて言えば、それ自体が保守を必要とする「負債」とも言えます。「単体テストの考え方/使い方」では以下のように説明しています。
テスト・ケースは多いほど良いと思っている開発者も多く存在します。しかしながら、その考え方は間違いです。なぜなら、コードは資産ではなく負債 だからです。つまり、コードが増えれば、ソフトウェアにバグが持ち込まれる経路が増えることとなり、プロジェクトを維持するコストもさらに高くなってしまう、ということです。そのため、この問題を解決するためには、コードは最小限にすべきなのです。
そして、ここで言うコードには、テスト・コードも含まれます。
改善につながるような、価値をもたらすテストコードはこの負債を打ち消しメリットが上回ると思いますが、質の低いテストコードは無い方がいいです。
自動テストはコードの変更に勇気を与える
テストの意味と注意点を確認したところで、次に「自動テスト」が何を解決するのかを考えます。開発者は自動テストに「迅速なフィードバックとスピード」を求めています。いつコードが壊れたか判明すれば、素早く改善活動に繋げることができます。勇気を持って変更を加えることができるようになります。
テストコードの質を上げる
ここまで、テストがしやすいコードを書くことが、「読みやすく、使いやすい、まあまあ高品質なコード」になり、自動テストが変更に勇気を与えることを説明しました。ここからはテストコードの品質について考えます。テストコードの品質を上げることで、開発者が変更を加える際、不安を募らせることが減り、コードが「読みやすく、使いやすいものである」という自信を得ることができます。
コード変更時に感じる、不安の正体を探る
コードやサービスの規模が大きく複雑なほど、変更には不安が伴います。この不安の正体を探ります。まず「良いテスト」が何であるか、何を解決しようとしているのかを調べます。そこから不安の正体を明らかにします。
「レガシーコードからの脱却」では以下を述べています。
良いテストの基準とは、テストが未知の理由ではなく既知の理由で失敗することと、その既知の理由で失敗するテストはシステム内で1つだけであることだ
「単体テストの考え方/使い方」で「良いユニットテストを構成する4本の柱」として以下を述べています。
- リグレッションに対する保護
- リファクタリングへの耐性
- 迅速なフィードバック
- 保守のしやすさ
ここからわかるのは、変更の際、開発者は以下の点を不安に感じると言えます。
- 加えた変更が開発者が意図しないふるまいにも影響する
- 開発者にとって未知の、仕様上の影響がある
この不安を解消するのは、「リグレッションに対する保護」です。
リグレッションに対する保護
リグレッションとは、「コードの変更等特定のイベント後に発生し、意図したように機能しなくなる、退行のこと」です。テストに「リグレッションに対する保護」がどれだけ備わっているかは以下の点に着目することで把握できます。
- テスト時に実行されるプロダクションコードの量
- コードが扱っているドメインの重要性
- コードの複雑さ
実行されるプロダクションコードの量・扱うドメインの重要性
実行されるプロダクションコードの量は、テスト多く書くか、E2Eテストのようなスコープの広い自動テストを行うことで、増やすことができます。実行されるコードを増やすことで、リグレッションが見つかる可能性が高まります。
ただし、注意が必要です。テストを多く書いても、改善につながらないようなテストコードは前述した通り、負債です。逆にテストの質が悪くなります。またE2Eテストに関しても、「壊れやすさ」や「実行速度が遅い」という点で注意です。ThoughtWorksのTechnology Radarという記事では、コストの高さや環境構築の複雑性から、非推奨としています。本番環境での手動テストの方がシンプルなケースもあると思います。どちらにも共通して言えるのは、「コードが扱っているドメインの重要性」も考慮する必要があるということです。プロダクトが解決しようとしている課題に対し、そのコードがどれだけ役に立つのかを考えることが大事です。とても大事なコードであるなら、ある程度自動テストをやりすぎてもコストを回収できるかもしれません。まずは、チーム内でプロダクトにとってどんな機能がどれくらい重要なのかを擦り合わせることで、何にどれだけ「リグレッションに対する保護」を備える必要があるか明確になります。そしてテストコードの品質を上げることにつながります。
コードの複雑さ
コードの複雑さについてはある程度数値化することができます。循環的複雑度はコードの複雑さを表す指標の1つです。「単体テストの考え方/使い方」では以下のように定義しています。
この数値が高いほど、複雑な傾向にあると言えそうです。この複雑さに対して、どれだけテストが行われたかはカバレッジがある程度参考になります。様々な種類がありますが、ブランチカバレッジ(分岐網羅率)が使いやすいかもしれません。ブランチカバレッジは、以下のように表すことができます。
ただし、カバレッジの数値が高いからと行って、品質の高いテストというわけではありません。あくまで、「どれだけテストを行ったか」という指標に過ぎません。「カバレッジが高い」だけで「リグレッションに対する保護」が備わっている訳ではありませんし、「リグレッションに対する保護」だけでテストコードの品質が上がる訳でもありません。あくまで参考となる指標です。またカバレッジを上げる方法は、テストコードだけではありません。プロダクションコードの分岐を減らし、複雑さを下げることもカバレッジをあげる方法です。
開発を妨げない
テストコードの質を高めるには、質の低いテストコードを下げる必要があります。テストはプロダクションコードの改善や変更をサポートするための工程です。通常の開発を阻害するようなテストコードは質が低いといえます。阻害する例として以下のようなケースが考えられます。
- テストを書くのが難しい・大変
- 自動テストが遅いので、待ち時間が発生する
- すぐにテストが壊れるので、変更(改善)を避けて、そのままにする
- たまにウソをつくので、テストが失敗しても無視している
テストを書くのが難しい・大変・壊れやすい時はプロダクションコードを改善する
「テストを書くのが難しい・大変」要因の1つには、プロダクションコード自体に問題がある場合があります。テストコードの書き方に悩むのではなく、「プロダクションコード改善のきっかけ(センサー)」と考え、テストでどうにかするよりプロダクションコードを改善しましょう。
例えば、テストの前準備に必要なコードが長すぎて大変であれば、そのテスト対象のコードが大きすぎることを示唆しているので、コード分割を検討すると良いかもしれません。実行のたびに結果が変わってしまい、テストを書くのが難しいのであれば、そのテスト対象のコードには意図しない変更が影響する可能性がありそうです。副作用を減らしてコードの見通しを良くしたり、副作用を限定的な箇所にとどめるような設計を検討すると良いかもしれません。
ネガティブなフィードバックをテストコードに還元するのではなく、プロダクションコードに適用して改善していくことが、壊れやすいテスト防ぐこと(保守のしやすさ)にもつながります。
自動テストが遅い場合、テスト比率を見直す
「自動テストが遅い」と、次の行動までの待ち時間が発生し、開発を阻害します。また「迅速なフィードバック」は質の良いテストを語る上では不可欠です。フィードバックが遅いと一番大事な変更のチャンスを失ってしまいます。この問題を解決するには、テスト比率を見直すと良いかもしれません。テストピラミッドという自動テストのモデルが有名です。
高い層ほどリグレッションに対する保護を備えなくてはならないのに対し、低い層は迅速なフィードバックを備えなくてはなりません。高い層に位置するE2Eテストは前述した通り、実行されるプロダクションコードの量は多いですが、実行速度は比較的に遅いです。低層に位置する単体テストは、E2Eとは逆の特性を備えています。自動テストが遅い状態にならないためにも、可能であれば高層のテストを低層に分解していく働きが必要です。この自動テストの比率の形は、必ずしもピラミッドの形になっている必要はありません。プロダクトによっては、四角になることもあります。
また、テスティングトロフィーという考え方が、webフロントエンドの間では有名です。中層(統合テスト)を厚くし、トロフィーのような形を目指すモデルです。
このモデルに当てはまるプロダクトもあると思いますが、これは「単体テスト」の捉え方によって生まれた考え方だと思います。単体テストの定義については省きますが、人によって解釈のブレがとても大きいです。「単体テスト」「統合テスト」のような言葉の解釈が曖昧なので、「テストサイズ」という考え方もあります。テストピラミッドの低層も、テスティングトロフィーの低層から一部中層にかけても、テストサイズでいう「small」に該当するので、結局同じ考えとも解釈できます。どのモデルにしろ、本質は「リグレッションに対する保護」と「迅速なフィードバック」の両立のために比率を適正に保つ働きかけをすることです。それによって自動テストが開発を阻害することを防ぎます。
テストがウソをつかないように、リファクタリングへの耐性を備える
テストがウソをつくようになると、開発者はテストが失敗しても無視するようになります。ウソには偽陰性と偽陽性の2種類があります。偽陰性とは、「テストが成功したにも関わらず、検証された機能に欠陥があること」を指します。偽陰性は、「テストが失敗したにも関わらず、検証された機能の振る舞いが正しいこと」を指します。偽陰性の悪影響は説明の必要がないと思いますが、偽陽性にも注意が必要です。嘘の警告に隠れて、本当の問題も開発者が無視してしまうようになるからです。この偽陽性を防ぐためには、「リファクタリングへの耐性」が鍵となります。「リファクタリングへの耐性」とは、テストが失敗することなく、どのくらいプロダクションコードのリファクタリングを行えるかということです。すなわち、偽陽性が生まれづらい性質といえます。
テストコードの目的を明確にする
偽陽性が発生してしまう原因(リファクタリングへの耐性が備わっていない原因)として、実装の詳細についてテストを行っていることが考えられます。本来、関心のない部分に自動テストを施してしまうことで、コードの変更に対して都度、頻繁に失敗してしまいます。何をテストするのか目的が明確になれば、実装の詳細へのテストを防ぐことができます。
自動テストを行うのは、不安の解消を行い、変更への勇気を得るためです。そして不安とは、意図しないふるまいを危惧するものでした。そのため、テストは「ふるまい」に対して行えば良いということになります。もう少しこの「ふるまい」について補足すると、「ユーザーから観察可能なふるまい」となります。どうやって実装されているかではなく、「ユーザーが何をできるか・何をするか・起こるか」に対してテストを行えば良いということです。この点を意識すると、テストコードの書き方も変わっていきます。テストケース(タイトル)にはふるまいを示します。この結果、テストコード自体も、解読に実際の実装を知らなくても良くなるので、比較的、読みやすいものになります。
さらに公開しなくてもいい実装やオーバーエンジニアリングに気づくこともできます。ふるまいから見た時、不要なオプションや不自然なテストケースは、本質的には不要である可能性もあります。削除するか、その実装の内部に入れて公開しない(カプセル化)等のリファクタリングにつながります。
結論
「読みやすく使いやすいコードを書くと、まあまあ品質が良くなる」と仮定しました。その総括をします。
テストしやすくすると、コードは読みやすく使いやすい
凝集度が高く、結合度の低い、適切に小さなコードはテストがしやすいことを説明しました。そしてこれらの要素を備えると、読みやすく使いやすいコードになることがわかりました。
質のいい自動テストは変更を加える勇気をくれる
質のいい自動テストを行うことで素早くフィードバックをもらうことが可能となり、コードを変更する勇気を得ることができることを説明しました。また自動テストからのフィードバックにも触れ、コードの変更に適用させる方法も少し触れました。質のいい自動テストは不安の検知に役立つことがわかりました。
まとめ
自動テストは変更のハードルを下げ、テストしやすいコードを矯正します。テストしやすいコードにすると、読みやすく使いやすいコードになります。現在そして未来に渡り変更しやすくなることで、機会損失を防ぎ、ビジネスにも貢献します。よって、「まあまあ良いコード」が実現できます。
課題
今回述べていない話題がいくつかあります。こちらについては各チームによっても方針が大きく異なるポイントかと思います。これらの話題について話し合う場があると、もしかしたらチーム全体のコード品質がより上がるかもしれません。
どこまでパフォーマンスを求め、何をどこまで犠牲にしていいか
コードの可読性と保守性については触れましたが、パフォーマンスについては言及しませんでした。もちろんパフォーマンスチューニングもとても大事です。しかし、パフォーマンスと引き換えに可読性や保守性が下がるケースもあると思います。すべてが最高水準なのが理想ですが、現実的にどのラインで妥協するのか、要求水準をチーム内で決めておくと、「良いコード」の解像度が深まるかもしれません。
どこまで読みやすい・使いやすいコードにするか
どのレベルのエンジニアを想定して、読みやすい・使いやすいコードにするかを考えることも大事です。仮に非エンジニアまで対象を広げた場合、コメントで文法の意味まで説明しなくてはなりません。想定するレベルをすり合わせておくと、やり過ぎを抑えられるかもしれません。
プロダクトで最も大事な機能は何か
「コードが扱っているドメインの重要性」が「リファクタリングへの耐性」をどこまで備えるかに影響します。自動テストをどこまで書くかが変わり、適切な自動テストのモデルが変わります。テストピラミッドなのか、ハニカムなのか、テスティングトロフィーなのか、それともまた別の何かか、そこまで影響が波及すると思っています。自動テストモデルありきではなく、プロダクトやドメインに向き合うことが近道だと思います。
VRTやスナップショット、インタラクションテストにどんなメリットとデメリットがあるか
特にwebフロントエンドには、ヴィジュアルリグレッションテストやスナップショットテスト、インタラクションテストというものが存在します。これらについては今回触れませんでした。それぞれにどんなメリットとデメリットがあるかを話し合うと、テストサイズのsmall・medium・largeのどれに属するかがわかり、目指す自動テストモデルが変わったり、より鮮明になるかもしれません。
どこまで自動テストを行い、何を手動でテストするか
開発者が行う自動テストは、どちらかといえばチェックに近いです。つまり品質保証とはやや遠いということです。品質の保証にはQAエンジニアの協力はもちろん、コードレビューや手動による動作検証が欠かせません。また、E2Eテストは様々なコストが高いことにも注意です。手動の方がコストが低い場合があります。自動テストのコストについて議論しておくと、責務が明確になり、行き過ぎた自動テストを防げるかもしれません。また、QAエンジニアとの協業にも良い影響を及ぼすかもしれません。
そもそも、コードを変更せずに済むにはどうしたら良いか
コードを変更しやすくする方法については触れましたが、そもそもコードを変更しないで済む方法については触れませんでした。こちらはアーキテクチャが大きく影響する分野だと思います。コードを変更しない層を設けたり、逆に変更を吸収するような層を設けたり、ビジネス上のドメインに依存するような設計にしたりなど、様々な方法があります。変更が限定的であれば、そもそもテストが不要な箇所が増えるかもしれません。結果、変更へのスピードは上がるかもしれません。
Discussion