👋

単体テストはただの負債だから今すぐ書くのをやめるべき (かもしれない)

2022/08/18に公開

初めに

若干扇情的なタイトルであるということは自覚しています。

個人的に疑問に感じていることなので、色々な人の目に止まって意見がもらえると嬉しいと思ってこうしました。

前提条件

チームでバックエンドのWEB APIを管理し、他チームや外部へ公開しているような場合。

(そのためフロントエンドや組み込み等については何も言及しませんが、似たような考え方はもしかしたらできるかもしれません。)

用語の定義

用語 定義
単体テスト メソッド単位で行われるホワイトボックステスト
結合テスト WEB APIを実際に建ててEnd to Endで行われるブラックボックステスト
脆いテスト メインコードの修正によってWEB APIの振る舞いが変わらないにもかかわらず失敗してしまう可能性が高いテスト
リファクタリング メインコードの可読性や保守性を向上させるが、WEB API自体の振る舞い自体は変わらないような変更

この記事の主張

単体テストは脆いテストであるため、リファクタリングを困難にし、保守性を著しく落とす等のデメリットがある。
このデメリットと単体テスト自体の保守コストは単体テストを書くことによって得られるメリットよりも大きい。
そのため、単体テストは (内部のアルゴリズムが特に重要な場合等の書くことに明確な理由がある場合を除いて) 書かず、
結合テストを書くことに労力を割くべきである。

参考

主張を裏付けるような書籍やWebページを紹介する。

主張のメインソースとしては最初に紹介するGoogle本で、それ以外はウェブ上から恣意的に集めてきた物である。

Googleのソフトウェアエンジニアリング

https://www.oreilly.co.jp/books/9784873119656/

12.2.1 変化しないテストを目指す より

理想のテストとは変化しないテストである。つまり、テスト対象システムの要件が変化しない限り、書かれた後は二度と変更の必要が無いテストだ。

テストを書いたら、リファクタリングをし、バグを修正し、新機能を追加する際に、そのテストに再度触れなければいけないというのは何かが間違っているということである。

これが正しいとするならば、内部の実装によって書かれるホワイトボックスのテストはメインコードの修正のたびに変更が必要になる可能性が高いため、何かが間違っている、すなわち書く方がおかしいということである。

12.2.2 公開API経由のテスト より

このこと (テスト対象システムの要件が変化しない限りテストが変化する必要がないこと)
を保証するのに群を抜いて最も重要な方法は、 テスト対象システムのユーザーが呼び出すのと同じ方法でシステムを呼び出すテストを書くことである。
それはつまり、システムの実装の内部的詳細部分ではなく、システムの公開APIに対して呼び出しを行うということだ。

我々が本章の文脈で「公開API」と述べる場合に実際に言及している対象とは、
ユニットのコードのオーナーであるチームの外部にいるサードパーティに対し、そのユニットが公開しているAPIだ。

これはこの記事の主張そのままとなる。実際外部との契約のみを担保するというのはテストコードとして必要十分に見える。

テストコードが多すぎても保守が辛いだけというのを考えると、テストコードは結局品質と工数のトレードオフになるから、どこまでテストするかの線引きは重要なはず。

単体テストをどこまでするかは結局「単体」の定義によるというようなことも本には書かれていて、それを公開APIとする案には個人的には納得した。

James O Coplien氏のTDD批判

@marubinotto氏のブログから引用する。引用の引用になってしまっているが、元論文を読む気力がなかったのでご容赦いただきたい。
https://ubiteku.oinker.me/2015/07/27/why-most-unit-testing-is-waste/

もちろんユニットテストにビジネス価値と直結する部分が全くないとは言い切れない。
例えば、キーとなるアルゴリズムを実装している部分などだ。しかし、基本的にビジネス価値はより粒度の高いテストで表現すべきである、
というのがCoplien氏の主張である。

結合テストの方が粒度は低い気がする (引用は逆になっている気がする) が、単体テストよりも結合テストの方がビジネス価値が高いという主張には同意できる。

逆に言えば、ビジネス価値が高い単体テストは書くべきでもある。

Henrik Warne氏の批判

https://postd.cc/a-response-to-why-most-unit-testing-is-waste/
James O Coplien氏のTDD批判をさらに批判する内容。

James氏とは想定しているコードの状態や技術力に相違があるように感じた。例えば以下

Jamesの意見によると、ビジネス価値のあるテストとは、ビジネス要求に由来するテストだけです。
ユニットテストは完成した機能ではなく構成部品を検証するテストに過ぎないため、信頼を得られません。
ユニットテストは、 この機能はこう動作すべきだというプログラマの思いつき に基づいています。
しかしプログラマは、いつも要求を小さな部品にかみくだいて把握しているのであり、それがプログラミングの手順なのです。
時には誤解もあるでしょうが、それは例外であり、いつもそうではないというのが私の見解です。

綺麗に分断され疎結合でかつ読みやすい完璧なコードで有能な人材のみが開発しているのであればこの批判ももっともかもしれないが、現実にはそのような場合はごく稀であるはず。
であれば「時には誤解もあるでしょうが、それは例外であり、いつもそうではない」は成り立たないと思う。
他の主張についても優秀な人員のみで構成されている前提であるような気がしており、 限られた技術力で開発する場合、結局のところJames氏の意見の方が優位になると思えた。

一方で、アルゴリズム的な部分に単体テストが有効というのはその通りである。

hide1024氏のブログ

https://hide1024.hatenablog.com/entry/2016/01/30/014800

まず、”個々の機能を正しく果たしているかどうかを検証する”とありますが、より噛み砕くと、
”詳細設計書で書いている機能を正しく果たしているかどうかを検証する”となります。
なので単体評価書の項目作成におけるインプットは詳細設計書であり、単体テストで関数レベルのテストがしたければ、
関数レベルの詳細設計書が必要になります。

この点については自分が詳細設計をざっくりしがちな人間なので何とも言えないが、ウォーターフォール的に考えれば一理あるなとは思った。

実際ガチガチにウォーターフォールをやっている人たちはどのように考えているんだろうか。

愚直に工数を懸けて、関数レベルの詳細設計、単体テストするより、デバッグ、結合テストに工数を充当したほうが、品質があがる

これについてはその通りだと感じる。実際結合テストを多く実装した方がバグを抑止できている実感があった。
これはサービスのバグと自チームのAPIのバグとがリンクしやすいからで、単体テストはあまりにも遠すぎてまったく抑止できている感じがしなかった。

@hecateball氏のブログ

https://shiodaifuku.io/articles/17638219-f1ef-406c-b20c-7f426d61b2af

Design by Contractの考えに基づけば、契約が履行されることを保証するテストコードは意味のあるテストコードであると言えます。
逆に、契約の履行を保証する役割を果たさないテストコードや契約と何ら関係ない動作を担保するテストコードは(この考えのもとでは)無意味であると言えます。

外部との契約に従いテストコードを書くという姿勢はGoogle本のそれと近しく納得できた。

ホワイトボックステストがテスト対象となるコードの実装に制約をかけるテストであることを理解しているエンジニアは極めて稀な存在です。

これはその通りだと思っていて、実際自分としても最初はそのような認識はなかった。

テストが無いよりはあった方が良い、という認識で止まってしまっている可能性があると思っていて、
場合によってはテストが無い方が良いということも広く知られるべきなのかもしれない。

まとめ

単体テストはただの負債なんじゃないかということを主張しました。

テストは絶対に必要ですし、無いコードなんてあり得ないというレベルですが、テストなら何でも良いというわけでもないはずです。

これが絶対に正しいなんて言うつもりはないので批判は大歓迎ですが、なるべく優しい語気でお願いします。m(_ _)m

Discussion