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


22

私はソフトウェアエンジニアではありません。私は地球科学の分野で博士課程の学生です。

ほぼ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_BClass_A create_dummy_object()テキストファイルを読み取らずにダミー関数を作成することを考えました。また、何らかのシリアル化の実装についても考えました。(クラスオブジェクトは複数のテキストファイルにのみ依存するため、クラスオブジェクトの作成をテストする予定はありません)

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

CIに関するその他のアドバイスは大歓迎です。



1
ソフトウェアが正常に動作していることをどのように確認しますか?すべての変更で実行できるように、そのチェックを自動化する方法を見つけることができますか?CIを既存のプロジェクトに導入する際の最初のステップになります。
バートヴァンインゲンシェ

そもそも、ソフトウェアが許容できる結果を生成することをどのように確認しましたか?それが実際に「機能する」ことを確認するものは何ですか?両方の質問に対する回答は、現在および将来のソフトウェアをテストするための多くの資料を提供します。
ポリノーム

回答:


23

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

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

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

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

  • 最低限の品質レベルである「クラッシュしない」ことは、すでに良いテスト結果です。
  • より強力な結果を得るには、ベースラインに対して結果を確認する必要もあります。ただし、これらのチェックは、たとえば丸め誤差を考慮するなど、ある程度の耐性が必要です。完全なデータ行ではなく、要約統計を比較することも役立ちます。
  • ベースラインに対するチェックが非常に壊れやすい場合は、出力が有効であり、いくつかの一般的なプロパティを満たしていることを確認します。これらは一般的なもの(「選択した場所は少なくとも2km離れている必要があります」)またはシナリオ固有のもの、たとえば「選択した場所はこのエリア内でなければなりません」です。

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

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

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

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


7

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

上記の関数を変更すると、(通常)テスト作成する必要があります。既存の機能について何百もの単体テストを書く必要はありません。これは時間の無駄です(ほとんど)。ソフトウェアは(おそらく)そのままで大丈夫です。これらのテストのポイントは、将来の変更が古い動作を壊さないことを確認することです。特定の関数を二度と変更しないと、テストに時間をかける価値はおそらくないでしょう(現在機能しており、常に機能しており、機能し続ける可能性があるため)。レガシーコードを効果的に使用することをお勧めしますこの前線のマイケル・フェザーズ。既に存在するものをテストするためのいくつかの素晴らしい一般的な戦略があります。依存関係の破りのテクニック、特性テスト(関数の出力をテストスイートにコピー/貼り付けして回帰動作を維持することなど)などです。

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

理想的ではありません。代わりに、パラメーターを作成しやすくします(したがって、設計をテストしやすくします)。確かに、設計の変更には時間がかかり、これらのリファクタリングはあなたのようなレガシープロジェクトでは難しい場合があります。TDD(テスト駆動開発)はこれに役立ちます。パラメータを作成するのが非常に難しい場合、テストファーストスタイルでテストを書くのは大変です。

短期的には、モックを使用しますが、モックの地獄と長期的にモックに伴う問題に注意してください。しかし、ソフトウェアエンジニアとして成長するにつれて、モックはほとんど常に小さな臭いであり、核心的な問題に対処せずに大きな問題を解決しようとしていることに気付きました。私はそれを「糞ラッピング」と呼ぶのが好きです。なぜなら、あなたのカーペットの犬のうんちにスズ箔を置くと、まだ臭いからです。あなたがしなければならないのは、実際に起きて、うんちをすくい、それをゴミに投げて、ゴミを取り出すことです。これは明らかにより多くの作業であり、糞便を手に入れる危険がありますが、長期的にはあなたとあなたの健康にとってはより良いです。あなたがそれらのうんちをただ包み続けるならば、あなたはずっと長くあなたの家に住みたくないでしょう。モックは本質的に似ています。

たとえばClass_A、700個のファイルを読み込む必要があるためインスタンス化が難しい場合は、それをモックするだけで済みます。あなたが知っている次のことは、あなたのモックは時代遅れになり、実際 Class_Aはモックとは大きく異なることをし、テストは失敗するはずですが、まだ合格しています。より良い解決策は、使いやすい/テストClass_Aやすいコンポーネントに分解し、代わりにそれらのコンポーネントをテストすることです。たぶん書く1つの実際にディスクに当たる統合テストをして確認してClass_A、全体として作品を。またはClass_A、ディスクから読み取る代わりに、単純な文字列(データを表す)でインスタンス化できるコンストラクターを用意することもできます。

結果に大きなばらつきがある場合のテストの書き方

いくつかのヒント:

1)逆を使用します(より一般的には、プロパティベースのテスト)。のfftは[1,2,3,4,5]何ですか?わからない。なにifft(fft([1,2,3,4,5]))?する必要があります[1,2,3,4,5](またはそれに近い、浮動小数点エラーが出てくるかもしれません)。

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

3)完全なアサートの代わりに大まかなアサートを使用します。しばらく前に、画像間のマッピングを作成するタイポイントを生成して2つの画像を登録し、それらを一致させるために画像間でワープを行うコードを作成しました。サブピクセルレベルで登録できます。どうやってテストできますか?次のようなもの:

EXPECT_TRUE(reg(img1, img2).size() < min(img1.size(), img2.size()))

重複する部分にのみ登録できるため、登録された画像最小の画像以下である必要あります)、および:

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

自分自身に登録された画像は自分自身に近いはずですが、手元のアルゴリズムに起因する浮動小数点エラーよりも少し多く発生する可能性があるので、各ピクセルが有効範囲の+/- 5%(0-255一般的な範囲、グレースケールです)。少なくとも同じサイズでなければなりません。あなたはただテストを吸うことさえできます(すなわち、それを呼び出して、それがクラッシュしないことを確認します)。一般に、この手法は、テストを実行する前に最終結果を(簡単に)計算できない大規模なテストに適しています。

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

実行再現可能である必要があります。ただし、再現可能な実行を取得する唯一の方法は、乱数ジェネレーターに特定のシードを提供することです。ランダム性テストは貴重なこともあります。科学的なコードに、ランダムに生成された縮退したケースで発生するバグを見てきました。常に同じシードを使用して関数を呼び出す代わりに、ランダムシードを生成し、そのシードを使用して、シードの値をログに記録します。この方法では、実行ごとにランダムシードが異なりますが、クラッシュした場合は、ログに記録したシードを使用して結果を再実行できます。私は実際にこれを実際に使用し、バグをつぶしたので、私はそれを言及するだろうと考えました。欠点:テスト実行を記録する必要があります。利点:正確性とバグ修正。

HTH。


2
  1. テストの種類

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

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

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

      既存の関数の単体テストを記述することは、レガシーコードベースで作業したことのある人にとってはまったく普通のことです。そもそも機能の理解を確認するのに良い方法であり、一度書けば、予期しない動作の変化を見つけるのに良い方法です。

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

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

  2. 単体テストの書き方

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

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

  3. 変動する結果

    結果にはいくつかの不変式が必要です。単一の数値ではなく、それらをテストします。

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


1
  1. CIを追加することは決して愚かな考えではありません。経験から、これは人々が自由に貢献できるオープンソースプロジェクトを持っているときに行く方法であることを知っています。CIを使用すると、コードがプログラムを中断した場合にコードの追加や変更を禁止できます。そのため、作業中のコードベースを作成する上で非常に貴重です。

    テストを検討する場合、エンドツーエンドのテスト(統合テストのサブカテゴリだと思います)を確実に提供して、コードフローが正常に機能していることを確認できます。統合テストの一部がテスト中に行われた他のエラーを補正できるため、少なくともいくつかの基本的な単体テストを提供して、関数が正しい値を出力することを確認する必要があります。

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

  3. このテーマに関する本の問題は、CI(およびdevopsのその他の部分)のランドスケープが非常に急速に進化することであり、おそらく数か月後には本の内容が古くなってしまうでしょう。私はあなたを助ける本を知りませんが、Googleはいつものようにあなたの救い主であるべきです。

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

いくつかのヒント:

  • GITプラットフォームのCIツールの統合を使用して、破損したコードがコードベースに入力されるのを防ぎます。
  • 他の開発者によってピアレビューが行われる前に、コードのマージを停止します。これにより、エラーがより簡単に認識され、壊れたコードがコードベースに入るのを再び防ぎます。

1

amonの前の返事で、すでにいくつかの非常に重要な点について言及しています。さらに追加してみましょう。

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

科学ソフトウェアの場合、当然のことながら、通常、科学の問題に焦点が当てられます。問題は、理論的背景の処理、最適な数値計算法の発見などです。ソフトウェアは、作業のほんの一部です。

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

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

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

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

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

ソフトウェアがますます複雑になり、数人が作業している場合、テストは必須です。しかしとしてアモンすでに述べたように、科学的なソフトウェアにユニットテストを追加することは極めて困難です。そのため、別のアプローチを使用する必要があります。

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

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

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

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

3.継続的な統合

テスト例の実行には時間がかかる場合があるため、継続的な統合は実現不可能だと思います。おそらく、追加部分について同僚と話し合う必要があります。たとえば、使用される数値的方法と一致する必要があります。

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

継続的な統合のために何らかの自動化を行うのは良い考えだとは思いません。

ところで、バージョン管理システムを使用していますか?

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

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

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

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


0

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

とはいえ、大切なのは次のとおりです。

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

防弾のコードベースを構築するあなたの本能は高貴ですが、これが商用製品ではないことを覚えておく価値があります。他の人が貢献するのに便利な、できるだけ移植性の高い、エラー防止(ユーザーのタイプ)にして、アルゴリズム自体に焦点を合わせます

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.