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


34

私は次のシステムで作業しています:

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

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

問題:

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

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

質問:

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

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


3
実際のデータフィードを「記録」し、後でサードパーティライブラリに「再生」できますか?
イダンアリー

2
誰かがこのような問題に関する本を書くことができます。実際には、マイケル羽は書き込みだけでその本でした:c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCodeをそれでそのコードがより多くのテスト可能になることができますので、彼は困難な依存関係を壊すための多くの技術を説明します。
cbojar

2
サードパーティのライブラリの周りのアダプタ?はい、それはまさに私がお勧めです。これらの単体テストでは、コードは改善されません。信頼性や保守性が向上することはありません。その時点で、誰かのコードを部分的に複製しているだけです。この場合、あなたはそれの音からいくつかの不十分に書かれたコードを複製しています。それは純損失です。いくつかの答えは、いくつかの統合テストを行うことを示唆しています。「これは機能していますか?」サニティーチェック。良いテストは難しく、良いコードと同じくらいのスキルと直感が必要です。
jpmc26

4
組み込みの悪の完璧な例。なぜライブラリが戻りませんTimestamp(という名前のメソッド(彼らが望む任意の表現を含む)クラスを提供して.seconds().milliseconds().microseconds().nanoseconds())、そしてもちろん名前のコンストラクタのを。その後、問題はなかっただろう。
マチューM.

2
...ここに頭に浮かぶ「コード内のすべての問題は、(間接的な、あまりにも多くの層の問題は、もちろん、除く)間接の層により解決することができる」と言って
ダン・パントリー

回答:


27

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

最も実用的なレベルでは、自分のコード用にスイートに「フルループ」統合テストの両方を常に含め、必要以上に多くのアサーションを作成してください。特に、完全な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));
}

しかし、第二に、あらゆる種類の、そしてあらゆるレベルの厳密さでの自動化されたテストは、すべてのバグからあなたを守ることに失敗することを忘れないでください。問題を発見したときにテストを追加することは非常に一般的です。QA部門がないため、これらの問題の多くはエンドユーザーによって発見されることになります。

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

そして第三に、ライブラリがフィールドやメソッドの名前を変更せずに戻り値やフィールドの意味を変更したり、依存コードを「壊したり」する場合(タイプを変更することによって)、私はそのパブリッシャーに非常に不満を感じるでしょう。また、変更ログがある場合はおそらく変更ログを読んでおく必要がありますが、出版社にあなたのストレスの一部を伝えるべきだと主張します。私は彼らがうまくいけば建設的な批判が必要だと主張します...


うーん、私はそれがライブラリにjson文字列を供給するのと同じくらい簡単だったことを望みます。そうではありません。から同等の(new JSONParser()).parse(datastream)データを直接取得することはできませんNetworkInterface。実際の解析を行うすべてのクラスはパッケージプライベートで保護されています。
durron597

また、変更ログには、タイムスタンプがmsからnsに変更されたという事実が記録されていませんでした。はい、私は彼らに非常に不満を抱いており、彼らにこれを表明しました。
durron597

@ durron597ああ、それはほとんどありません。ただし、最初のコード例のように、基になるデータソースを偽造することがよくあります。...ポイントは次のとおりです。可能な場合は完全なループ統合テストを実行し、可能な場合はライブラリの理解度をテストし、バグを未処理のままにすることに注意してください。また、サードパーティベンダー、目に見えない破壊的な変更を行う責任を負う必要あります。
svidgen

@ durron597私はよく知らないNetworkInterface... localhostのポートにインターフェイスを接続するなどしてデータをフィードできるのか?
svidgen

NetworkInterface。ネットワークカードを直接操作したり、ソケットを開いたりするための低レベルオブジェクトです。
durron59715年

11

簡単な答え:難しいです。あなたはおそらく良い答えがないように感じています、それは簡単な答えがないからです。

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

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

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

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

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


7

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

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

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

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

いっそのことだろうTimeSinceEpoch、より精度は、このような追加など、追加されたとして、それのインターフェースを適応させることができ、オブジェクト#toLongNanoseconds側に沿ってメソッドに#toLongMilliseconds、方法をあなたのコードを一切への変更を必要としません。

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


2
@ durron597私はまだバグだと主張します。ドキュメントの不足を超えて、なぜ期待される動作をまったく変更するのですか?なぜ新しい精度を提供する新しいメソッドではなく、古いメソッドでもミリ秒を提供できるのでしょうか?そして、コンパイラーが戻り値の型の変更を警告する方法を提供してみませんか?ドキュメントだけでなくコード自体でも、これをもっと明確にするのに多くのことは必要ありません。
cbojar

1
「彼らは貧しいリリース慣行を持っていること」@gbjbaanb、私にはバグのように思える
アルトゥーロ・トレス・サンチェス・

2
@gbjbaanbサードパーティのライブラリは、ユーザーと「契約」を結ぶべきです。その契約を破る-それが文書化されているかどうかにかかわらず-バグと見なすことができます/すべきです。他の人が言ったように、何かを変更する必要がある場合は、新しい関数/メソッドを使用してコントラクトに追加します(...Ex()Win32APIのすべてのメソッドを参照)。これが実行可能でない場合、関数(またはその戻り値の型)の名前を変更して契約を「破る」方が、動作を変更するよりも優れているはずです。
-TripeHound

1
これはライブラリのバグです。ナノ秒を長時間使用することは、それを推進しています。
ジョシュア

1
@gbjbaanb予期しない動作であっても、意図した動作であるため、バグではないと言います。その意味では、実装のバグではありませんが、同じバグです。これは、設計上の欠陥またはインターフェースのバグと呼ばれる場合があります。欠点は、明示的なユニットではなくlongでプリミティブな強迫観念を露呈し、その内部実装の詳細をエクスポートするため(データが特定のユニットのlongとして格納される)抽象性が漏洩し、違反するという事実にあります微妙な単位の変更を伴う最小限の驚きの原則。
cbojar

5

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

単体テストは、コードが期待どおりに動作することを確認するのに最適です。理解しているように、仮定に挑戦したり、期待が正気であることを確認したりすることはありません。

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

テストのレベルが高ければ高いほど、予期しないものから保護されます。それにはコストがかかります(利便性、速度、脆さ...)ので、ユニットテストはテストの基礎のままである必要がありますが、他のレイヤーが必要です-最終的に-キャッチするのに長い道のりをする小さな人間のテストを含む誰も考えなかった愚かなこと。


2

最良の方法は、最小限のプロトタイプを作成し、ライブラリが正確に機能する方法を理解することです。そうすることで、ドキュメントの質の低いライブラリに関する知識を得ることができます。プロトタイプは、そのライブラリを使用して機能を実行する最小限のプログラムです。

それ以外の場合、半分定義された要件とシステムの弱い理解で、単体テストを書くことは意味がありません。

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


1

人気のある安定したライブラリを使用している場合は、おそらくそれはあなたに厄介なトリックを再生しないと仮定することができます。しかし、あなたが説明したようなことがこのライブラリで起こった場合、これは明らかにそうではありません。この悪い経験の後、このライブラリとのやり取りで何か問題が発生するたびに、間違いを犯した可能性だけでなく、ライブラリが間違いを犯した可能性も調べる必要があります。だから、これはあなたが「不確か」なライブラリだとしましょう。

「不明」なライブラリで使用される手法の1つは、システムと前述のライブラリの間に中間層を構築することです。これにより、ライブラリによって提供される機能が抽象化され、将来の私たちの生活、そのライブラリにブートを与え、より良い動作をする別のライブラリに置き換えることに決めた場合。


これは本当に質問に答えません。ライブラリをシステムから分離するレイヤーが既にありますが、問題は、ライブラリが警告なしに変更されると、抽象レイヤーに「バグ」が発生する可能性があることです。
durron597

1
@ durron597その場合、レイヤーはライブラリをアプリケーションの他の部分から十分に分離していない可能性があります。そのレイヤーのテストに苦労していることがわかった場合は、動作を単純化し、基になるデータをより強力に分離する必要があるかもしれません。
cbojar

@cbojarが言ったこと。また、上記のテキストで見過ごされていたかもしれないことを繰り返しましょう。assertキーワード(または、使用している言語に応じて機能または機能)はあなたの友人です。私はユニット/統合テストでのアサーションについて話しているのではなく、分離層はアサーションで非常に重く、ライブラリの動作についてアサーション可能なすべてをアサートする必要があると言っています。
マイクナキス

これらのアサーションは、実稼働環境で必ずしも実行されるわけではありませんが、テスト中に実行され、分離レイヤーのホワイトボックスビューがあるため、レイヤーがライブラリから受け取る情報を(可能な限り)確認できます音です。
マイクナキス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.