自動化された一連の回帰テストを含む、完全なプログラムを検証するテスト(収束テストなど)を使用することの価値を強く確信しています。プログラミングの本を読んだ後、ユニットテスト(つまり、単一の関数の正確性を検証し、問題を解決するためにコード全体を実行するテストではない)を作成する必要があるというしつこい気持ちになりました。。ただし、単体テストは常に科学的コードに適合しているとは限らず、結果的に人為的または時間の無駄に感じます。
研究コードの単体テストを作成する必要がありますか?
自動化された一連の回帰テストを含む、完全なプログラムを検証するテスト(収束テストなど)を使用することの価値を強く確信しています。プログラミングの本を読んだ後、ユニットテスト(つまり、単一の関数の正確性を検証し、問題を解決するためにコード全体を実行するテストではない)を作成する必要があるというしつこい気持ちになりました。。ただし、単体テストは常に科学的コードに適合しているとは限らず、結果的に人為的または時間の無駄に感じます。
研究コードの単体テストを作成する必要がありますか?
回答:
長年、私は自分のコードの単体テストを書くのに十分な時間がなかったという誤解を受けていました。私がテストを書いたとき、それらは肥大化した、重いものであり、それが必要だとわかったときだけユニットテストを書くべきだと思うように促しました。
それから私はテスト駆動開発を使い始め、それが完全な啓示であることがわかりました。ユニットテストを書かない時間はないと確信しています。
私の経験では、テストを念頭に置いて開発することで、よりクリーンなインターフェース、より焦点を絞ったクラスとモジュール、そして一般的にテスト可能なコードがよりソリッドになります。
ユニットテストがないレガシーコードを操作し、手動で何かをテストする必要があるたびに、「このコードにすでにユニットテストがある場合、これは非常に高速になる」と考え続けます。結合テストの高いコードに単体テスト機能を追加しようとするたびに、「分離された方法で記述されていれば、これは非常に簡単になる」と考え続けます。
私がサポートする2つの実験ステーションの比較と対照。1つはしばらく前から存在し、多くのレガシーコードがありますが、もう1つは比較的新しいものです。
古いラボに機能を追加する場合、多くの場合、ラボに降りて、必要な機能の意味と、他の機能に影響を与えずにその機能を追加する方法を理解するために多くの時間を費やします。コードは単にオフラインテストを許可するように設定されていないため、ほとんどすべてをオンラインで開発する必要があります。オフラインで開発しようとした場合、合理的な数よりも多くのモックオブジェクトが作成されます。
新しいラボでは、通常、デスクでオフラインで開発し、すぐに必要なもののみをモックアウトし、その後ラボで短時間を費やして、拾わない残りの問題を解決することで、機能を追加できます-ライン。
わかりやすくするために、@ naught101が尋ねたので...
私はいくつかのアドホックデータ分析を使用して、実験制御およびデータ収集ソフトウェアに取り組んでいる傾向があるため、TDDと改訂制御の組み合わせは、基礎となる実験ハードウェアの変更と、長期にわたるデータ収集要件の変更の両方を文書化するのに役立ちます。
ただし、探索コードを開発する状況であっても、仮定を成文化することで大きな利点が得られ、それらの仮定が時間とともにどのように変化するかを見ることができます。
科学的なコードは、通常は問題の数学的構造のため、私が取り組んだビジネスコードよりも頻繁にインターロック機能の星座を持つ傾向があります。そのため、個々の機能の単体テストは非常に効果的ではないと思います。ただし、効果的な単体テストのクラスがあり、特定の機能を対象とするという点でプログラム全体のテストとはまったく異なると思います。
これらの種類のテストの意味を簡単に定義します。回帰テストでは、コードに変更が加えられたときに、既存の動作の変更(何らかの方法で検証済み)を探します。単体テストではコードを実行し、仕様に基づいて目的の出力が得られることを確認します。出力が有効であると判断しなければならなかったため、元の回帰テストは単体テストであったため、それらはそれほど違いはありません。
数値単体テストの私のお気に入りの例は、有限要素実装の収束率のテストです。確かに単純ではありませんが、PDEの既知の解決策を取り、メッシュサイズを小さくすることでいくつかの問題を実行し、エラーノルムを曲線に適合させます(は収束速度)。Pythonを使用してPETScのポアソン問題に対してこれを行います。回帰のように違いを探しているのではなく、特定の要素に指定された特にレート探しています。C h r r r
PyLithに由来するユニットテストのもう2つの例は、ポイント位置(合成結果を生成しやすい単一の関数)と、メッシュ内のゼロボリューム凝集セルの作成です。コード内の機能。
保存と一貫性のテストを含む、この種のテストは多数あります。操作は回帰とそれほど違いはありません(テストを実行して標準に対して出力をチェックします)が、標準出力は以前の実行ではなく仕様から取得されます。
コードコンプリート、第2版のテスト駆動開発について読んで以来、ユニットテストフレームワークを使用してきました開発戦略の一環として、作成したさまざまなテストは診断的であるため、デバッグに費やす時間を削減することで生産性が劇的に向上しました。副次的な利点として、私は自分の科学的結果にはるかに自信を持ち、自分の結果を守るために何度もユニットテストを使用しました。単体テストでエラーが発生した場合、通常はかなり迅速にその理由を突き止めることができます。アプリケーションがクラッシュし、すべてのユニットテストがパスした場合、コードカバレッジ分析を実行して、コードのどの部分が実行されていないかを確認し、デバッガーでコードをステップ実行してエラーの原因を特定します。次に、新しいテストを作成して、バグが修正されたままであることを確認します。
私が書いたテストの多くは、純粋な単体テストではありません。厳密に定義すると、単体テストは1つの機能の機能を実行することになっています。モックデータを使用して1つの関数を簡単にテストできる場合、それを行います。また、特定の関数の機能を実行するテストを作成するために必要なデータを簡単にモックできないため、統合テストでその関数を他の関数と一緒にテストします。統合テスト複数の関数の動作を一度にテストします。Mattが指摘しているように、科学コードは多くの場合、インターロック関数のコンステレーションですが、多くの場合、特定の関数が順番に呼び出され、中間テストで出力をテストするユニットテストを作成できます。たとえば、実稼働コードが5つの関数を順番に呼び出す場合、5つのテストを作成します。最初のテストでは、最初の関数のみが呼び出されます(したがって、単体テストです)。次に、2番目のテストは最初と2番目の関数を呼び出し、3番目のテストは最初の3つの関数を呼び出します。コード内のすべての機能について単体テストを作成できたとしても、プログラムのさまざまなモジュラー部分を組み合わせるとバグが発生する可能性があるため、とにかく統合テストを作成します。最後に、私が必要だと思うすべての単体テストと統合テストを書いた後、私は 結果を再現可能にしたいので、ケーススタディを単体テストでラップし、回帰テストに使用します。再現性がなく、異なる結果が得られる場合、その理由を知りたいです。回帰テストの失敗は実際の問題ではないかもしれませんが、新しい結果が少なくとも古い結果と同じくらい信頼できるかどうかを判断することを私に強制します。
また、単体テストに加えて、静的コード分析、メモリデバッガー、コンパイラの警告フラグを使用してコンパイルして、単純なエラーや未使用のコードをキャッチすることもできます。
私の経験では、科学研究コードの複雑さが増すにつれて、プログラミングにおいて非常にモジュール式のアプローチをとる必要があります。これは、大規模で古いベースのコード(f77
誰か?)にとっては痛いことですが、前進する必要があります。モジュールがコードの特定の側面を中心に構築されると(CFDアプリケーションの場合、境界条件または熱力学を考える)、ユニットテストは、新しい実装を検証し、問題とさらなるソフトウェア開発を分離するために非常に貴重です。
これらのユニットテストは、コード検証の 1レベル下(波動方程式の解析解を回復できますか?)およびコード検証の 2レベル下(乱流パイプフローの正しいピークRMS値を予測できます)で、プログラミングが保証されている必要があります(引数は正しく渡されていますか、ポインターは正しいものを指しているのですか?)および「数学」(このサブルーチンは摩擦係数を計算します。数値のセットを入力して解を手動で計算すると、ルーチンは同じになりますか結果?)は正しいです。基本的に、コンパイラーが発見できるレベル、つまり基本的な構文エラーの1つ上のレベルに進みます。
アプリケーションの少なくともいくつかの重要なモジュールには絶対にお勧めします。ただし、非常に面倒で時間がかかることを理解する必要があるため、無制限の人手がない限り、100%の複雑なコードにはお勧めしません。
科学コードの単体テストは、さまざまな理由で役立ちます。
特に3つは次のとおりです。
単体テストは、他の人がコードの制約を理解するのに役立ちます。基本的に、単体テストはドキュメントの形式です。
単体テストでは、コードの単一ユニットが正しい結果を返していることを確認し、詳細が変更されてもプログラムの動作が変わらないことを確認します。
単体テストを使用すると、研究コードを簡単にモジュール化できます。これは、コードの並列化やGPGPUマシンでの実行に関心がある場合など、新しいプラットフォームをターゲットにしようとする場合に特に重要です。
何よりも、単体テストは、コードを使用して作成している研究結果が有効で検証可能であるという自信を与えます。
質問で回帰テストについて言及していることに注意してください。多くの場合、リグレッションテストは、ユニットテストや統合テストの自動化された定期的な実行によって達成されます(統合されたコードの一部をテストします。信頼されている以前のプログラムの結果)。大規模で複雑なコンポーネントのレベルですでに統合テストまたは単体テストを使用しているようです。
私が言うことは、研究コードがますます複雑になり、他の人のコードとライブラリに依存するようになると、エラーが発生したときにどこでエラーが発生するかを理解することが重要だということです。単体テストを使用すると、エラーをより簡単に特定できます。
科学的コンピューティングのベストプラクティスで共著した論文のセクション7「間違いの計画」の説明、証拠、および参考文献が役立つことがあります。また、防衛的プログラミングの補足概念も紹介しています。
私のdeal.IIのクラスでは、私は正常に動作(と私は意図的「と述べストレスに進んでいないのテストを持っていないソフトウェアを教えるんではない」、正しく動作しない」ことが正しく動作しないが)。
もちろん、私はマントラを守っています-これが取引です。IIはすべてのコミットで2,500テストを実行するようになりました;-)
もっと真剣に言うと、Mattはすでに2つのクラスのテストを適切に定義していると思います。私たちは低レベルのものの単体テストを書いており、それは自然に高レベルのものの回帰テストに進んでいます。私たちのテストをどちらか一方に分ける明確な境界線を引くことができるとは思いません、確かに多くの人が出力を見て、それが大部分が合理的であると判断した行を踏みます(ユニットテスト?)精度の最後の部分を調べずに(回帰テスト?)。
はいといいえ。確かに、変換ルーチン、文字列マッピング、基本的な物理学、数学など、生活を楽にするために使用する基本的なツールセットの基本的なルーチンのユニットテストを行います。実際には、ユニットとしてではなく機能テストとしてテストすることをお勧めします。また、レベルと用途が大きく変化するクラスやエンティティ(最適化の目的など)、または何らかの理由で内部の詳細が変更されるクラスとエンティティを、ユニットテストとストレスをかけます。最も典型的な例は、ディスクからマップされた巨大なマトリックスをラップするクラスです。
絶対に!
何、それはあなたにとって十分ではありませんか?
科学プログラミングでは、他のどの種類よりも、物理システムを一致させることに基づいて開発しています。テスト以外で行ったことがあるかどうか、どのようにしてわかりますか?コーディングを開始する前に、コードの使用方法を決定し、いくつかの例を実行してみてください。可能性のあるエッジケースをキャッチしてみてください。モジュール方式で実行します。たとえば、ニューラルネットワークの場合、単一のニューロンのテストセットと完全なニューラルネットワークのテストセットを作成できます。そうすれば、コードの記述を開始するときに、ネットワークでの作業を開始する前にニューロンが機能することを確認できます。このような段階で作業するということは、問題が発生した場合、テストするコードの最新の「段階」しかなく、以前の段階はすでにテスト済みであることを意味します。
さらに、テストを取得した後、別の言語でコードを書き換える必要がある場合(たとえば、CUDAに変換する場合)、または更新するだけの場合でも、既にテストケースがあり、それらを使用して作成できますプログラムの両方のバージョンが同じように機能することを確認してください。
私はこの質問に独断的にではなく、実用的にアプローチします。「関数Xで何が問題になる可能性があるか」という質問を自問してください。コードにいくつかの典型的なバグを導入すると、出力がどうなるかを想像してください:誤ったプリファクター、間違ったインデックス、...そして、その種のバグを検出する可能性が高い単体テストを書きます。与えられた関数について、関数自体のコードを繰り返さずにそのようなテストを書く方法がない場合は、しないでください-しかし、次のより高いレベルのテストについて考えてください。
科学コードの単体テスト(または実際には任意のテスト)のはるかに重要な問題は、浮動小数点演算の不確実性にどのように対処するかです。私の知る限り、まだ一般的な解決策はありません。
Tangurenaには申し訳ありません。このあたりで、マントラは「テストされていないコードは壊れたコード」であり、それはボスから来たものです。単体テストを行うすべての正当な理由を繰り返すのではなく、いくつかの詳細を追加したいだけです。
化学ソルバー(複雑な地質領域用)の開発中に使用したわずかに異なるアプローチは、コピーアンドペーストスニペットによるユニットテストと呼ばれるものでした。
大規模な化学システムモデラーに組み込まれた元のコードのテストハーネスを構築することは、時間枠内では実現できませんでした。
しかし、さまざまな表現の単体テストとして、化学式の(ブーストスピリット)パーサーがどのように機能するかを示すスニペットのますます複雑なセットを作成することができました。
最後の最も複雑な単体テストは、システムで必要なコードに非常に近く、そのコードをモック可能に変更する必要はありませんでした。このようにして、ユニットテストされたコード全体にコピーすることができました。
これを単なる学習演習と真の回帰スイート以上のものにしているのは、2つの要因です。ユニットテストはメインソースに保持され、そのアプリケーションの他のテストの一部として実行されます。スピリットは2年後に変更されます)-コピーアンドペーストされたコードは実際のアプリケーションで最小限の変更しか行われていないため、ユニットテストを参照するコメントを追加して、同期を保つことができます。
より大きなコードベースの場合、高レベルのもののテスト(必ずしも単体テストではありません)が役立ちます。一部のより単純なアルゴリズムの単体テストは、ヘルパー関数がのsin
代わりに使用しているため、コードが意味のないことを確認するのにも役立ちますcos
。
しかし、全体的な研究コードについては、テストを記述して維持することは非常に困難です。アルゴリズムは意味のある中間結果がないと大きくなる傾向があり、明らかなテストが行われ、結果が出るまでに実行に時間がかかることがよくあります。もちろん、良い結果が得られた参照の実行に対してテストすることもできますが、これは単体テストの意味では良いテストではありません。
多くの場合、結果は真の解の近似値です。単純な関数がイプシロンまで正確であればテストできますが、結果のメッシュが正しいかどうかを確認するのは非常に困難です。
そのような場合、自動化されたテストのコストと利益の比率が高すぎることがよくあります。より良いものをお勧めします:テストプログラムを作成します。たとえば、エッジサイズのヒストグラムやメッシュの角度、最大および最小の三角形の面積、それらの比率など、結果に関するデータを作成するために中規模のPythonスクリプトを作成しました。
私は、通常動作時の入力と出力メッシュを評価するためにそれを使用することができ、両方と私はアルゴリズムを変更した後、健全性チェックを持っているためにそれを使用します。アルゴリズムを変更しても、新しい結果がより良いかどうかは必ずしもわかりません。なぜなら、どの近似が最良であるかという絶対的な尺度がないことが多いからです。しかし、そのようなメトリックを生成することにより、「新しいバリアントは最終的には角度比は向上しますが、収束率は低下します」などの優れた要因をいくつか知ることができます。