web-dev-qa-db-ja.com

科学的ソフトウェアの継続的統合

私はソフトウェアエンジニアではありません。私は地球科学の分野で博士号を取得しています。

ほぼ2年前、私は科学的なソフトウェアのプログラミングを始めました。継続的インテグレーション(CI)を使用したことはありませんでした。これは、最初はそれが存在することを知らなかったため、このソフトウェアに取り組んでいたのは私だけだったからです。

現在、ソフトウェアのベースが実行されているため、他の人々がそれに興味を持ち始め、ソフトウェアに貢献したいと考えています。計画は、他の大学の他の人がコアソフトウェアに追加機能を実装していることです。 (私は彼らがバグをもたらす可能性があると怖いです)。さらに、ソフトウェアは非常に複雑になり、テストがますます難しくなりました。また、私はそれについて作業を続ける予定です。

この2つの理由から、CIの使用を検討することが増えています。私はソフトウェアエンジニアの教育を受けたことはなく、周囲の誰もCIについて聞いたことがないので(私たちは科学者であり、プログラマーではありません)、プロジェクトを始めるのは難しいと思います。

いくつかアドバイスが必要な質問がいくつかあります。

まず、ソフトウェアがどのように機能するかについての簡単な説明:

  • ソフトウェアは、必要なすべての設定を含む1つの.xmlファイルによって制御されます。入力引数として.xmlファイルへのパスを渡すだけでソフトウェアを起動し、実行して結果を含むいくつかのファイルを作成します。 1回の実行に30秒ほどかかる場合があります。

  • それは科学的なソフトウェアです。ほとんどすべての関数には複数の入力パラメーターがあり、その型はほとんどが非常に複雑なクラスです。これらのクラスのインスタンスを作成するために使用される大きなカタログを含む複数の.txtファイルがあります。

今私の質問に来ましょう:

  1. 単体テスト、統合テスト、エンドツーエンドテスト?:私のソフトウェアは現在、約30.000行のコードであり、数百の関数と約80のクラスがあります。すでに実装されている何百もの関数の単体テストを書き始めるのは、ちょっと奇妙に感じます。だから私は単にいくつかのテストケースを作成することを考えました。 10〜20個の異なる.xmlファイルを準備し、ソフトウェアを実行します。これはエンドツーエンドテストと呼ばれるものだと思いますか?私はよくこれを行うべきではないことをよく読んでいますが、もしあなたがすでに動作しているソフトウェアを持っているなら、それは最初から大丈夫でしょうか?または、すでに動作しているソフトウェアにCIを追加しようとするのは、単にばかげた考えです。

  2. 関数のパラメーターを作成するのが難しい場合、どのようにユニットテストを記述しますか?関数があると想定しますdouble fun(vector<Class_A> a, vector<Class_B>)。通常、最初に複数のテキストファイルを読み込んでオブジェクトを作成する必要があります。 _Class_A_および_Class_B_と入力します。テキストファイルを読み込まずに、Class_A create_dummy_object()のようないくつかのダミー関数を作成することを考えました。なんらかの serialization の実装についても考えました。 (複数のテキストファイルにのみ依存しているため、クラスオブジェクトの作成をテストする予定はありません)

  3. 結果が大きく変動する場合のテストの記述方法私のソフトウェアは大きなモンテカルロシミュレーションを利用し、繰り返し動作します。通常、反復は1000回までで、反復ごとに、モンテカルロシミュレーションに基づいてオブジェクトのインスタンスを500〜20.000個作成しています。 1回の反復の1つの結果だけが少し異なる場合、今後の反復全体が完全に異なります。この状況にどう対処しますか?最終結果は非常に変動しやすいので、これはエンドツーエンドのテストに対して大きなポイントだと思いますか?

CIに関するその他のアドバイスは高く評価されます。

22
user7431005

科学的ソフトウェアのテストは、複雑な主題と典型的な科学的開発プロセスの両方のために困難です(機能するまでハックしますが、通常はテスト可能な設計にはなりません)。科学は再現可能であるべきだと考えると、これは少し皮肉なことです。 「通常の」ソフトウェアと比較して変化するのは、テストが有用かどうか(はい!)ではなく、どの種類のテストが適切かです。

ランダム性の処理:ソフトウェアのすべての実行は再現可能でなければなりません。モンテカルロ法を使用する場合は、乱数ジェネレータに特定のシードを提供できるようにする必要があります。

  • これを忘れるのは簡単です。グローバル状態に依存するCのRand()関数を使用する場合。
  • 理想的には、乱数ジェネレーターは、関数を介して明示的なオブジェクトとして渡されます。 C++ 11のrandom標準ライブラリヘッダーは、これを非常に簡単にします。
  • ソフトウェアのモジュール間でランダムな状態を共有する代わりに、最初のRNGから乱数でシードされる2番目のRNGを作成すると便利です。次に、他のモジュールによるRNGへの要求数が変化しても、最初のRNGによって生成されるシーケンスは同じままです。

統合テストは完全に問題ありません。これらは、ソフトウェアのさまざまな部分が正しく連携して動作することを確認し、具体的なシナリオを実行するのに適しています。

  • 最小の品質レベルである「クラッシュしない」は、すでに優れたテスト結果である可能性があります。
  • より強力な結果を得るには、ベースラインと比較して結果を確認する必要もあります。ただし、これらのチェックは、ある程度許容できる必要があります。丸め誤差を考慮します。完全なデータ行の代わりに要約統計量を比較することも役立ちます。
  • ベースラインに対するチェックが脆弱すぎる場合は、出力が有効であり、いくつかの一般的なプロパティを満たしていることを確認してください。これらは一般的なもの(「選択された場所は少なくとも2km離れている必要があります」)またはシナリオ固有のもの(例: 「選択した場所はこのエリア内にある必要があります」.

統合テストを実行するときは、テストランナーを別のプログラムまたはスクリプトとして作成することをお勧めします。このテストランナーは、必要なセットアップを実行し、テストする実行可能ファイルを実行し、結果を確認して、後でクリーンアップします。

ユニットテストスタイルチェックは、ソフトウェアがそのために設計されていないため、科学的ソフトウェアに挿入するのが非常に難しい場合があります。特に、テスト中のシステムに多くの外部依存関係/相互作用がある場合、単体テストは難しくなります。ソフトウェアが純粋にオブジェクト指向でない場合、一般的にこれらの依存関係をモック/スタブ化することはできません。純粋な数学関数とユーティリティ関数を除いて、そのようなソフトウェアの単体テストはほとんど避けた方がいいと思いました。

いくつかのテストでも、テストがないよりはましです。 「コンパイルする必要がある」チェックと組み合わせると、継続的インテグレーションへの出発点としてすでに優れています。いつでも戻って後でテストを追加できます。次に、破損する可能性が高いコードの領域に優先順位を付けることができます。開発活動が増えるからです。単体テストでカバーされていないコードの部分を確認するには、コードカバレッジツールを使用できます。

手動テスト:特に複雑な問題のドメインでは、すべてを自動的にテストすることはできません。例えば。現在、確率的検索の問題に取り組んでいます。ソフトウェアが常にsameの結果を生成することをテストする場合、テストを中断することなく結果を改善することはできません。代わりに、manualテストを簡単に実行できるようにしました。固定シードを使用してソフトウェアを実行し、結果の視覚化(設定に応じて、R、Python/Pyplot、およびMatlabを使用すると、データセットの高品質の視覚化を簡単に取得できます)。この視覚化を使用して、物事がひどく間違っていないことを確認できます。同様に、少なくともログに記録するイベントの種類を選択できる場合は、ログ出力を介してソフトウェアの進行状況を追跡することは、実行可能な手動テスト手法になる可能性があります。

23
amon

すでに実装されている何百もの関数の単体テストを書き始めるのは、ちょっと奇妙に感じます。

テストを(通常は)記述したいと思うでしょう変更に応じて上記の関数。既存の関数について何百ものユニットテストを書く必要はありません。これは(大部分)時間の浪費になります。ソフトウェアは(おそらく)そのまま動作します。これらのテストのポイントは、futureの変更が古い動作を壊していないことを確認することです。特定の関数を二度と変更しない場合は、時間をかけてテストする価値はないでしょう(現在は機能しており、常に機能しており、機能し続ける可能性が高いためです)。この面でMichael FeathersによるWorking Effectively With Legacy Codeを読むことをお勧めします。彼は、依存関係を破壊する手法、特性評価テスト(関数の出力をテストスイートにコピー/貼り付けして、回帰動作を維持することを保証)など、すでに存在するものをテストするための優れた一般的な戦略をいくつか持っています。

関数パラメーターを作成するのが難しい場合、どのようにユニットテストを作成しますか?

理想的にはそうではありません。代わりに、パラメーターを作成しやすくします(したがって、設計をテストしやすくします)。確かに、設計の変更には時間がかかり、これらのリファクタリングは、従来のプロジェクトでは難しい場合があります。 TDD(テスト駆動開発)がこれを支援します。パラメータを作成するのが非常に難しい場合、テストファーストスタイルでテストを作成するのに多くの問題が発生します。

短期的にはモックを使用しますが、地獄のモックとそれに伴う長期的な問題には注意してください。私はソフトウェアエンジニアとして成長してきたので、私はモックは、ほとんどの場合、いくつかのより大きな問題をまとめようとし、コアの問題に対処していないミニ匂いです。私はそれを「芝生のラッピング」と呼ぶのが好きです。なぜなら、カーペットの上にある犬のうんちの上にスズ箔を置いたとしても、まだ臭いからです。あなたがしなければならないことは、実際に起きて、糞をすくい、それをゴミに捨てて、それからゴミを取り出すことです。これは明らかにより多くの仕事であり、あなたはあなたの手にいくつかの糞便の問題を抱えるリスクがありますが、長期的にはあなたとあなたの健康に良いです。あなたがそれらのうんちを包み続けるだけなら、あなたはあなたの家にずっと長く住みたいとは思わないでしょう。モックは本質的に似ています。

たとえば、700個のファイルを読み取らなければならないためインスタンス化が難しい_Class_A_がある場合、それをモックするだけで済みます。 次に知っておくべきことは、モックが古くなり、real _Class_A_がモックとはまったく異なる何かを実行し、テストがまだ成功しているにもかかわらずそれらはshould失敗しています。より良い解決策は、_Class_A_をeasier to use/testコンポーネントに分解し、代わりにそれらのコンポーネントをテストすることです。たぶんone実際にディスクにヒットする統合テストを記述し、_Class_A_が全体として機能することを確認します。あるいは、ディスクから読み取る代わりに、単純な文字列(データを表す)でインスタンス化できる_Class_A_のコンストラクターを持っているだけかもしれません。

結果が非常に変動しやすい場合、テストを作成するにはどうすればよいですか?

いくつかのヒント:

1)逆を使用します(より一般的には、プロパティベースのテストです)。_[1,2,3,4,5]_のfftは何ですか?わからないifft(fft([1,2,3,4,5]))とは何ですか? _[1,2,3,4,5]_でなければなりません(またはそれに近い場合、浮動小数点エラーが発生する可能性があります)。

2)「既知の」アサートを使用します。行列式関数を作成する場合、行列式が100x100行列であるとは言いにくい場合があります。ただし、単位行列が100x100であっても、行列式の行列式が1であることは知っています。また、関数は非可逆行列(0でいっぱいの100x100など)で0を返す必要があることも知っています。

3)exact assertsの代わりにラフアサートを使用します。マッピングを作成するタイポイントを生成して2つのイメージを登録するコードを少し前に書きました画像とそれらの間でワープを行い、それらを一致させます。サブピクセルレベルで登録できます。どのようにテストできますか?のようなもの:

EXPECT_TRUE(reg(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)であることを確認してくださいは一般的な範囲です(グレースケール)。少なくとも同じサイズでなければなりません。単にsmoke testを実行することもできます(つまり、呼び出してクラッシュしないことを確認します)。一般に、この手法は、テストを実行する前に最終結果を(簡単に)事前に計算できない大規模なテストに適しています。

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

実行doは再現可能である必要があります。ただし、再現可能な実行を取得する唯一の方法は、乱数ジェネレータにspecificシードを提供することです。時々ランダム性テスト価値があるランダムに生成された退化したケースで発生する科学的なコードのバグを見ました。 alwaysと同じシードで関数を呼び出す代わりに、randomシードを生成し、そのシードを使用して、シードの値をログに記録します。このように、すべての実行にはdifferentランダムシードがありますが、クラッシュが発生した場合は、デバッグのためにログに記録したシードを使用して結果を再実行できます。私は実際にこれを実際に使用し、バグをつぶしました。 欠点:テストの実行をログに記録する必要があります。メリット:正確性とバグの増加

HTH。

7
  1. テストの種類

    • すでに実装されている何百もの関数の単体テストを書き始めるのは、ちょっと奇妙に感じます

      逆に考えてみてください。複数の関数に触れるパッチがエンドツーエンドのテストの1つを壊した場合、どのテストが問題であるかをどのようにして特定するのですか?

      プログラム全体よりも個々の関数の単体テストを作成する方がはるかに簡単です。個々の関数を十分にカバーしていることを確認するのは簡単です。ユニットテストで、破損したコーナーケースを確実にキャッチできる場合は、関数をリファクタリングする方がはるかに簡単です。

      既存の関数の単体テストを作成することは、レガシーコードベースで作業したことがある人にとってはまったく正常です。これらは、最初に関数の理解を確認するための優れた方法であり、記述後は、予期しない動作の変化を見つけるための優れた方法です。

    • エンドツーエンドのテストも価値があります。記述が簡単な場合は、必ず最初にそれらを実行し、アドホックに単体テストを追加して、他のユーザーが壊すことについて最も懸念する機能をカバーしてください。一度にすべてを行う必要はありません。

    • はい、CIを既存のソフトウェアに追加することは賢明であり、正常です。

  2. 単体テストの書き方

    オブジェクトが本当に高価で複雑な場合は、モックを作成します。ポリモーフィズムを使用する代わりに、実際のオブジェクトを使用したテストとは別に、モックを使用したテストをリンクするだけです。

    とにかくインスタンスを作成する簡単な方法が必要です-ダミーインスタンスを作成する関数は一般的ですが、実際の作成プロセスをテストすることも賢明です。

  3. 変動する結果

    結果にはsomeインバリアントが必要です。単一の数値ではなく、それらをテストします。

    モンテカルロコードがパラメーターとしてそれを受け入れる場合は、疑似乱数ジェネレーターを提供できます。これにより、少なくともよく知られたアルゴリズムでは結果が予測可能になりますが、文字通り毎回同じ数を返す場合を除き、脆弱です。

2
Useless

以前の返事で amon はいくつかの非常に重要なポイントをすでに述べました。さらに追加しましょう:

1。科学ソフトウェアと商用ソフトウェアの開発の違い

もちろん、科学的なソフトウェアの場合、焦点は通常、科学的な問題です。問題は、理論的背景をよりよく処理し、最適な数値的手法を見つけることなどです。ソフトウェアは、作業のほんの一部にすぎません。

ソフトウェアは、ほとんどの場合、1人または数人だけで書かれています。多くの場合、特定のプロジェクト用に書かれています。プロジェクトが完了してすべてが公開されると、多くの場合、ソフトウェアは不要になります。

商用ソフトウェアは通常、長期間にわたって大規模なチームによって開発されます。これには、アーキテクチャ、設計、単体テスト、統合テストなどの多くの計画が必要です。この計画には、かなりの時間と経験が必要です。科学的な環境では、通常そのための時間はありません。

プロジェクトを商用ソフトウェアに類似したソフトウェアに変換したい場合は、以下を確認する必要があります。

  • 時間とリソースはありますか?
  • ソフトウェアの長期的な展望は何ですか?仕事を終えて大学を卒業するとき、ソフトウェアはどうなりますか?

2。エンドツーエンドのテスト

ソフトウェアがますます複雑になり、何人かの人がそれに取り組んでいる場合、テストは必須です。しかし amon で既に述べたように、科学ソフトウェアに単体テストを追加することは非常に困難です。したがって、別のアプローチを使用する必要があります。

ほとんどの科学ソフトウェアと同様に、ソフトウェアはファイルから入力を取得するため、いくつかのサンプル入力および出力ファイルを作成するのに最適です。各リリースでこれらのテストを自動的に実行し、結果をサンプルと比較する必要があります。これは単体テストの非常に良い代替品になる可能性があります。この方法でも統合テストを取得できます。

もちろん、再現性のある結果を得るには、乱数ジェネレーターに同じシードを使用する必要があります amon はすでに書いています。

例は、ソフトウェアの典型的な結果をカバーする必要があります。これには、パラメーター空間のエッジケースと数値アルゴリズムも含まれます。

実行にあまり時間をかけなくても、典型的なテストケースをカバーできる例を見つけるようにしてください。

3。継続的な統合

テスト例の実行には時間がかかる可能性があるため、継続的な統合は現実的ではないと思います。追加のパーツについては、同僚と話し合う必要があるでしょう。たとえば、使用される数値的手法と一致する必要があります。

ですから、理論的な背景や数値的な方法、慎重なテストなどについて話し合った後、明確に定義された方法で統合を行う方が良いと思います。

継続的インテグレーションのためのある種の自動化機能を持つことは良い考えではないと思います。

ちなみに、バージョン管理システムを使用していますか?

4。数値アルゴリズムのテスト

数値結果を比較する場合、たとえばテスト出力を確認するときは、浮動小数点数が等しいかどうかを確認しないでください。丸め誤差が常に発生する可能性があります。代わりに、その差が特定のしきい値より低いかどうかを確認してください。

また、アルゴリズムをさまざまなアルゴリズムに対してチェックするか、科学的な問題をさまざまな方法で定式化して結果を比較することもお勧めします。 2つ以上の独立した方法を使用して同じ結果が得られる場合、これは理論と実装が正しいことを示しています。

これらのテストをテストコードで実行し、本番用コードに最速のアルゴリズムを使用できます。

1
bernie
  1. CIを追加することは決してばかげた考えではありません。経験から、これは、人々が自由に貢献できるオープンソースプロジェクトがある場合の方法であることを知っています。 CIを使用すると、コードがプログラムを破壊した場合に、ユーザーがコードを追加または変更できないようにすることができます。そのため、コードベースを機能させることは、非常に貴重です。

    テストを検討するときは、コードフローが意図したとおりに機能していることを確認するために、エンドツーエンドテスト(統合テストのサブカテゴリだと思う)を確実に提供できます。統合テストの一環として、テスト中に発生した他のエラーを補正できるため、関数が正しい値を出力することを確認するために、少なくともいくつかの基本的なユニットテストを提供する必要があります。

  2. テストオブジェクトの作成は非常に困難で面倒なものです。あなたはダミーオブジェクトを作りたいと思っています。これらのオブジェクトにはデフォルトがいくつかありますが、エッジの場合、出力がどうあるべきかが確実にわかっている値です。

  3. この主題に関する本の問題は、CI(および開発のその他の部分)の状況が急速に進化するため、本の内容が数か月後に古くなる可能性があることです。私はあなたを助けることができるどんな本も知りませんが、グーグルはいつものようにあなたの救い主であるべきです。

  4. 自分でテストを複数回実行し、統計分析を行う必要があります。そうすることで、複数の実行の中央値/平均を取り、それを分析と比較して、どの値が正しいかを知るいくつかのテストケースを実装できます。

いくつかのヒント:

  • GITプラットフォームのCIツールの統合を使用して、壊れたコードがコードベースに入らないようにします。
  • 他の開発者によるピアレビューが行われる前にコードのマージを停止します。これにより、エラーがより簡単にわかるようになり、壊れたコードがコードベースに入らないようになります。
1
Pelican

私のアドバイスは、あなたの努力をどのように使うかを注意深く選ぶことです。私の分野(バイオインフォマティクス)では、最新のアルゴリズムは急速に変化するため、コードのエラー防止にエネルギーを費やす方がアルゴリズム自体に費やすほうがよいでしょう。

とは言っても、大切なのは:

  • アルゴリズムの点で、それは当時最良の方法ですか?
  • 異なるコンピューティングプラットフォーム(異なるHPC環境、OSフレーバーなど)への移植がいかに簡単か
  • 堅牢性-MYデータセットで実行されますか?

防弾コードベースを作成する本能は高貴ですが、これは商用製品ではないことを覚えておく価値があります。他の人が貢献できるように、できるだけ移植可能で、エラー防止(ユーザーのタイプに応じて)にして、アルゴリズム自体に集中する

0
pufferfish