web-dev-qa-db-ja.com

結果の予測が難しいコードの単体テストをどのように記述しますか?

関数の正確な結果を事前に予測することが難しい、非常に数値的/数学的なプログラムを頻繁に使用しています。

この種のコードでTDDを適用しようとするとき、期待される結果を見つける唯一の方法はアルゴリズム自体を適用することです(私の頭、紙の上、またはコンピュータで)。これは間違っているように感じます。私はテスト中のコードを効果的に使用してユニットテストを検証しているのです。

テスト対象のコードの結果を予測することが難しい場合に、単体テストを記述してTDDを適用する既知の手法はありますか?

結果を予測するのが難しいコードの(実際の)サンプル:

関数weightedTasksOnTime、1日あたりの作業量workPerDay、範囲(0、24]、現在の時間initialTime> 0、およびタスクのリストtaskArray、それぞれに完了までの時間time> 0、期日due 、および重要度の値importance;は、範囲[0、1]の正規化された値を返します。各タスクがdueで指定された順序で完了し、taskArrayから開始された場合、initialTimeの日付より前に完了することができるタスクの重要度を表します。

この関数を実装するアルゴリズムは比較的単純です。taskArrayのタスクを繰り返します。タスクごとに、timeinitialTimeに追加します。新しい時間が<dueの場合、アキュムレータにimportanceを追加します。時間は、workPerDayの逆数によって調整されます。アキュムレータを返す前に、正規化するタスクの重要度の合計で割ります。

function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time * (24 / workPerDay)
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator / totalImportance(taskArray)
}

上記の問題は、workPerDayと正規化要件を削除することにより、コアを維持しながら、次のように簡略化できると考えています。

function weightedTasksOnTime(initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator
}

この質問は、テスト中のコードが既存のアルゴリズムの再実装ではない状況に対処します。コードが再実装されている場合、アルゴリズムの既存の信頼できる実装は自然なテストOracleとして機能するため、コードは本質的に結果を簡単に予測できます。

127
PaintingInAir

テストが難しいコードでテストできることが2つあります。まず、退化したケース。タスク配列に要素がない場合、または1つまたは2つだけであるが、1つが期限を過ぎている場合などに発生します。実際の問題よりも簡単ですが、手動で計算するのが妥当です。

2つ目は、健全性チェックです。これらは、答えが正しいであるかどうかわからない場合に行うチェックですが、間違ったであるかどうかは確実にわかります。これらは、時間を進める必要がある、値が合理的な範囲内にある、パーセンテージを合計して100にする必要がある、などのようなものです。

はい、これは完全なテストほど良いものではありませんが、完全性アルゴリズムの問​​題を明らかにする、サニティチェックと退化ケースをめちゃくちゃにする頻度に驚くでしょう。

251
Karl Bielefeldt

私は以前、予測が困難な出力を持つ科学ソフトウェアのテストを作成していました。 Metamorphic Relationsをたくさん利用しました。基本的に、正確な数値出力を知らなくても、ソフトウェアの動作について知っていることがいくつかあります。

考えられるケースの例:毎日実行できる作業量を減らした場合、実行できる作業量の合計はせいぜい同じままですが、おそらく減少します。したがって、workPerDayのいくつかの値に対して関数を実行し、関係が成立することを確認します。

81

他の回答には、エッジまたはエラーのケースのテストを開発するための良いアイデアがあります。他の人にとっては、アルゴリズム自体を使用することは(明らかに)理想的ではありませんが、それでも有用です。

アルゴリズム(または依存するデータ)が変更されたかどうかを検出します

変更が偶然の場合は、コミットをロールバックできます。変更が意図的なものであった場合は、単体テストに再度アクセスする必要があります。

39
user949300

他の種類のコードの単体テストを作成するのと同じ方法:

  1. いくつかの代表的なテストケースを見つけ、それらをテストします。
  2. Edgeケースを見つけてテストします。
  3. エラー状態を見つけ、それらをテストします。

コードにランダムな要素が含まれていたり、コードが確定的でない(つまり、同じ入力を与えられても同じ出力が生成されない)場合を除き、ユニットテストが可能です。

副作用、または外力の影響を受ける機能を避けます。純粋な関数はテストが簡単です。

21
Robert Harvey

投稿されたコメントによる更新

元の答えは簡潔にするために削除されました-編集履歴で見つけることができます。

PaintingInAirコンテキスト:起業家および学問として、私が設計するアルゴリズムのほとんどは、自分以外の誰からも要求されていません。質問で与えられた例は、タスクの順序付けの質を最大化するための、派生物のないオプティマイザの一部です。内部でサンプル関数の必要性をどのように説明したかに関して、「時間どおりに完了するタスクの重要性を最大化するための目的関数が必要です」。ただし、このリクエストと単体テストの実装の間には、まだ大きなギャップがあるようです。

まず、TL; DRを使用して、他の方法では長い回答を回避します。

このように考える:
顧客がマクドナルドに入り、レタス、トマト、石鹸をトッピングにしたハンバーガーを要求します。この注文は料理人に与えられ、料理人は要求どおりにハンバーガーを作ります。顧客はこのハンバーガーを受け取り、それを食べてから、これは美味しいハンバーガーではないと料理人に不平を言います。

これは料理人のせいではありません-彼は顧客が明示的に要求したことだけを行っています。 リクエストされた注文が実際においしいかどうかを確認するのは料理人の仕事ではありません。料理人は、顧客が注文したものを作成するだけです。 おいしいものを注文するのはお客様の責任です

同様に、アルゴリズムの正確さを問うのは開発者の仕事ではありません。彼らの唯一の仕事は、要求に応じてアルゴリズムを実装することです。
単体テストは開発者のツールです。ハンバーガーが注文に一致することを確認します(キッチンを出る前に)。注文したハンバーガーが実際においしいことを確認しようとはしません(すべきではありません)。

次の場合でもあなたは顧客であり料理人でもありますが、以下の間に意味のある違いがあります。

  • 私はこの食事を適切に準備しなかった、それはおいしかった(=調理エラー)。焼けたステーキは、たとえステーキが好きでも、決して美味しくはならないでしょう。
  • 私は食事を適切に準備しましたが、それは好きではありません(=カスタマーエラー)。ステーキが嫌いなら、完璧に調理したとしても、ステーキを食べるのは好きではありません。

ここでの主な問題は、顧客と開発者(およびアナリスト-その役割は開発者も表すことができます)を分離していないことです。

コードのテストとビジネス要件のテストを区別する必要があります。

たとえば、顧客はそれが[this]のように機能することを望んでいます。ただし、開発者は誤解し、[that]を実行するコードを記述します。

したがって、開発者は、[that]が期待どおりに機能するかどうかをテストする単体テストを作成します。彼がアプリケーションを正しく開発した場合、アプリケーションが実行しなくてもユニットテストに合格します[this]、顧客は期待していた。

顧客の期待(ビジネス要件)をテストする場合は、別の(そしてそれ以降の)ステップで行う必要があります。

これらのテストを実行するタイミングを示す簡単な開発ワークフロー:

  • 顧客が解決したい問題を説明します。
  • アナリスト(または開発者)はこれを分析に書き留めます。
  • 開発者は、分析が説明することを行うコードを記述します。
  • 開発者コードをテスト(単体テスト)して、分析が正しく行われたかどうかを確認します
  • 単体テストが失敗した場合、開発者は開発に戻ります。ユニットテストがすべて成功するまで、これは無期限にループします。
  • テストされた(確認されて渡された)コードベースが作成されたので、開発者はアプリケーションをビルドします。
  • アプリケーションは顧客に渡されます。
  • 顧客は与えられたアプリケーションが実際に解決しようとしていた問題を解決するかどうかをテストします(QAテスト)

顧客と開発者が同じである場合、2つの別々のテストを実行することのポイントが何であるか疑問に思うかもしれません。開発者から顧客への「引き継ぎ」がないため、テストは次々に実行されますが、それらはまだ個別のステップです。

  • 単体テストは、開発段階が終了したかどうかを確認するのに役立つ専用ツールです。
  • QAテストは、アプリケーションを使用して実行されます

アルゴリズム自体が正しいかどうかをテストする場合は、開発者の仕事の一部ではありません。それが顧客の懸念であり、顧客はこれをsingアプリケーションでテストします。

起業家や学問として、ここでは重要な違いを見逃しているかもしれません。これは、さまざまな責任を強調しています。

  • アプリケーションが顧客が最初に要求したものに準拠していない場合、その後のコードの変更は通常行われます無料;開発者のエラーだからです。開発者はミスを犯し、それを修正するコストを支払わなければなりません。
  • アプリケーションが顧客が最初に要求したことを実行するが、顧客が今気が変わった場合(たとえば、別のより良いアルゴリズムを使用することを決定した場合)、コードベースへの変更は顧客に請求されますなぜなら、顧客が今求めているものとは異なるものを求めたのは開発者の責任ではないからです。考えを変えるのは顧客の責任(コスト)であり、したがって、以前は合意されていなかったものを開発するために開発者にもっと努力を払わせます。
17
Flater

プロパティテスト

数学関数は、従来の例に基づく単体テストよりも「プロパティテスト」の方が適している場合があります。たとえば、整数の「乗算」関数のようなものの単体テストを書いているとしましょう。関数自体は非常に単純に見えるかもしれませんが、それが乗算する唯一の方法である場合、関数自体のロジックなしでどのように徹底的にテストするのですか?予想される入力/出力で巨大なテーブルを使用することもできますが、これには制限があり、エラーが発生しやすくなります。

このような場合、特定の期待される結果を探す代わりに、関数の既知のプロパティをテストできます。乗算の場合、負の数と正の数を乗算すると負の数になり、2つの負の数を乗算すると正の数になるはずです。テスト値は、そのような関数をテストするための良い方法です。通常、複数のプロパティをテストする必要がありますが、すべてのケースで期待される結果を必ずしも知らなくても、関数の正しい動作を一緒に検証するプロパティの有限セットを識別することができます。

私が見たプロパティテストの最適な紹介の1つは、F#の this one です。うまくいけば、構文はテクニックの説明を理解するための障害にはなりません。

9

コードを書き、結果が「正しく見える」かどうかを確認するのは魅力的ですが、当然のことながら、それは良い考えではありません。

アルゴリズムが難しい場合、結果の手動計算を簡単にするためにいくつかのことができます。

  1. Excelを使用します。計算の一部またはすべてを実行するスプレッドシートを設定します。手順を確認できるように、十分にシンプルにしてください。

  2. メソッドをより小さなテスト可能なメソッドに分割し、それぞれに独自のテストを行います。小さいパーツが機能することが確実な場合は、それらを使用して手動で次のステップに進みます。

  3. 集計プロパティを使用してサニティチェックを行います。たとえば、確率計算機があるとします。個々の結果がどうあるべきかはわからないかもしれませんが、それらすべてを合計すると100%になるはずです。

  4. 強引な。考えられるすべての結果を生成するプログラムを作成し、アルゴリズムが生成するものよりも優れているものがないことを確認します。

4
Ewan

TL; DR

他の回答にはないアドバイスについては、「比較テスト」セクションに進んでください。


始まり

アルゴリズムで拒否する必要があるケース(ゼロまたは負のworkPerDayなど)と簡単なケース(空のtasks配列など)をテストすることから始めます。

その後、最初に最も単純なケースをテストします。 tasks入力の場合、さまざまな長さをテストする必要があります。 0、1、2要素をテストすることで十分です(2はこのテストの「多く」のカテゴリに属します)。

精神的に計算できる入力を見つけることができれば、それは良い出発点です。私が時々使用する手法は、望ましい結果から始めて、(仕様で)その結果を生成するはずの入力に戻ることです。

比較テスト

入力と出力の関係が明確でない場合がありますが、1つの入力が変更されたときに異なる出力間の予測可能な関係があります。例を正しく理解していれば、(他の入力を変更せずに)タスクを追加しても、時間どおりに行われる作業の割合が増えることはないので、関数を2回呼び出すテストを作成できます。 -そして、2つの結果の不平等を主張します。

フォールバック

ときどき、仕様に対応する手順で手動で計算した結果を示す長いコメントに頼らなければなりませんでした(このようなコメントは通常、テストケースよりも長くなります)。最悪のケースは、異なる言語または異なる環境で以前の実装との互換性を維持する必要がある場合です。テストデータに/* derived from v2.6 implementation on ARM system */などのラベルを付ける必要がある場合があります。それは非常に満足できるものではありませんが、移植時の忠実度テストとして、または短期間の松葉杖としては許容できるかもしれません。

リマインダー

テストの最も重要な属性はその可読性です。入力と出力がリーダーに対して不透明な場合、テストの値は非常に低くなりますが、リーダーがそれらの関係を理解するのに役立つ場合、テストには2つの目的があります。

不正確な結果(浮動小数点など)には、適切な「ほぼ等しい」を使用することを忘れないでください。

過剰テストを避けてください-他のテストでは到達できない何か(境界値など)をカバーする場合にのみテストを追加してください。

2
Toby Speight

この種のテストが難しい機能については、特別なことは何もありません。外部インターフェースを使用するコードにも同じことが当てはまります(たとえば、REST制御できないサードパーティアプリケーションのAPIであり、テストスイートによるテストが確実に行われていないか、または戻り値の正確なバイト形式が不明なサードパーティライブラリ)。

いくつかの健全な入力に対してアルゴリズムを実行し、それが何をするかを確認し、結果が正しいことを確認し、入力と結果をテストケースとしてカプセル化することは、非常に有効なアプローチです。これをいくつかのケースで実行して、いくつかのサンプルを取得できます。入力パラメーターをできるだけ異なるものにしてください。外部API呼び出しの場合、実際のシステムに対して数回の呼び出しを行い、いくつかのツールでそれらをトレースしてから、ユニットテストにモックして、プログラムがどのように反応するかを確認します。これは、いくつかを選択するのと同じです。タスク計画コードの実行、手動での検証、テスト結果のハードコーディング。

次に、(例では)空のタスクリストのようなEdgeケースを持ち込みます。そういうもの。

テストスイートは、結果を簡単に予測できる方法ほど優れていない場合があります。ただし、テストスイートなし(または単に煙テスト)よりも100%優れています。

ただし、問題が決定結果isであるかどうかがわかりにくい場合は、まったく別の問題です。たとえば、任意の数が素数かどうかを検出するメソッドがあるとします。乱数をほとんど投げることができず、結果が正しいかどうかを「見る」ことができます(頭や紙の上で素数を決めることができない場合)。この場合、実際にできることはほとんどありません-既知の結果(つまり、いくつかの大きな素数)を取得するか、別のアルゴリズムで機能を実装する必要があります(たぶん別のチームでさえ-NASAは気に入っているようです)それと、どちらかの実装にバグがある場合でも、少なくともそのバグが同じ間違った結果につながらないことを願っています。

これが通常のケースである場合は、要件エンジニアとよく話し合う必要があります。彼らがあなたのためにチェックするのが簡単な(または可能な限り)方法であなたの要件を定式化できない場合、いつあなたはあなたが終わったかどうか知っていますか?

2
AnoE

他の回答も良いので、これまでにまとめて見逃してきたいくつかの点にぶつかってみます。

Synthetic Aperture Radar(SAR)を使用して画像処理を行うソフトウェアを作成(および徹底的にテスト)しました。それは本質的に科学的/数値的です(幾何学、物理学、および数学がたくさん含まれています)。

いくつかのヒント(一般的な科学的/数値的テスト用):

1)逆数を使用します。 _[1,2,3,4,5]_のfftは何ですか?わからないifft(fft([1,2,3,4,5]))とは何ですか? _[1,2,3,4,5]_でなければなりません(またはそれに近い場合、浮動小数点エラーが発生する可能性があります)。 2Dの場合も同様です。

2)既知のアサートを使用します。行列式関数を記述する場合、100x100のランダムな行列の行列式が何であるかを言うのは難しい場合があります。ただし、単位行列が100x100であっても、行列式の行列式が1であることは知っています。また、関数は非可逆行列(0でいっぱいの100x100など)で0を返す必要があることも知っています。

exact assertsの代わりにラフアサートを使用します。画像間のマッピングを作成するタイポイントを生成して2つの画像を登録する上記のSAR処理用のコードを書きました。それらを一致させるためにそれらの間でワープを行います。サブピクセルレベルで登録できます。先験的に、2つの画像の登録がどのように見えるかについてanythingと言うのは難しいです。どのようにテストできますか?のようなもの:

_EXPECT_TRUE(register(img1, img2).size() < min(img1.size(), img2.size()))
_

重なっている部分にしか登録できないため、登録された画像mustは、最小の画像と同じか、それより小さく、さらに次のようになります。

_scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)
_

それ自体に登録された画像はそれ自体に近いはずですが、手元のアルゴリズムにより浮動小数点エラーより少し多く発生する可能性があるため、各ピクセルがピクセルが取り得る範囲の+/- 5%内にあることを確認してください(0-255はグレースケールで、画像処理で一般的です)。結果は少なくとも入力と同じサイズでなければなりません。

あなただけのテストを喫煙することもできます(つまり、それを呼び出して、クラッシュしないことを確認してください)。一般に、この手法は、テストを実行する前に最終結果を(簡単に)事前に計算できない大規模なテストに適しています。

4)使用OR RNGの乱数シードを保存します。

Runs doは再現可能である必要があります。ただし、再現可能な実行を取得する唯一の方法は、乱数ジェネレーターに特定のシードを提供することです。ランダム性テストが役立つ場合があります。ランダムに生成された縮退したケースで発生する科学的コードのバグを見たり聞いたりしました(複雑なアルゴリズムでは、縮退したケースが何かわかりにくい場合がありますeven is)。常に同じシードで関数を呼び出すのではなく、ランダムシードを生成し、そのシードを使用して、シードの値をログに記録します。この方法では、実行ごとにランダムなシードが異なりますが、クラッシュが発生した場合は、ログに記録したシードを使用して結果を再実行できます。私は実際にこれを実際に使用し、バグをつぶしたので、私はそれについて言及したいと思いました。確かにこれは一度だけ起こったことであり、私はそれを行う価値がないと確信しているので、慎重にこのテクニックを使用してください。ただし、同じシードのランダムは常に安全です。 マイナス面(常に同じシードを常に使用するのではなく):テストの実行をログに記録する必要があります。アップサイド:正確さとバグの増加。

あなたの特定のケース

1)空であることをテストtaskArrayを返します(既知のアサート)

2)次のようなランダム入力を生成 _task.time > 0_、_task.due > 0_、and _task.importance > 0_ for alltasks、そして結果がより大きいことをアサート _0_ (ラフアサート、ランダム入力)。狂ったようにランダムなシードを生成する必要はありません。アルゴリズムはそれを保証するほど複雑ではありません。それが報われる可能性は約0です。テストを単純にしてください。

3)テストの場合 _task.importance == 0_ すべての場合tasks、その後の結果は _0_ (既知のアサート)

4)他の回答がこれに触れましたが、あなたのparticularケースにとって重要かもしれません:APIをチーム外のユーザーが使用するように作成している場合は、テストする必要があります縮退した場合。たとえば、_workPerDay == 0_の場合、無効な入力であることをユーザーに知らせる素敵なエラーをスローするようにしてください。 APIを作成しておらず、それがあなたとあなたのチームのためだけの場合は、おそらくこのステップをスキップして、退化したケースでの呼び出しを拒否することができます。

HTH。

2

アルゴリズムのプロパティベースのテストのために、アサーションテストを単体テストスイートに組み込みます。特定の出力をチェックする単体テストの作成に加えて、メインコードでアサーションエラーをトリガーして失敗するように設計されたテストを作成します。

多くのアルゴリズムは、アルゴリズムのステージ全体で特定のプロパティを維持することの正当性の証明に依存しています。関数の出力を見てこれらのプロパティを慎重に確認できる場合は、単体テストだけでプロパティをテストできます。それ以外の場合、アサーションベースのテストでは、アルゴリズムが想定するたびに実装がプロパティを維持することをテストできます。

アサーションベースのテストは、アルゴリズムの欠陥、コーディングのバグ、および数値の不安定性などの問題による実装の失敗を明らかにします。多くの言語には、コンパイル時またはコードが解釈される前にアサーションを取り除くメカニズムがあり、本番モードで実行したときにアサーションによってパフォーマンスが低下することはありません。コードが単体テストに合格したものの、実際のケースでは失敗した場合は、デバッグツールとしてアサーションをオンに戻すことができます。

1
Tobias Hagge

ここでの他の回答のいくつかは非常に良いです:

  • ベース、エッジ、コーナーケースのテスト
  • 健全性チェックを実行する
  • 比較テストを実行する

...私は他のいくつかの戦術を追加します:

  • 問題を分解します。
  • アルゴリズムをコード外で証明します。
  • [外部で証明された]アルゴリズムが設計どおりに実装されていることをテストします。

分解を使用すると、アルゴリズムのコンポーネントが期待どおりに動作することを確認できます。また、「適切な」分解により、それらが適切に接着されていることも確認できます。 great分解は、アルゴリズムを一般化および簡略化して、できる範囲で (簡略化された一般的なアルゴリズムの)結果を手作業で十分に予測し、完全なテストを記述します。

その程度まで分解できない場合は、自分や同僚、利害関係者、顧客を満足させるのに十分な方法で、コードの外側でアルゴリズムを証明してください。そして、実装が設計と一致することを証明するのに十分なだけ分解します。

1
svidgen

これは理想的な答えのように思えるかもしれませんが、さまざまな種類のテストを識別するのに役立ちます。

厳密な答えが実装にとって重要である場合、例と予想される答えは、アルゴリズムを記述する要件に実際に提供する必要があります。これらの要件はグループで検討する必要があり、同じ結果が得られない場合は、理由を特定する必要があります。

アナリストとインプリメンターの役割を果たしている場合でも、実際に要件を作成し、単体テストを作成するずっと前にそれらをレビューしてもらう必要があります。この場合、予想される結果がわかり、それに応じてテストを作成できます。

一方、これが実装している部分であり、ビジネスロジックの一部ではないか、ビジネスロジックの回答をサポートしている場合は、テストを実行して結果を確認し、期待どおりにテストを変更する必要があります。それらの結果。最終結果は要件に対してすでにチェックされているので、それらが正しい場合、それらの最終結果を供給するすべてのコードは数値的に正確である必要があり、その時点でユニットテストは、エッジ障害のケースと将来のリファクタリングの変更を検出するためのものであり、特定のアルゴリズムは正しい結果を生成します。

0
Bill K

プロセスに従うことは時々完全に許容できると思います:

  • テストケースを設計する
  • ソフトウェアを使用して答えを得る
  • 手で答えを確認してください
  • ソフトウェアの将来のバージョンがこの答えを提供し続けるように、回帰テストを作成します。

これは、回答の正しさを手動で確認することが第一原理から手動で回答を計算するよりも簡単な状況では、合理的なアプローチです。

印刷されたページをレンダリングするソフトウェアを作成し、印刷されたページに正確に正しいピクセルが設定されていることを確認するテストを行っている人を知っています。これを行うための唯一の健全な方法は、ページをレンダリングするコードを記述し、見栄えがよいことを目で確認し、その後のリリースの回帰テストとして結果を取り込むことです。

特定の方法論が最初にテストケースを書くことを奨励しているという本を読んだからといって、常にそうする必要があるわけではありません。ルールは破られるべきです。

0
Michael Kay

他の回答回答には、テストされた関数の外部でspecificの結果を判別できない場合のテストの外観に関するテクニックがすでにあります。

私が他の答えで見つけていない追加のことは、何らかの方法でテストを自動生成することです:

  1. 「ランダム」入力
  2. データの範囲にわたる反復
  3. 境界のセットからのテストケースの構築
  4. 上記のすべて。

たとえば、関数がそれぞれ許容入力範囲[-1,1]の3つのパラメーターを取る場合、各パラメーターのすべての組み合わせをテストします{-2、-1.01、-1、-0.99、-0.5、-0.01、0,0.01 、0.5,0.99,1,1.01,2、(-1,1)}でさらにランダム

要するに:ときどき質の悪いものは、数量によって助成されることがあります。

0
Keith