Island Life

< Kumu Kahua Theatre: The Hilo Massacre | テストの最適化 >

2010/06/18

コードに対するテストも要るんじゃないかなあ

From RSpec のすごいところ:

結局、テストコードは何をもとにして書くのか、ということだよね。Test::Unit を使ってた頃は、テストコードはソースコードに対して書くものだと思ってた。でもそれは間違いで、テストコードは仕様に対して書くものということなんだ。

TDDで最初に書くテストは仕様に対するテストと考えて良いと思うのだけれど、 実装が済んだ後で (もしくは、実装がある程度具体化した後で) 改めてその実装に対するテストを書く ことは必要なんじゃないだろうか。

というのは、特定の条件を満たさないと通らないコードパス、っていうのは ほとんどのアルゴリズムにあって、 そういうパスを通すためのテストデータは実装が具体化してからでないと作れないから。 経験上、アルゴリズム内のパスが切り替わる境界条件付近にバグが潜んでることが 多いので、境界条件をつつくテストは仕様のテストよりずっと重視している。 (例えば浮動小数点数絡みで、計算結果に桁上げが生じて54bit目が 丸められる条件を突く、とか。外から見えない内部のテーブルがいっぱいになって 次のデータ追加でテーブルの拡張が起きる条件を突く、とか。)

実装が変わったら改めて境界条件を突っつくデータを用意しないとならない。 最適化すると大抵場合分けが増えるのでテストすべき境界条件も複雑になる。 最近はカバレッジ分析みたいなツール使って ある程度自動化するものかな? 私はロートルなんでいまだに手でデータを用意してるけれど。

そんで、そういうテストを書く場合、入力セットも期待すべき出力も 複雑になることが多いので、assert_equalをずらずら並べるテストというのは すぐに限界に達する。assert_equal並べるのは楽だからつい自分も書いちゃうけど。 どんな入力に対しても結果が満たすべき条件、というのを検査するテストルーチンを 書いて、それに実装の境界を突っつく色々な入力を与えるというのが良いと思っている。 簡単な目安として、assert_equalのexpectedに定数を書いてたらたぶんそれは 良くないテスト。(自分もそういうテストをたくさん書いてるんで、自戒込みで。)

たまに、テストしたい機能と、条件検査に使ってる機能が対でバグってて、 テストを通っちゃうこともあるので、別の処理系や言語で計算したexpectedを入れる ことはある。その場合はexpectedを計算するコードをコメントに書いたり しているけど、もっとうまい方法はあるかなあ。

(追記2010/06/20 10:56:49 UTC): From http://twitter.com/tyt/statuses/16608793012

http://tinyurl.com/2b86bkq 最適化のコードのテストは最適化されてないバージョンのコードを使えばいいのではと思った。入力の型以外はほぼ自動でできそうな感じ。(一回そうやってテストしたことあるけどテストはすぐ終ったし最適化後に仕様変更は無かったなあ。)

そうだなあ。 問題は、最適化されてないバージョンをテストで走らせられる 状態で持っとかないとならないってことなんだけど (ただ最適化前の状態を取っておくのではなく、最適化後にもコードは進化するので その後の変更を最適化前のコードにも統合してゆく必要がある)、 回避できるケースもある。

  • 納品したらおしまい、という仕事で、最終ステージでががっと最適化する場合。 コンソールゲームとか。イベント向けの一発仕事とか。
  • 仕様が明確で安定している場合。ランタイムオプションやコンパイルフラグで 常に最適化のあり/なしを切り替えられるようにしておける。 Gaucheでも-fno-inlineとか持ってるので、最適化オプションあり/なしでの 結果をダンプしといて比較することは自動化できそうだ。

でもやっぱり、最適化後のコードのパスを全部カバーするようなテストセットは 改めて作る必要があるよなあ。

Tag: Programming

Post a comment

Name: