読者です 読者をやめる 読者になる 読者になる

テストの男坂

ござ先輩の記事に触発されたので書いてみる。

自分が書いたプログラムに対してテストをする必要があることについてはご認識の通りでしょう。自分の趣味に留まらず、そのプログラム(で作られたシステム)がビジネスを回す上で必要だったり、「お金」という対価を得る場合は特に。
「ちゃんとテストしました」と自信満々で言われ、いざ使ってみてつまんない実行時エラーが出てきた時、受け取った側はどんな顔をすればいいかわかりません。ましてやそんなのが何回も続くとその人の信用はガタ落ちです。時間をかけにかけまっくてシステムを作って、満を持してユーザに見せた時にそんな状態だと人格そのものを否定される可能性だってあります。
言い訳がましく「入ってるべきデータが入っていなかったからです」と報告しても「なんで落ちたかの説明よりもとっとと直せ。お前が作ったプログラムは信用ならん」と言われるの、切ないですよね。でも、ユーザがあなたに依頼してるのは「ちゃんと動くもの」を作って頂戴、ってことなんです。

テスト、テスト、テスト

開発者にとって一番身近なのは単体テストでしょう。最近では単体テストと言えばテストコードによる自動化が当たり前になってきています。何回も同じテストを手動で繰り返すのかったるいですもんね。デグレが発生しても検知してくれるし。
ただ、テストコードを書いているから品質はバッチリです、というのはいささか危険。システムの全ての振る舞いをテストコードに落としこむのは時間がいくらあっても足りません。いくらテストコードを書いても、「バグが存在しないこと」は証明できません。
言い方はアレですが、単体テストは所詮、単体テストであり、システム全体の品質を担保するものとは考えない方が良いでしょう。品質については、「次のテスト工程で観点の異なるテストケースを確認する為に簡単には落ちない」くらいにしておいた方が気が楽になるとおもいます。単体テストで全部のテストパターンを考えるとなると、ビックバンテストを前倒しにやってるだけになっちゃいますからね。

テストコードを書いてるんだからバグは存在しないでしょ?の誤解

テストコードの中で、「Aであること」は定義できますが、「Aでないこと」の定義には膨大なテストパターンが必要になります。
例えば、「Aテーブルにレコードを登録する」処理を呼び出した時に「Aテーブルにレコードが登録されること」は容易に確認できますが、その処理の中で「Bテーブルにレコードが『登録されない』こと」を証明するには考えられる前提条件全てに対してテストしなければなりません。前提条件には入力パラメータだけでなく、システムで扱う全ての要素(テーブル、CPU、メモリ等々)を考慮しなければならず、証明するにはテストパターンがいくらあっても足りないことは容易に想像がつくでしょう。Bテーブルの登録処理を呼び出していないから大丈夫、というのはあくまでもソースコード上の話であり、テストコードを実行した結果での証明にはなりません。テストコードを書いてもバグが存在しないことを証明できないのはこの為です。

じゃーどうすればいいのよ

だからといって、テストコードを書くことが意味のないことではありません。「Aであること」方式の定義はしやすいので、そのパターンを増やせば間違いなくバグは出にくくなります。テストしてない所にはバグが潜んでいる可能性があります。どこまで想像力豊かにテストケースを洗い出せるかがバグが少ないシステムへの分かれ道です。

  • 複雑なSQL
    • InsertやUpdateよりも、Select文。それも複数のテーブルを結合したものや、検索条件が気持ち悪いくらい入り組んでいるもの。
    • ストアド使ってるならそれも入れよう。
    • システムはデータを出し入れするものなのでここが間違っているとシステムの存在価値ゼロ。
  • そのシステムのキモとなるビジネスロジック
    • この処理ちょっと面倒だな、と思った箇所、そこがテストコード書くべき箇所です。
    • 共通的に使われる箇所もキモになるビジネスロジックでしょう
    • 表示するデータや登録するデータをメモリ上でこねくり回す処理なんかは絶好のテストコード記述ポイント
  • 手動でテストしづらい箇所
    • 絶妙なタイミングで確認するテストとか
  • 繰り返し実施するのがかったるい箇所

メソッドの作りも呼び出したらどうなるか、が定義しやすいように心がけることでテストしやすいものに大変身。assertを記述しやすいメソッドの構成を心がけましょう。
また、テストコードがあれば100%デグレに気づくわけではなく、テストコードに定義されていないものはデグレとして検知されません。例えばあるメソッドに、「Cテーブルにレコードを追加する処理」を追加機能として実装した時、元のテストコードを動かしてもテスト失敗することは無いでしょう。テストコードに「Cテーブルのレコードが登録されないこと」の定義がされていない限りは。そのような時はMockクラスを使うのですが、そこまでやるかは工数とスケジュール次第かと思います。変わるかどうかもわからない箇所にMockを仕込むよりも、処理を追加した時にテストクラスを見なおした方がかかる工数が低いこともあるでしょう。ユーザが欲しいのはテストコードでなく、「動くシステム」の筈なのでその辺を天秤にかけた方がいいと思います。

いきなり手を動かすのも大事だけど

考えるより手を動かしてテストコード書け、という話は必ずしも正解ではないと思います。なんたって私たちは知的労働者なんです。何をどう作るか考えた上で手を動かしたほうが効率がいいこともあるでしょう。処理を実装する際には、机上でも良いので実装設計することをオススメします。その際に同値分割とかディシジョンテーブルを用いて効率的なテストケースを作れると、ワンランクアップ。作りながらでも良いですが、ソースコードを書くテンションが高すぎて見逃すこともあるでしょう。はやる気持ちを抑えて深呼吸です。
設計者と実装者が分かれている場合、設計の段階でテストケース込みの設計書があれば、意思疎通のズレを早期に打ち取ることができます。
後は、扱うデータにnullが渡される可能性があるのか無いのか、nullの場合どうなるべきかを意識するだけでつまらないバグを減らすことができる筈です。データの出処を抑えることが良い設計の第一歩。
更に言うと、設計した時にデータの流れを見なおして考慮漏れがないかを確認すべきです。実装してから間違いに気づいてしまうと、なかなかソースコードを捨てられないことから対応がやっつけになることが多く、そんなコードを後世に残してしまったことを悔やむことになるからです。ちゃんと設計して実装するのとリファクタ込みで実装するのとトータルの工数がどれだけ差があるのかわかりませんが。

テストコードだけでは幸せになれない

テストコード書いて楽できるところは楽しようぜ、という世の中の流れは大いに賛成です。ですが、それだけでは幸せになれません。そもそも間違った仕様でテストコードを記述してしまえば、Allグリーンでも価値をもたらさないクソコードです。
設計と実装の距離を短くして、さらにユーザにも近づこうよ、というアジャイル的な流れが大きくなりつつありますが、「アジャイルであれば今までの失敗がなかったんだ」的なことになりそうなのがちょっと怖いです。ウォーターフォールでもアジャイルでも失敗したんじゃね?(もしくはその逆も)という観点が抜けてやいないか大いに気になるところではあります。価値をもたらさないクソシステムを量産する側にならないように一層気を引き締めなければならんなー、と感じる今日この頃でした。