Ryuz's tech blog

FPGAなどの技術ブログ

テストと検証の違いについて考えてみた

はじめに

今回は定期的にやってくる駄文というかポエム回です。

ソフトウェア開発において「テスト(Test)」と言うものは非常に重要です。一方でLSI開発などを含めて、RTL界隈では「検証(Verification)」という言葉をよく聞きます。

業種や人によっても用語の定義が微妙に違ったりもするので一概に定義できない部分もある気がしますが、今日は 主観100% となりますが、私なりの考えでこの二つを考えてみようと思います。 このあたりは一家言ある人が多い気がしているのでマサカリ飛んで来そうで怖いんですが、まあこんな考え方もあるよというレベルで私見として書いておきます。

私の理解として

  • テストはバグがあればそれを証明する試み
  • 検証はバグが無いことを保証しようとする試み

という理解をしております。

もちろん検証が完了したからバグがゼロ個であることを保証できるわけではないのですが、目的として目指しているものが異なるという点は重要かと思います。

そしてこれは、ソフトウェアだからとかハードウェアだからという問題ではないと考えています。実際ソフトウェアでも検証は必要ですし、CPU/GPU/FPGA向けのソフトウェアで低レイヤーの部分では検証が必要になってくるケースは多い気がしています。

機能の確認と、機能の保証の違い

例えば python二次方程式を計算する関数を書いてテストして意図した値が得られたとしましょう。

def func(x):
  return 3 * x**2 + 2 * x + 1

この関数はおそらく下記のようなことは起こりません

  • 何度も実行したら3回目で違う結果が返ってきた
  • Python と一緒に Excel を起動したら計算結果が変わった
  • CPUコア数の違うパソコンで実行したら結果が変わった
  • Windowsでは合うが、Linux では結果が変わった
  • その日の天気で結果が変わった

そのため、Excel と一緒に起動するテストや、晴れの日と雨の人雪の日でそれぞれテストするようなテスト項目を作ったりはしません。

一方で、OSを開発したりベアメタルで開発するなどの低レイヤーの開発の場合以下のようなバグはしばし発生します。

  • ある命令の区間で割り込みが来た場合に限り変数が壊れる
  • たまたまあるスレッドが先に終了したときだけ変数が壊れる
  • マルチコアへのスレッドの割り当て順が特定のケースで変数が壊れる
  • コンパイルオプションで最適化を指定すると結果が変わる
  • メモリバリアを書き忘れるなどして実行順序依存でキャッシュの状態次第で変数が壊れる

何が言いたいかと言うと、先の Python のコードが Python のコードに閉じて正しい機能かどうかだけをテストすれば済んでいるのは、プロセッサがメモリコヒーレンシを保証し、バグのないスピンロックの機構や、メモリ保護や、復帰可能な割り込みや、温度保証などの物理的な保証もしており、OS開発者がコンテキストの継続性を保証できるスレッドを用意し、安全なメモリ割り当てを行い、言語開発者がバグのないインタプリタコンパイラを提供しているから成り立っているわけです。

ものすごく乱暴な言い方をすると、先の Python コードが正しいかを確認するのが「テスト」で、それを保証する環境をのものを担保しているのが「検証」ではないかと思う次第です。

まさに「プログラマをダメにするハードウェア」の上に成り立っているのがテストなんじゃないかとさえ思ってしまうわけです。

テストと検証のやり方の違い

テストの場合いろいろな考え方があって

  • 境界値条件
  • 実行網羅/分岐網羅/条件網羅
  • ディシジョンテーブル
  • 原因結果グラフ

などなど教科書的な話に始まり、如何に効率的に目的の機能を満たすべく目の前のコードのバグを出していくかと言う点に心血が注がれます。

一方で、検証となると、起こりうるすべての事象で同じ機能が満たされること を確認する為、常に 状態の組み合わせ爆発 と戦う事になります。

そもそもとして起こりうる状態をちゃんと、漏れなくリストアップしてすべての組み合わせを把握できているか、もとても重要になります。

よくある「コーナーケースでバグが出た」なんて言うのは結局、状態組み合わせを見通せてなかったか、工数が無くて検査しなかったかのどちらかです。

そういった場合に案外強力なのが

などの考えになってくる気がします。

テストの場合、「境界値をちゃんと考えないランダムなテスト」と言うのは、質の悪いテストと思われがちですが、こと検証の場合は、検証予定の組み合わせパターンの100%網羅が目的ですので、「少ない工数で効率よく100%までもっていく」点でランダムパターンは有効なケースがしばしあります。

ランダムと言っても取りうる状態に条件があるので、複雑な条件を持った複数のオブジェクトの状態組み合わせとなると、多次元空間の探索ですので、それこそモンテカルロ法で使われるギブスサンプリングみたいなものが必要になってくるのではないかなどと思ってしまうわけです。

で、仕様が許す範囲での最大限の多様性を持った外乱を与えつつ、内部設計仕様を満たしていることをプロパティ検証などで見ていくのはなかなか有効な手段であり、いわゆる「ソフトウェアテスト技法」とも少し違ったものを思う次第です。

また、これらとは全く違うアプローチで、フォーマル検証も重要なのだと思います。

こちらも正しく条件を定義するところまでは同じですが、それが組み合わせ爆発の中ですべて満たされるかどうか、数学的に証明しようという試みなので適用可能なシーンでは非常に強力な武器になりそうな気はします。生憎なかなか簡単に使えるツールでもないので経験は無いのですが、興味深く思います。

テストと検証で似ているところ

ブラックボックス(外部仕様)/ホワイトボックス(内部設計)での区分けとか、

みたいに、階層化して問題に対処していくあたりは両者似ているなと思ってみたりもします。

TDD のように、先にテストや検証を定義してから、実装設計を進めるのも両者ともに有効だと思います。

境界はあるのか

ちなみに実務的にはテストと検証の境界は案外曖昧な気もしています。

ソフトウェアテストであっても、組み合わせ全網羅みたいなことをすることはありますし、検証であっても全部扱えないので検証項目を何らかのルールで絞ることも多いです。

感覚的に、低レイヤーは検証寄りの考えが重要になりがちで、上位レイヤーほどテスト技法が有効に機能する気がしています。

また、全部テスト/検証しきれないときに費用対効果を考えて、項目を落としていくビジネス観点でのセンスが必要なこともあるでしょう。

なかなかこれはテストをしているのか検証をしているのか、混ざっているのか、曖昧なケースは多い印象はあります。

研究開発などのプロトタイピングにおける検証

私は長らくそういうポジションでの開発が多いのですが、プロトタイピングで重要なのは 脳内設計だけで一発でバグのないコードを書く ところに達するまでひたすら修行することじゃないかと思っていたりもします。

この過程で重要なのは、「仮説を立てた事象が正しいかどうかの検証」だけなので、それができればどうだっていいわけです。

が、まあ、「しばし DMA転送が時々途中で固まって、実験が止まる」なんてことはよくあるわけで、そこで検証の出番となるわけです。

  • 疑うところを間違わない(手を抜いたところ、実績が無いところ)
  • 疑わしいところに絞って検証してバグ落としする
  • 場合によっては姑息な手法で暫定回避する

などにもセンスが問われます。

しばし試作システムでバグのモグラ叩きをはじめてしまい、全く研究が進まない人が居ますが、研究とかプロト開発のフェーズでは、それらはどれだけ頑張っても何一つ成果として認められないので、まあ、適当にさっさと何とかする 必要があったりします。

そして、「適当にさっさと何とかする」には「ちゃんとしたテスト技法や検証論の知識」がとても重要だったりするなと思う次第です。