私はこのsnafuについて読みました: 15年間テストデータと間違えられた合法的なトランザクションの後に、プログラミングバグはCitigroup $ 7mを要します 。
システムが1990年代半ばに導入されたとき、プログラムコードは089から100までの3桁の分岐コードが与えられたトランザクションをすべて除外し、テスト目的でこれらのプレフィックスを使用しました。
しかし、1998年、同社は事業を拡大するにつれ、英数字の支店コードの使用を開始しました。その中には、コードが10B、10Cなどであり、システムは除外範囲内として処理したため、それらのトランザクションはSECに送信されたレポートから削除されました。
(これは、非明示的データインジケーターの使用が最適ではないことを示していると思います。意味的に明示的なBranch.IsLive
プロパティを設定して使用する方がはるかに優れていました。)
それはさておき、私の最初の反応は「ユニットテストはここで助けになったでしょう」でした...しかし、彼らはそうでしょうか?
私は最近読んだ なぜほとんどの単体テストが無駄である 興味を持って、そして私の質問は:英数字の分岐コードの導入で失敗したであろう単体テストはどのように見えるでしょうか?
あなたは本当に「ユニットテストはここで助けになったでしょうか?」または「どんな種類のテストもおそらくここで助けになったでしょうか?」と尋ねていますか?.
役立つと思われる最も明白なテスト形式は、コード自体の前提条件アサーションであり、ブランチ識別子は数字のみで構成されている(これは、コードを記述する際にコーダーが依存する仮定であると想定)。
これは、ある種の統合テストで失敗する可能性があり、新しい英数字のブランチIDが導入されるとすぐに、アサーションが爆発します。しかし、それは単体テストではありません。
あるいは、SECレポートを生成する手順の統合テストが存在する可能性があります。このテストは、すべての実際のブランチ識別子がトランザクションを報告することを保証します(したがって、実際の入力、つまり使用中のすべてのブランチ識別子のリストが必要です)。したがって、それも単体テストではありません。
関連するインターフェースの定義やドキュメントは表示されませんが、ユニットテストでエラーが検出されなかった可能性がありますユニットに障害がなかったため。ユニットがブランチ識別子が数字のみで構成されていると想定することが許可されており、開発者がそうでない場合にコードが行うべきことを決して決定しなかった場合、開発者はすべきではないにユニットテストを書き込みます数字以外の識別子の場合は特定の動作を強制します。これは、テストが英数字のブランチ識別子を正しく処理したユニットの架空の有効な実装を拒否し、通常、有効な将来の実装と拡張を妨げるユニットテストを記述したくないためです。あるいは、40年前に1つのドキュメントが暗黙的に定義されて(より人間にやさしい照合規則ではなく、生のEBCDICの辞書編集範囲を介して)、10Bが実際には089から100の間にあるため、テスト識別子であると定義されています15年前に誰かがそれを実際の識別子として使用することを決めたため、「障害」は元の定義を正しく実装するユニットにありません。10Bがテスト識別子として定義されていることに気付かなかったプロセスにありますしたがって、ブランチに割り当てるべきではありません。同じことがASCIIで発生するのは、テスト範囲として089-100を定義してから、識別子10 $または1.0を導入した場合です。EBCDICでは、数字が文字の後に来るだけです。
おそらくが1日を節約できた可能性がある1つの単体テスト(またはおそらく機能テスト)は、新しいブランチ識別子を生成または検証する単体のテストです。このテストは、識別子に数字のみが含まれている必要があることを表明し、ブランチ識別子のユーザーが同じであると想定できるように記述されます。または、実際のブランチIDをインポートするがテストIDを見ないユニットがどこかにあり、ユニットテストしてすべてのテストIDを確実に拒否することができます(IDが3文字のみの場合、すべてを列挙して、それらが一致することを確認するためのtest-filterのバリデーターへのバリデーターは、スポットテストへの通常の反対を扱います)。次に、誰かがルールを変更した場合、ユニットテストは新しく必要な動作と矛盾するため失敗します。
テストは正当な理由でそこにあったので、ビジネス要件の変更のためにテストを削除する必要があるポイントは、誰かが仕事を与えられる機会になります。「私たちが望む動作に依存するコード内のすべての場所を見つける変化する"。もちろん、これは難しく、したがって信頼性が低いので、その日を節約できるとは限りません。しかし、プロパティを仮定しているユニットのテストで仮定をキャプチャした場合、あなたは自分にチャンスを与えたので、努力は無駄になりません完全に無駄になります。
もちろん、ユニットが「おかしな形の」入力で最初に定義されていなかった場合、テストするものは何もないことに同意します。面倒な名前空間の分割は、おかしな定義を実装することに問題があるわけではないため、適切にテストするのが難しい場合があります。おかしな定義を誰もが理解し、尊重するようにすることです。これは、1つのコード単位のローカルプロパティではありません。さらに、一部のデータ型を「数字の文字列」から「英数字の文字列」に変更することは、ASCIIベースのプログラムでUnicodeを処理することに似ています。コードが元の定義に強く結合している場合、およびデータ型は、プログラムが行うことの基本であり、多くの場合、強く結合されます。
主に無駄な努力だと考えるのは少し不安です
ユニットテストが失敗することがある場合(たとえば、リファクタリングしているときに)、そうすることで有用な情報(たとえば、変更が間違っているなど)を提供できれば、労力は無駄になりませんでした。彼らがしないことは、あなたのシステムが機能するかどうかをテストすることです。したがって、単体テストを作成している場合、代わりに機能テストと統合テストを行っていると、時間を最適に使用できていない可能性があります。
ユニットテストでは、ブランチコード10Bと10Cが「テスト用ブランチ」として誤って分類されていることを検出できたかもしれませんが、そのブランチ分類のテストがそのエラーを検出するのに十分なほど広範囲に及ぶことはありません。
一方、生成されたレポートのスポットチェックでは、分岐した10Bと10Cが一貫してレポートから失われ、バグが存在し続けることを許可された15年よりもはるかに早くなくなった可能性があります。
最後に、これは、1つのデータベースでテストデータと実際の本番データを混在させることが悪い考えである良い例です。彼らがテストデータを含む別のデータベースを使用していた場合、公式レポートからそれをフィルタリングする必要はなく、あまり多くをフィルタリングすることは不可能でした。
ソフトウェアは特定のビジネスルールを処理する必要がありました。ユニットテストがあった場合、ユニットテストはソフトウェアがビジネスルールを正しく処理したことを確認します。
ビジネスルールが変更されました。
ビジネスルールが変更されたことに誰も気づかなかったようで、新しいビジネスルールを適用するためにソフトウェアを変更した人はいません。ユニットテストがあった場合、それらのユニットテストを変更する必要がありますが、ビジネスルールが変更されたことに誰も気付いていなかったため、誰もそれをしなかったでしょう。
だから、ユニットテストはそれを捕まえなかっただろう。
例外は、ユニットテストとソフトウェアが独立したチームによって作成され、ユニットテストを実行するチームがテストを変更して新しいビジネスルールを適用する場合です。その後、単体テストは失敗し、ソフトウェアの変更につながると期待されます。
もちろん、同じ場合で、ユニットテストではなくソフトウェアのみが変更された場合、ユニットテストも失敗します。単体テストが失敗しても、それはソフトウェアが間違っていることを意味するのではなく、ソフトウェアまたは単体テスト(場合によっては両方)が間違っていることを意味します。
いいえ。これは、単体テストの大きな問題の1つです。それらは、誤った安心感にあなたを惑わします。
すべてのテストに合格しても、システムが正しく機能しているとは限りません。これは、すべてのテストが合格していることを意味します。これは、意識的に考えてテストを記述したデザインの部分が、意識的に思ったとおりに機能していることを意味します。これは、実際にはそれほど大したことではありません。それは、実際に注意を払っていたものです。とにかく、あなたはとにかくそれを正しく持っている可能性が非常に高いです!しかし、あなたがそれらのテストを書くことを決して考えなかったので、これのようなあなたが決して考えなかったケースを捕まえることは何もしません。(そしてあなたがコードの変更が必要であることを知っていて、それらを変更しているはずです。)
いいえ、必ずしもそうとは限りません。
元々の要件は数値分岐コードを使用することでした。そのため、さまざまなコードを受け入れ、10Bのようなものをすべて拒否するコンポーネントの単体テストが作成されます。システムは機能しているものとして渡されていたはずです(そうでした)。
次に、要件が変更されてコードが更新されますが、これにより、不良データ(つまり、現在は良好なデータ)を提供する単体テストコードを変更する必要があります。
これで、システムを管理する人々はこれが事実であることを認識し、ユニットテストを変更して新しいコードを処理することになると想定します...とにかくコード..そして彼らはそれをしませんでした。最初にコード10Bを拒否した単体テストは、そのテストを更新することを知らなかった場合、実行時に「すべてがここで問題ない」と喜んで言ったでしょう。
単体テストは、元の開発には適していますが、システムテストには適していません。特に、要件が長い間忘れられてから15年はかかりません。
このような状況で必要なのは、エンドツーエンドの統合テストです。機能すると予想されるデータを渡して、機能するかどうかを確認できる場所。誰かが彼らの新しい入力データがレポートを作成しないことに気づいて、それからさらに調査するでしょう。
タイプテスト(Haskellテストライブラリ QuickCheck と他の言語でインスピレーションを得たさまざまなポート/代替手段によって例示される、ランダムに生成された有効なデータを使用して不変条件をテストするプロセス)は、この問題を捕捉した可能性があります、ユニットテストはほぼ確実に行われなかったでしょう。
これは、分岐コードの有効性に関するルールが更新されたときに、これらの特定の範囲をテストして正しく機能することを確認することを誰も考えなかったためです。
ただし、型テストが使用されていた場合、元のシステムが実装された時点で誰かがをペアにして、テストブランチの特定のコードはテストデータとして扱われ、ブランチコードのデータ型定義が更新されたときに他のコードがなかったことを確認するコードが変更されました(変更のテストを可能にするために必要でした)数字から数値への分岐コードが機能した場合)、このテストは新しい範囲の値のテストを開始し、おそらく障害を特定します。
もちろん、QuickCheckは1999年に最初に開発されたため、この問題を把握するには遅すぎました。
ユニットテストがこの問題に影響を与えるかどうかは本当に疑わしいです。新しいブランチコードをサポートするように機能が変更されたため、これらのトンネルビジョンの状況の1つに聞こえますが、これはシステムのすべての領域で実行されたわけではありません。
ユニットテストを使用してクラスを設計します。ユニットテストの再実行は、デザインが変更された場合にのみ必要です。特定のユニットが変更されない場合、変更されていないユニットテストは以前と同じ結果を返します。単体テストは、他のユニットへの変更の影響を示すことはありません(もしそうであれば、単体テストを作成していません)。
この問題は、次の方法でしか合理的に検出できません。
十分なエンドツーエンドのテストがないと、さらに心配になります。システム変更のONLYまたはMAINテストとしてユニットテストに依存することはできません。新しくサポートされたブランチコード形式でレポートを実行するだけでよいようです。
これからのポイントは Fail Fast です。
コードがないため、コードに応じてブランチのプレフィックスをテストする、またはしないプレフィックスの例も多くありません。私たちが持っているのはこれだけです:
コードが数値と文字列を許可するという事実は、少し奇妙なことです。もちろん、10Bと10Cは16進数と見なされますが、プレフィックスがすべて16進数として扱われる場合、10Bと10Cはテスト範囲外になり、実際の分岐として扱われます。
これは、プレフィックスが文字列として格納されているが、場合によっては数値として扱われることを意味します。これは私が考えることができる最も単純なコードで、この動作を再現しています(説明のためにC#を使用しています)。
bool IsTest(string strPrefix) {
int iPrefix;
if(int.TryParse(strPrefix, out iPrefix))
return iPrefix >= 89 && iPrefix <= 100;
return true; //here is the problem
}
英語では、文字列が数値で89〜100の場合、それはテストです。数値でない場合は、テストです。そうでなければ、それはテストではありません。
コードがこのパターンに従っている場合、コードのデプロイ時にユニットテストでこれを捕捉することはできませんでした。ユニットテストの例をいくつか示します。
assert.isFalse(IsTest("088"))
assert.isTrue(IsTest("089"))
assert.isTrue(IsTest("095"))
assert.isTrue(IsTest("100"))
assert.isFalse(IsTest("101"))
assert.isTrue(IsTest("10B")) // <--- business rule change
ユニットテストは、「10B」をテストブランチとして扱う必要があることを示しています。上記のユーザー@ gnasher729は、ビジネスルールが変更されたと述べ、それが上記の最後のアサーションが示すものです。ある時点で、アサートはisFalse
に切り替わるはずでしたが、実際には起こりませんでした。単体テストは開発時とビルド時に実行されますが、その後は実行されません。
ここでの教訓は何ですか?コードは、予期しない入力を受け取ったことを通知する何らかの方法を必要とします。このコードを記述する別の方法は、接頭辞が数値であると想定していることを強調しています。
// Alternative A
bool TryGetIsTest(string strPrefix, out bool isTest) {
int iPrefix;
if(int.TryParse(strPrefix, out iPrefix)) {
isTest = iPrefix >= 89 && iPrefix <= 100;
return true;
}
isTest = true; //this is just some value that won't be read
return false;
}
C#を知らない人のために、戻り値は、コードが指定された文字列からプレフィックスを解析できたかどうかを示します。戻り値がtrueの場合、呼び出しコードはisTest out変数を使用して、ブランチプレフィックスがテストプレフィックスであるかどうかを確認できます。戻り値がfalseの場合、呼び出しコードは、指定されたプレフィックスが予期されていないことを報告する必要があり、isTest out変数は意味がないため、無視する必要があります。
例外があっても問題ない場合は、代わりにこれを行うことができます。
// Alternative B
bool IsTest(string strPrefix) {
int iPrefix = int.Parse(strPrefix);
return iPrefix >= 89 && iPrefix <= 100;
}
この代替案はより簡単です。この場合、呼び出しコードは例外をキャッチする必要があります。どちらの場合も、コードは、整数に変換できないstrPrefixを予期していないことを呼び出し元に報告する何らかの方法を備えている必要があります。このようにして、コードはすばやく失敗し、銀行はSECの細かい恥ずかしさなしに問題をすばやく見つけることができます。
ランタイムに組み込まれたアサーションが役に立った可能性があります。例えば:
bool isTestOnly(string branchCode) { ... }
のような関数を作成します以下も参照してください。
非常に多くの答えがあり、ダイクストラの引用も1つではありません。
テストは、バグがないことではなく、存在を示します。
したがって、状況によって異なります。コードが適切にテストされていれば、おそらくこのバグは存在しないでしょう。