あのUnit Testing Principles, Practices, and Patternsの本の方のpodcastを聞いて、メモ書きながらでないと聞き流しちゃうので、メモを書いておきます。
Unit Testとは?
抽象的(ハイレベル)に定義すると、開発者が書いたコードに対して、その開発者が書くテストすべて。
詳細的(ローレベル)には、一つの振る舞い(behaivor)をカバーする自動化されたテスト。どれだけテストが早く始まり(quick)、速く実行されるか(fast)、他のテストから独立している(isolated)。
このどれかが達成できないとそれはユニットテストにならず、Integration Testになる。
Unit Testいる? Integration Testだけで良くない?
Unit Testに反対する人のUnit Testは価値が低く、信頼できないものになっている。リファクタリングのためにテストが失敗するようになり、壊れやすく、バグに対して保護が少ないものになっている。その立場からするとIntegration Testはコードをたくさんカバーし、バグをキャッチでき、そして、false positiveを出すことも少ない。
そういうIntegration Test派の方に対して、言いたいのは それは単にちゃんとUnit Testをかけていないだけで、壊れやすく、価値を提供しないように書いていているだけ。 Unit Testを取り除くのではなく、改善して、価値があるようにリファクタするのが良い。
Unit Testのゴールは?
メインゴールは、ソフトウェアプロジェクトの継続的な成長を行うこと。なぜかこのゴールかと言うと、プロジェクトが成長して、プロジェクトのコードの量が増え、コードの中にバグが有る可能性がある場所が増えるから。これは、コードをデプロイする障壁になったり、手動テストにたくさん時間を使う必要がでてくる。手動テストは市場に出るまでの時間を大幅に上げてしまう。ユニットテストはこの状況を助けることができ、セーフティーネットとして動く。またコードのリファクタも怖くなくなり、簡単になる。
TDD(Test Driven Development)、Test First Approachについて好みある?
TDDやTest First Approachに価値はあるが、タイミングを考える必要がある。まだドメインがよく分かっていなかったり、どうやって実装するかわからない状態でテストを書き始めると、テストが重荷になる。プロトタイプは先に作ったりとか、ドメインへ理解が進んだタイミングでやると良い。
TDDは既存コードが有るときにうまく働く。例えばバグを再現するテストを書いて、 テストを失敗させて、修正するとき、テストが正しく動くことを確認できる。 なぜならテストがいい理由で失敗するのを確認できていて、テストが通るのも確認できているため。
TDDは2つの問題を解決する。
- テスト自体がうまく動くことを確認できる。コードファーストでも出来るけど、わざとコードの方を壊してテストが失敗することを確認する必要がある。
- 開発プロセスを構造化出来る。まず最初に抽象的な全体的なテストを書いて、その後に細かいユニットテストを作ることになる。これは構造を与える。
TDDとかってクライアントとか視点で書くので、良いコード書きやすくなるって聞いたけど?
良いコードはテストがかける必要があるけど、テストが書けるからといって、良いコードとは限らない。ただ、テストが書けることを確かめられる。
なぜコードカバレッジは重視されるか、他のメトリクスはあるか。
自動的に取れるメトリックなので人気がある。50とか40%などで低い場合は、十分にテストされていないことを示すが、90%か100%だったとしても、テストが良い品質かどうかは分からない。起動するだけのテストだと、エラーにもならないが、たくさんのカバレッジを確保できるなど。
ユニットテストを始めるときに狙うべきものとは? testはdevelopment cycleに統合するべき、コードベースで大切なところにフォーカスするべき、またメンテナンスコストを少なくし最大限の価値を得るべき、など3つ触れていたともいますが、説明しますか?
-
testはdevelopment cycleに統合するべき
testから価値を得られるのはtestを実行するときなので、コードを変更する度などに実行するべき。 -
コードベースで大切なところにフォーカスするべき
コードでも大切な部分とそうでない部分があるため。 -
またメンテナンスコストを少なくし最大限の価値を得るべき、など3つ触れていたともいますが、説明しますか?
言うのは簡単だけど、成し遂げるのは大変で、本でカバーしている部分。
実用的、実利的であるというときに、どのぐらいのコードカバレッジがいいと思いますか?
コードカバレッジがこのぐらいがいいなどを言わないし、目標は決めません。 もし80% 70%とかを目標にして、それ以下になったらビルドが落ちるようにしたとしたとすると、開発者は価値のないテストをたくさん書き始め、また難しいのでアサーションを書かなくなる。 ただ、ドメインモデルに関しては90%など高いカバレッジを持つべき。
メンテナンスコストに関してユニットテストがなぜメンテナンスコストが高くなり得ると思いますか?
悪名高い部分でいうとテストが脆い(brittle)とき、そのテストはいい理由では失敗せず、毎回のリファクタリングで壊れる。これは4つのユニットテストの柱の一つでもある。
ユニットテストの4つの柱についておしえてください。
(説明のために順番が違う。)
-
3.Fast feedback
どれだけ速くテストが実行できるか。速くフィードバックを受け取るために重要。バグがプロダクションなど遅いステージで見つかるほど、治すのに労力が必要になる。 -
4.Maintainability
どれだけ簡単にテストを読めるか。テストが大きくなればなるほど難しくなる。サブメトリクスとして、テスト操作の依存関係をどれだけ簡単に管理できるかがある。例えばDBやネットワークの状態など。 -
1.Protection against bug
バグをコードベースに入れたときに、どれだけバグが見つかりやすいか。なので、これは基本的には、"テストを実行したときにどれだけ多くのコードを実行するか"になる。 -
2.Resilience to refactoring
テストがカバーしているコードをリファクタリングしたときにどれだけテストが壊れないか。
メトリクス- 偽のアラーム(false alarm)やfalse positiveがどれだけ起こりやすいか。偽のアラーム(false alarm)はテストは落ちるけど、コードはちゃんと動く場合。リファクタして、コードの詳細を変えて、そのテストが実装詳細に依存しているときによく起こる。
この"Resilience to refactoring"は一番重要なメトリクスで、良いテストと悪いテストの違い。 リファクタは最初はしないので、最初は重要ではないが、機能を追加していくと必要になる。最初は必要でないので、たくさんの開発者が気を使わない理由。
テストはシグナル(Signal)とノイズ(Noise)に分けられる。シグナルはリグレッションをキャッチできるか。ノイズは偽のアラーム。ノイズも大事、なぜならノイズが多すぎると、本当のテストの誤りに気づかない。
Testは実装詳細に依存してはいけないってどういう意味?
こんな感じの関数があったとする (実際はpodcastなので、ここまで詳細にいってません。)
class Response(private val time) {
val isSoon = time.within10Days()
fun output(): String {
val body = if(isSoon) "soon" else "not soon"
return "<html><body>Comming $body</body></html>"
}
}
fun generateResponse(time): Response {
return Response(time)
}
2つの方法がある。
1: 全部のHTMLをチェックする。これは良いテストで、かつ、brittle(脆い)テストの例。
assertThat(
generateResponse("20231014").output()
)
.isEquals("<html><body>Comming soon</body></html>")
2: 内部の条件をチェックする。これも脆い。実装に依存する。(isSoonを変えると壊れる。)それによって、false positiveが増えて、バグを見つけるのを難しくしてしまう。
assertThat(
generateResponse("20231014").isSoon
).isEquals(true)
良いテストをもう少し詳しく言える?
どうやってユニットテストを構成するかで触れている。Arrange Act Assertで行う。(説明しているけど、ググるとでてくるのでググってみてください。)。
- 最初のアイデアはif文をテストのどこでも使わないこと、テストはできる限りdumpであるべきであるから。テスト自身を確かめる確かめるテストが必要になる。
- いちばん重要なガイドライン、テスト同士が依存してはならない。テストをいじったときに他のテストが落ちてほしくないから。
複数のAssertionはどう思う?
いいと思う。多分このAssertionが来ている理由は、UnitTestは小さい単位をカバーするべきというところから来ている。UnitTestは1つの振る舞い(Behavior)を検証するので、3つクラスを作ってテストしたら3つのクラスで確認しても良い。
Mockについてはどう思う?
よく議論になるトピックの一つ。
2つの派閥がある。
London school, Mockist schoolとも呼ばれる。 どんな依存関係にMockを使う。
Classical school, デトロイトとかシカゴschoolなどとも呼ばれる。これはMockはout of practiceな依存関係にだけ使うというもの。
Mockは一番のテスト壊れやすく(Brittle)にする理由である。
class User(
val company
)
London school。
val companyMock = mock()
val user = User(companyMock)
ただ、重要なのは2つのクラスの最後の状態であり、どのようにコミュニケーションを取るかは重要ではない。
ただ、Classicalだと、DatabaseとかをMock置き換える感じになる。一旦Mockのメリットに立ち返ると、Mockに置き換えたいのは、そのコミュニケーションが長持ちする(durable)場合だけ。
APIを作っているとする。2つの依存関係がある。1つはDBで、もう一つはmessage busだったとする。クライアントから受け取ったらDBを書き換えて、messageを送る。さて、この2つの依存関係のうちどれをMockにするべきか?どのように関わっているかによる。
メインのガイドラインは、新しいバージョンのソフトを作ったときに互換性がある必要があること。例えばbusに送るメッセージが別チームで同時にリリースできない場合など。構造が変わると別のソフトは理解できない。このような互換性を保つようなときにMockは役立つ。
だが、このような互換性はDBには必要ない。なぜなら別のソフトはそのDBに触っておらず、ソフトと共にデプロイしている。これがDBに対してMockを使わない理由。DBとアプリケーション間の通信はこの場合実装詳細。ユーザーから見て、DBとアプリケーションは1つのシステムとして振る舞う。DBの最後の状態だけテストでは確認すれば良い。Unmanaged dependenciesだけMockする。
dependenciesは2つに分けられる。ManagedとUnmanagedと分けられる。UnmanagedはMessage busやサードパーティAPIや課金システムなど。これらの依存関係は見えているので、Mockにする必要がある。
Unit Testのアンチパターン、バッドプラクティスある??
全部のアンチパターンなどは4つの柱から作ることができる。
よく言われるのはprivate methodをテストするな。privateなのには理由がある。priavteだということはproductionコードはそれが必要ない。それをテストするということはテストが壊れやすくなる(brittle)ということ。(resilience to refactoringに違反)もう一つはprivate のstateに関するもの、こちらも同様。
あなたのリーダーシップを教えて
"put your thoughts in writing" 。ブログに書き始めてからたくさんのことを学んだ。10年プログラマやってたけど、書き始めてからのほうが学んでいる。 書くのをpublicにしていったほうが良い。誰も読まなかったとしても、それは価値がある。考えを構造化する必要があり、関係性が見えてくる。書き始める前のほうが書き始める後より理解していないはず。全員に言えるアドバイス。