web-dev-qa-db-ja.com

オブジェクトのモックが難しいシステムをテストするにはどうすればよいですか?

次のシステムを使用しています。

Network Data Feed -> Third Party Nio Library -> My Objects via adapter pattern

最近、使用しているライブラリのバージョンを更新するという問題がありました。これにより、特に、タイムスタンプ(サードパーティのライブラリがlongとして返す)がエポック後のミリ秒からエポック後のナノ秒。

問題:

サードパーティのライブラリのオブジェクトを模擬するテストを作成する場合、サードパーティのライブラリのオブジェクトについて誤りを犯した場合、私のテストは正しくありません。たとえば、モックが間違ったデータを返したため、タイムスタンプの精度が変化し、ユニットテストを変更する必要があることを知りませんでした。これはライブラリのバグではありません。ドキュメントで何かを見逃したために発生しました。

問題は、私はこれらのデータ構造に含まれるデータについて確信が持てないことです。なぜなら、私は、実際のデータフィードなしでは実際のデータを生成できないためです。これらのオブジェクトは大きくて複雑で、多くのそれらの異なるデータの一部。サードパーティライブラリのドキュメントは不十分です。

質問:

この動作をテストするようにテストを設定するにはどうすればよいですか?テスト自体が簡単に間違っている可能性があるため、単体テストでこの問題を解決できるかどうかはわかりません。さらに、統合されたシステムは大きくて複雑であり、見落としがちです。たとえば、上記の状況で、タイムスタンプの処理をいくつかの場所で正しく調整しましたが、そのうちの1つがありませんでした。このシステムは私の統合テストでほぼ正しいことをしているように見えましたが、実稼働環境(より多くのデータを持っている)にシステムをデプロイしたところ、問題が明らかになりました。

現在、統合テストのプロセスはありません。テストは基本的に、ユニットテストを適切に保ち、問題が発生したときにテストを追加してから、テストサーバーにデプロイして、正常に見えることを確認してから、本番環境にデプロイします。このタイムスタンプの問題は、モックが正しく作成されなかったためにユニットテストに合格し、すぐに明らかな問題を引き起こさなかったために統合テストに合格しました。 QA部門がありません。

34
durron597

すでにデューデリジェンスを行っているようですね。だが ...

最も実用的なレベルではは常に、独自のコード用の「フルループ」統合テストのほんの一握りをスイートに含め、必要以上のアサーションを記述します。特に、完全なcreate-read- [do_stuff] -validateサイクルを実行する少数のテストが必要です。

[TestMethod]
public void MyFormatter_FormatsTimesCorrectly() {

  // this test isn't necessarily about the stream or the external interpreter.
  // but ... we depend on them working how we think they work:
  var stream = new StreamThingy();
  var interpreter = new InterpreterThingy(stream);
  stream.Write("id-123, some description, 12345");

  // this is what you're actually testing. but, it'll also hiccup
  // if your 3rd party dependencies introduce a breaking change.
  var formatter = new MyFormatter(interpreter);
  var line = formatter.getLine();
  Assert.equal(
    "some description took 123.45 seconds to complete (id-123)", line
  );
}

そして、あなたはすでにこの種のことをしているように思えます。あなたは、不安定で複雑なライブラリを扱っているだけです。そしてその場合、ライブラリの理解を確認し、ライブラリの使用方法の例として役立つ、いくつかの「これはライブラリの動作方法」タイプのテストを投入することをお勧めします。

JSONパーサーがJSON文字列の各「タイプ」をどのように解釈するかを理解し依存する必要があるとします。スイートに次のようなものを含めると便利で簡単です。

[TestMethod]
public void JSONParser_InterpretsTypesAsExpected() {
  String datastream = "{nbr:11,str:"22",nll:null,udf:undefined}";
  var o = (new JSONParser()).parse(datastream);

  Assert.equal(11, o.nbr);
  Assert.equal(Int32.getType(), o.nbr.getType());
  Assert.equal("22", o.str);
  Assert.equal(null, o.nll);
  Assert.equal(Object.getType(), o.nll.getType());
  Assert.isFalse(o.KeyExists(udf));
}

しかし2番目に、あらゆる種類の自動テスト、およびほぼすべてのレベルの厳密さでは、すべてのバグからユーザーを保護できないことに注意してください。問題を発見したときにテストを追加することは完全に一般的ですQA部門がないため、これらの問題の多くはエンドユーザーによって発見されます。

そして、かなりの程度、それは普通のことです。

そして3番目に、フィールドまたはメソッドの名前を変更することなく、またはその他の方法で依存コードを「破壊」することなく(おそらくそのタイプを変更することによって)、ライブラリが戻り値またはフィールドの意味を変更すると、かなり気の毒になるその出版社に不満。そして、変更ログがある場合はおそらく変更ログを読んでいるはずですが、ストレスの一部をパブリッシャーにも渡す必要があると私は主張します。私は彼らがうまくいけば建設的な批判が必要だと主張します...

27
svidgen

短い答え:難しいです。良い答えはないような気がしますが、それは簡単な答えがないからです。

長い答え: @ ptyxは言う のように、システムテストと統合テスト、および単体テストが必要です。

  • 単体テストは高速で実行が簡単です。コードの個々のセクションでバグをキャッチし、モックを使用してそれらを実行できるようにします。必要に応じて、コードの断片間の不一致(ミリ秒とナノ秒など)をキャッチできません。
  • 統合テストとシステムテストは実行が遅く、困難ですが、より多くのエラーをキャッチします。

いくつかの具体的な提案:

  • システムテストをできるだけ多くのシステムで実行することには、いくつかの利点があります。動作の大部分を検証できない場合や、問題を特定するのに十分に機能しない場合でも。 (Micheal Feathersは、これについて 効果的なレガシーコードの使用 で説明しています。)
  • テスト容易性への投資が役立ちます。ここで使用できるhugeテクニックは多数あります。継続的インテグレーション、スクリプト、VM、再生するツール、プロキシ、または redirect ネットワークトラフィック。
  • テスト可能性への投資の(少なくとも私にとっては)利点の1つは自明ではないかもしれません。テストが退屈で煩わしい、または作成や実行が面倒な場合、プレッシャーがかかった場合にテストをスキップするのは簡単すぎます。または疲れた。テストを「非常に簡単なので、しないしないわけにはいかない」というしきい値を下回るようにすることが重要です。
  • 完璧なソフトウェアは実現可能ではありません。 他のすべてのように、テストに費やされた努力はトレードオフであり、時には努力する価値がありません。制約(QA部門がないなど)が存在します。バグが発生することを受け入れ、回復し、学習します。

私はプログラミングを、問題と解決策について学ぶ活動として説明してきました。事前にすべてを完璧にすることは現実的ではないかもしれませんが、事後に学ぶことができます。 (「タイムスタンプの処理をいくつかの場所で修正しましたが、1つは見落としました。データ型またはクラスを変更して、タイムスタンプの処理をより明確にして見落としにくくしたり、一元化したりして、変更する場所を1か所だけにできますか?変更できますか?タイムスタンプ処理のより多くの側面を検証するためのテスト?テスト環境を簡略化して将来これを簡単にすることはできますか?これを簡単にするツールを想像できますか?そうであれば、そのようなツールをGoogleで見つけることができますか? 「など」

11
Josh Kelley

ライブラリのバージョンを更新した結果、タイムスタンプ(サードパーティのライブラリがlongとして返す)をエポック後のミリ秒からエポック後のナノ秒に変更しました。

これはライブラリのバグではありません

私はここであなたに強く反対します。それははライブラリのバグです、実際にはかなり油断のならないバグです。戻り値のセマンティックタイプは変更されましたが、戻り値のプログラムタイプは変更されませんでした。これは、特にマイナーバージョンのバンプの場合はもちろん、メジャーバージョンのバンプの場合でも、あらゆる種類の混乱を引き起こす可能性があります。

その代わりに、ライブラリがMillisecondsSinceEpochを保持する単純なラッパーであるlongのタイプを返したとしましょう。彼らがそれをNanosecondsSinceEpoch値に変更した場合、コードはコンパイルに失敗し、明らかに変更が必要な場所を指摘していたでしょう。この変更により、プログラムを静かに破損させることはできませんでした。

より良いのは、TimeSinceEpochオブジェクトを追加して、#toLongNanosecondsメソッドと一緒に#toLongMillisecondsメソッド。コードをまったく変更する必要はありません。

次の問題は、ライブラリに対する統合テストの信頼できるセットがないことです。あなたはそれらを書くべきです。そのライブラリの周りにインターフェースを作成して、それをアプリケーションの残りの部分から切り離してカプセル化する方がよいでしょう。他のいくつかの回答がこれに対処しています(入力するたびにさらに表示されます)。統合テストは、単体テストよりも実行頻度を低くする必要があります。そのため、バッファレイヤーを使用すると役立ちます。統合テストを別の領域に分離(またはそれらに別の名前を付ける)して、必要に応じて実行できるようにしますが、ユニットテストを実行するたびに実行できるわけではありません。

6
cbojar

統合テストとシステムテストが必要です。

単体テストは、コードが期待どおりに動作することを確認するのに最適です。ご存知のように、それはあなたの仮定に挑戦したり、あなたの期待が正気であることを保証したりすることは何もしません。

製品が外部システムとほとんどやり取りしない場合、またはよく知られていて安定しており、自信を持って模倣できるように文書化されているシステムとやり取りしない限り(これは現実の世界ではめったに起こりません)、単体テストでは不十分です。

テストのレベルが高いほど、予期しないテストから保護されます。これにはコスト(利便性、速度、もろさなど)が伴います。そのため、単体テストはテストの基礎を維持する必要がありますが、他のレイヤーも必要です。誰も考えなかった愚かなこと。

5
ptyx

最善の方法は、最小限のプロトタイプを作成し、ライブラリがどのように機能するかを理解することです。そうすることにより、ライブラリに関する知識が不足し、ドキュメントが不十分になります。プロトタイプは、そのライブラリを使用して機能を実行する最小限のプログラムにすることができます。

さもなければ、半分定義された要件とシステムの弱い理解でユニットテストを書くことは意味がありません。

あなたの特定の問題について-間違ったメトリックの使用について:私はそれを要件の変更として扱います。問題を認識したら、単体テストとコードを変更します。

2
BЈовић

人気のある安定したライブラリを使用している場合は、悪質なトリックを実行しないと思われるかもしれません。しかし、あなたが説明したようなことがこのライブラリで発生した場合、明らかに、これは1つではありません。この悪い経験の後、このライブラリとのやり取りで何か問題が発生するたびに、間違いを犯した可能性だけでなく、図書館が間違いを犯した可能性も調べる必要があります。それで、これがあなたが「わからない」ライブラリであるとしましょう。

私たちが「確信が持てない」ライブラリーで採用されている手法の1つは、システムと上記のライブラリーの間に中間層を構築することです。そのライブラリを起動して、より適切に動作する別のライブラリに置き換えることを決定した場合、私たちの将来の人生。

1
Mike Nakis