テストコードの品質

はじめに

私はテストを書くのは素人なので、「こいつなんにもわかってない」と思われるかもしれませんが、そういう人はスルーしていただければと思います。また、テスト駆動開発とか、アジャイルとか、いまいちわかっていません。この記事は、あくまでも分散ストレージをテストしていく中の一要素として、テストコードを見た時に、私が日々感じていることをまとめてみます。以下、関数レベルのunit testではなく、Integration test用のコード、くらいが対象です。

1.なにが最も重要か

製品コードの品質といった時、分散ストレージの場合は、1番は「データを壊さないこと」。随分離れた2番は「サービスが落ちないこと」です。その後、コンポーネントによって、性能や、拡張性や、可読性や、その他色々入ってきます。

テストコードは「バグを見つける」ためにあります。もう少し分類すると、「何かした時に、期待の動作をしていること(リグレッションしてないこと)」「なんかよくわからないことをした時に、壊れてないこと(stress, fuzzer, random walk)」がFSのテストの2本柱です。前書いた時は3本柱だった気がしますが、忘れました。なので、1番は「バグを見つけること」なのですが、それって項目に落とし込めてない気もするので、あえてひねり出すとすると、テストコードの品質項目で1番重要なのは、「そのテストが実行されること」です。素晴らしいテストやツールを書いて、それが書かれた後に二度と実行されないのが最悪のパターンです。意外とこれ、多いです。

その他は、重要な順に:

  • Correctness ・・・ False positive*1, False negativeが無い。
  • ロギング、再現性・・・エラーが起きた時に調査できること、再現できること
  • 可読性
  • バレージ
  • 性能
  • 拡張性

2.性能

製品コードでは、例えば O(N^2)のロジックを最適化してO(NlogN)にできたらすっごく嬉しいです。もしくは、Critical pathが10%早くなったらとても嬉しいです。が、テストコードは、直感的な実装で最適化された実装をチェックしたい場合が多いので、O(N^2)で構いません。別にテストを書く人がアタマが悪いのでN^2になっているわけではありません(そういう場合もあるだろうけど)。テストの性能は、そもそものテストの切り口や、パターンのピックアップで、現実的な時間内に終わらせることが重要で、コーディング中にテストコード自体の性能を考えることはあまり無いです。

3.スケーラビリティ、拡張性

将来の拡張性を考えて風呂敷を広げすぎたテストツールは、将来にわたってゴミを抱えたまま、
数年後に他のツールで置き換わることを待つことになります。製品コードが拡張性を考えないといけないのは、アップグレードをサポートする必要があるからです。既存の顧客が、既存のストレージを、データを保持したままアップグレードする。そうなると、何か一つ変えようとしても、昔適当に設計したものが足を引っ張ることになります。テストツールは、今その時点でベストであることが大切です。

スケーラビリティーも、製品コードである日リンクリストをツリー構造に変えようとしたら、うんざりするようなアップグレード計画が待っているわけですが、テストコードは、その時必要なスケーラビリティを定義すれば十分です。

4.マクロ病

製品コードを書く人(うちだとCコーダー)は、特にMAGICナンバーを嫌う傾向があります。

何かの処理
sleep 10 # 適応されるまで待つ
次の処理

これを、APPLY_SLEEP_SEC=10 と定義せよ、みたいなやつ。もっというと、4 * 1024 * 1024 * 1024 を
GB=1024*1024*1024 (or 1 << 30) として4 * GB とせよ、みたいなの。個人的には、単にテストの見通しが悪くなるだけなので、
「マクロ病」と呼んで無視しています。とくに、コマンド実行の run(cmd) という文で、cmd を切り刻んだロジックの集合にして喜ぶ人たちがいますが、同意しかねます。

5.スタイルは守る

うちだと Cは style9, pythonはPEP8, で、それぞれ少しカスタマイズされてますが、特別な事情がない限り、これを守るのは絶対です。逆に、Style Guideに定義されていない「好み」の部分をCRで指摘するのはあまり好きではないです。もしその好みが全社的な標準なら、Style Guideで定義されているべきなので。で、Style Guideを守ったうえで、最低限の労力で、必要最小限のテストをまず書く、というのが大事だと個人的には思ってます。

6. エラーハンドリング

製品コードはエラーハンドリングの塊なので、たとえば、pythonでいえばスタックトレースがベチャッと吐かれることはまずないですし、panicしたら大変です。テストコードも頑張ってエラーハンドリングしちゃう例をよく見かけますが、単に必要情報をログに残してスタックトレース吐いて落ちたり、Kernel側のテストであれば、panicするほうが嬉しいです。エラーハンドリングに関しては製品コードとテストコードは真逆のスタンスになります。

7. 再現性とランダム性

再現性を高めるには、どういう操作がどの順番で、どのタイミングで行われたかを記録するか、事前に計画する必要があります。ランダム性を高める上で、I/O Aと I/O Bが、ほぼ同時に実行されて欲しい、という場合があります。ほぼ、というのは、Aがちょっと早い場合と、Bがちょっと早い場合と、AとBが本当に(物理的には本当に同時はなくても、まあ、現実的なレベルで)同時に実行される場合。原子時計でも積んでいないとそこは不確実性に任せてストレステストを書くこともあります。その場合は、エラーが起きた時にできるだけ絞り込む工夫はしますが、100%の再現性は求めないことになります。何事もバランスです。

*1:必要悪として許容する場合もある