ユニットテストは、シティグループがこの高価な間違いを避けるのに役立ちましたか?


86

私はこのスナフについて読みました:正当な取引が15年間テストデータと間違えられた後、Citigroupのプログラミングバグには700万ドルの費用がかかります

システムが1990年代半ばに導入されたとき、プログラムコードは、089から100までの3桁のブランチコードが与えられたトランザクションを除外し、それらのプレフィックスをテスト目的で使用しました。

しかし1998年に、同社は事業を拡大するにつれて英数字のブランチコードの使用を開始しました。その中にはコード10B、10Cなどがあり、システムは除外範囲内にあると見なしたため、SECに送信されたレポートからそれらのトランザクションは削除されました。

(これは、非明示的なデータインジケーターを使用することは...最適ではないことを示していると思いBranch.IsLiveます。セマンティックに明示的なプロパティを設定して使用する方がはるかに良いでしょう。)

それはさておき、私の最初の反応は「ユニットテストはここで助けになるだろう」でした...しかし、彼らはどうでしょうか?

最近読んだのは、ほとんどのユニットテストがなぜ無駄なのかということでした。そのため、私の質問は次のとおりです。英数字ブランチコードの導入で失敗したユニットテストはどのようなものでしょうか。



17
SECにエクスポートされたトランザクションの量をチェックする統合テストも見逃したようです。妥当なチェックになるエクスポート機能を構築する場合。
リュックフランケン

31
この記事の著者は単体テストを理解していないようです。一部の主張は単純にばかげている(「単体テストは特定のメソッドの1兆分の1以上の機能をテストする可能性は低い」)、他の主張は回帰の可能性を破壊する(1年以内に失敗したことのないテストを見て、それらを捨てることを検討してください」)。または、「単体テストをアサーションに変える」などの提案は、ランタイム例外の失敗したテストを変更することになっていますか?
グルー

25
@gnat私は外部リンクを読んでいませんでしたが、この質問にはまだ意味があると感じました
Jeutnarg

23
価値のあることについては、「なぜほとんどのユニットテストが無駄なのか」のすべてにほとんど同意しません。私は反論を書きますが、このマージンはそれを抑えるには小さすぎます。
ロバートハーベイ

回答:


19

あなたは本当に「単体テストはここで助けたでしょうか?」と尋ねていますか、それとも「何らかの種類のテストがここで助けたでしょうか?」

助けになるテストの最も明白な形式は、コード自体の前提条件アサーションです。ブランチ識別子は数字のみで構成されます(これは、コードを書く際にコーダーが依存する仮定であると仮定します)。

これは、ある種の統合テストで失敗する可能性があり、新しい英数字のブランチIDが導入されるとすぐに、アサーションが爆発します。しかし、それは単体テストではありません。

または、SECレポートを生成する手順の統合テストがあります。このテストは、すべての実際のブランチ識別子がそのトランザクションを報告することを保証します(したがって、実際の入力、使用中のすべてのブランチ識別子のリストが必要です)。したがって、それは単体テストでもありません。

関連するインターフェイスの定義やドキュメントは表示されませんが、ユニットが故障していないため、ユニットテストでエラーを検出できなかった可能性があります。ユニットがブランチ識別子が数字のみで構成されていると想定することが許可されており、開発者がコードがそうでない場合に何をすべきかを決定したことがない場合、彼らはすべきではありません数字以外の識別子の場合に特定の動作を強制する単体テストを作成します。これは、英数字ブランチ識別子を正しく処理したユニットの仮想の有効な実装をテストが拒否するためです。将来の実装と拡張。または、40年前に書かれた1つの文書は、10Bは実際には089から100の間にあるため、テスト識別子であると暗黙的に定義されています(生のEBCDICの辞書編集範囲を介して)。 15年前、誰かがそれを実際の識別子として使用することを決めたため、「欠陥」は元の定義を正しく実装するユニットにはありません。10Bがテスト識別子として定義されているため、ブランチに割り当てるべきではないことに気づかなかったプロセスにあります。089-100をテスト範囲として定義し、識別子10 $または1.0を導入すると、ASCIIでも同じことが起こります。EBCDICでは、文字の後に数字が来ることがあります。

一つのユニットテスト(またはおそらく機能テスト)が考えられます1日を節約できたかもしれませんが、新しいブランチ識別子を生成または検証するユニットのテストです。このテストは、識別子に数字のみが含まれている必要があることを表明し、ブランチ識別子のユーザーが同じ数字を想定できるようにするために記述されます。または、実際のブランチ識別子をインポートしても、テスト識別子は表示されず、すべてのテスト識別子を拒否するようにユニットテストできるユニットがあるかもしれません(識別子が3文字のみの場合、すべてを列挙し、動作を比較できます)テストフィルターのバリデーターが一致することを確認します。これにより、スポットテストに対する通常の異議が処理されます。次に、誰かがルールを変更したとき、ユニットテストは新しく必要な動作と矛盾するため失敗します。

テストは正当な理由で行われたため、ビジネス要件の変更により削除する必要があるポイントは、誰かが仕事を与えられる機会になります。「コードの中で、私たちがしたい動作に依存するすべての場所を見つける変化する"。もちろん、これは難しく、したがって信頼性が低いため、1日の節約を保証するものではありません。あなたがの性質を想定しているユニットのテストであなたの仮定をキャプチャした場合でも、あなたは自分にチャンスを与えてくれたので、努力がされていない完全に無駄になります。

もちろん、ユニットが「面白い形の」入力で最初に定義されていなかった場合、テストするものは何もないことに同意します。面倒な名前空間の分割は、あなたの面白い定義を実装することに困難がなく、誰もがあなたの面白い定義を理解し、尊重することを確認することにあるため、適切にテストするのが難しい場合があります。これは、1つのコード単位のローカルプロパティではありません。さらに、一部のデータ型を「数字の文字列」から「英数字の文字列」に変更することは、ASCIIベースのプログラムでUnicodeを処理することに似ています。コードが元の定義に強く結合している場合、データ型は、プログラムが行うことの基本であり、多くの場合、強く結合されます。

それは主に無駄な努力だと思うのは少し不安です

ユニットテストが時々失敗する場合(リファクタリング中など)、そうすることで有用な情報(たとえば、変更が間違っている)が提供されれば、労力は無駄になりませんでした。彼らがしないことは、システムが動作するかどうかをテストすることです。したがって、機能テストや統合テストを行う代わりに単体テストを作成している場合、時間を最適に使用していない可能性があります。


アサーションは良いです!

3
@nocomprende:レーガンがそうであったように、「信頼するが、検証する」。
スティーブジェソップ

1
また、「ユニットテストが悪い!」と言っていました。しかし、私はほとんどの人が動物農場への言及を見逃し、私が言っていることを考えるのではなく実際にを批判し始めると思った(膝関節の反応は効果的ではない)が、私はそれを言わなかった。たぶん、より賢く、より博識な人がその点を指摘できます。

2
「すべてのテストに合格していますが、一部のテストは他のテストよりも合格しています!」
グラハム

1
テストは赤いニシンです。これらの人たちは、「分岐コード」がどのように定義されているのか知らなかった。これは、米国郵便局が4桁を追加したときに郵便番号の定義を変更していることを知らないようなものです。
レーダーボブ

120

単体テストでは、ブランチコード10Bおよび10Cが「テストブランチ」として誤って分類されていることを検出できましたが、そのブランチ分類のテストがそのエラーを検出するのに十分な広さだったとは思われません。

一方、生成されたレポートのスポットチェックにより、バグが存在するようになった15年よりもずっと早く、分岐10Bおよび10Cが一貫してレポートから欠落していることが明らかになった可能性があります。

最後に、これは、1つのデータベースでテストデータと実際の運用データを混在させるのが悪い考えである理由を示しています。テストデータを含む別のデータベースを使用していた場合、公式レポートから除外する必要はなく、除外することは不可能でした。


80
1つのユニットテストは、(テストや実際のデータをミキシングなど)が悪い設計上の決定を補うことはできません
Jeutnarg

5
テストデータと実際のデータを混在させないことが最善ですが、実際のデータを変更する必要がある場合、運用システムを検証するのは困難です。たとえば、本番環境で銀行口座の合計を変更して銀行システムを検証することは悪い考えです。コード範囲を使用して意味を指定することには問題があります。レコードのより明示的な属性は、おそらくより良い選択でした。
ジミージェームズ

4
@Voo実際に展開された本番システムをテストする価値があるまたは必要であると見なされる場合、ある程度の複雑さまたは信頼性の要件が存在するという暗黙の仮定があると思います。(構成変数が間違っているために、どれだけ間違っている可能性があるかを検討してください。)これは、大規模な金融機関の場合であることがわかりました。
jpmc26

4
@Voo私はテストについて話していません。私はシステムの検証について話している。実際の本番システムでは、コードとは関係なく、失敗する多くの方法があります。新しい銀行システムを実稼働環境に導入する場合、dbやネットワークなどに問題があり、取引がアカウントに適用されない可能性があります。私は銀行で働いたことはありませんが、偽のトランザクションで実際のアカウントを変更し始めることは嫌いだと確信しています。そのため、偽のアカウントを設定するか、しばらく待ってから祈ることになります。
ジミージェームズ

12
@JimmyJamesヘルスケアでは、実稼働データベースをテスト環境に定期的にコピーして、できるだけ実際に近いデータでテストを実行することが一般的です。銀行でも同じことができると思います。
dj18

75

ソフトウェアは特定のビジネスルールを処理する必要がありました。単体テストがある場合、単体テストは、ソフトウェアがビジネスルールを正しく処理したことを確認していました。

ビジネスルールが変更されました。

どうやら、ビジネスルールが変更されたことに誰も気づかず、新しいビジネスルールを適用するためにソフトウェアを変更した人はいませんでした。単体テストがあった場合、それらの単体テストを変更する必要がありますが、ビジネスルールが変更されたことに誰も気付かなかったため、誰もそれを実行しませんでした。

そのため、ユニットテストではそれを把握できませんでした。

例外は、単体テストとソフトウェアが独立したチームによって作成され、単体テストを行うチームが新しいビジネスルールを適用するためにテストを変更した場合です。その場合、単体テストは失敗し、ソフトウェアの変更が発生することを期待していました。

もちろん、単体テストではなくソフトウェアのみが変更された場合、同じ場合、単体テストも失敗します。単体テストが失敗するたびに、それはソフトウェアが間違っているという意味ではなく、ソフトウェアまたは単体テスト(時には両方)が間違っていることを意味します。


2
あるチームがコードに取り組んでおり、別のチームが「ユニット」テストに取り組んでいる別のチームを持つことは可能ですか?どうすればそれも可能ですか?...私はいつもコードをリファクタリングしています。
セルジオ

2
@Sergioはある観点から、リファクタリングは動作を維持しながら内部を変更します。したがって、内部に依存せずに動作をテストする方法でテストが記述されている場合、更新する必要はありません。
デニーズ

1
これは何度も起こります。ソフトウェアは苦情なしに生産されており、突然のユーザーはすべて、それが機能しなくなり、長年にわたって徐々に失敗しているという苦情で爆発します。それは...あなたは、標準の通知プロセスに従わずに、あなたの内部手続きを行って、変更することを決定したときに何が起こるかだ
ブライアンKnoblauch

42
「変更されたビジネスルール」は重要な観察です。単体テストは、ロジックが正しいことではなく、実装したと考えているロジックを実装したことを検証します。
ライアンキャバノー

5
私が何が起こったかについて正しいなら、これをキャッチする単体テストが書かれることはまずありません。テストを選択するための基本原則は、「良い」ケース、「悪い」ケース、および境界を囲むケースをテストすることです。この場合、「099」、「100」、および「101」をテストします。「10B」は古いシステムで「非数字を拒否する」テストでカバーされており、新しいシステムで101を超えている(したがってテストでカバーされている)ので、テストする理由はありません。 EBCDIC、「10B」は「099」と「100」の間でソートされます。
マーク

29

いいえ。これは、単体テストの大きな問題の1つです。これらは、あなたを誤った安心感に陥らせます。

すべてのテストに合格しても、システムが正常に機能しているわけではありません。すべてのテストに合格していることを意味します。それは、あなたが意識してテストを書いたあなたのデザインの部分が、あなたが意図的に思った通りに機能していることを意味します。とにかく、とにかくあなたはそれを正しくした可能性が高いです!しかし、あなたがそれらのテストを書くことを考えたことがないので、あなたがそれらのような、あなたが考えなかったケースをキャッチすることは何もしません (もしあれば、それはコードの変更が必要であることを知っていたでしょう、そしてあなたはそれらを変更したでしょう。)


17
父は私に次のように尋ねていました。(彼が「あなたが知らない場合、尋ねなさい!」と言うことによってそれを混乱させるのが常であった)しかししかしどうすれば私が知らないことを知っているか。

7
「それは、あなたがデザインについて意識的に考え、テストを書いた部分が、あなたが意図的に思ったように機能していることを意味します。」その通りです。この情報は、リファクタリングを行っている場合、またはシステムのどこかで変更が行われ、想定を破る場合に非常に貴重です。誤ったセキュリティ感覚に落ち着いた開発者は、単体テストの制限を理解していないだけですが、単体テストが役に立たないツールになっているわけではありません。
ロバートハーベイ

12
@MasonWheeler:あなたと同じように、著者はユニットテストがあなたのプログラムが機能することを何らかの形で証明することになっていると考えています。そうではありません。繰り返しますが、単体テストではプログラムが機能することを証明しません。 単体テストは、メソッドがテスト契約を満たしていることを証明し、それがすべてです。残りの論文は、その単一の無効な前提に基づいているため、落ちます。
ロバートハーヴェイ

5
当然、そのような誤った信念を持っている開発者は、ユニットテストが完全に失敗した場合に失望しますが、それはユニットテストではなく開発者の責任であり、ユニットテストが提供する真の価値を無効にするものではありません。
ロバートハーヴェイ

5
o_O @最初の文。単体テストでは、誤った安心感が得られますが、ハンドルを握るなどのコーディングは、運転中に誤った安心感を与えます。
-djechlin

10

いいえ、必ずしもそうではありません。

元の要件は数値分岐コードを使用することでしたので、さまざまなコードを受け入れ、10Bのようなものを拒否したコンポーネントに対して単体テストが作成されていました。システムは動作しているものとして渡されていたはずです(動作していました)。

次に、要件が変更され、コードが更新されますが、これは不良データ(現在は良好なデータ)を提供するユニットテストコードを変更する必要があることを意味します。

システムを管理する人々はこれが事実であることを知っており、新しいコードを処理するために単体テストを変更するでしょう...しかし、彼らがそれが起こっていることを知っていたなら、彼らはこれらを処理するコードを変更することも知っていたでしょうとにかく..そして彼らはそれをしませんでした。元々コード10Bを拒否した単体テストは、そのテストを更新することを知らなかった場合、実行時に「ここですべては問題ありません」と喜んで言うでしょう。

単体テストは、元の開発には適していますが、システムテストには適していません。特に、要件が長く忘れられてから15年はかかりません。

この種の状況で必要なのは、エンドツーエンドの統合テストです。動作するはずのデータを渡し、動作するかどうかを確認できる場所。誰かが、新しい入力データではレポートが作成されないことに気づき、さらに調査します。


スポットオン。そして、単体テストの主な(唯一の?)問題。私は正確に同じことを言っているだろうと、私自身の答えは私の文言保存:)(おそらくより悪い!)
明度レース軌道に

8

型テスト(ランダムに生成された有効なデータを使用して不変条件をテストするプロセス。HaskellテストライブラリQuickCheckや、他の言語でそれに触発されたさまざまなポート/代替が例として挙げられます) 。

これは、ブランチコードの有効性のルールが更新されたときに、それらが正しく機能することを確認するために特定の範囲をテストすることを誰も考えなかったためです。

しかし、型式試験が使用されていた場合、誰かが必要があり、元のシステムが実施された時の特性の組を書かれている、一つは試験分岐のための特定のコードがない他のコードいることを確認するテストデータと一つとして扱われたことを確認します分岐コードのデータ型定義が更新されたとき(数字から数値への分岐コードの変更のいずれかが機能することをテストするために必要だった)、このテストは値のテストを開始していました新しい範囲であり、ほとんどの場合、障害を特定しているでしょう。

もちろん、QuickCheckは1999年に最初に開発されたため、この問題をキャッチするには遅すぎました。


1
このプロパティベースのテストを呼び出すことはより普通だと思います、そしてもちろん、この変更が与えられた場合でもパスするプロパティベースのテストを書くことは可能な限りです(あなたはそれを見つけることができるテストを書く可能性が高いと思いますが)
jk。

5

ユニットテストがこの問題に影響を与えることは本当に疑わしい。新しい分岐コードをサポートするために機能が変更されたため、トンネルビジョンの状況の1つのように聞こえますが、これはシステムのすべての領域で実行されませんでした。

ユニットテストを使用してクラスを設計します。単体テストの再実行は、設計が変更された場合にのみ必要です。特定のユニットが変更されない場合、未変更のユニットテストは以前と同じ結果を返します。単体テストでは、他のユニットへの変更の影響を示すことはできません(そうする場合は、単体テストを作成していません)。

この問題は、次の方法でのみ合理的に検出できます。

  • 統合テスト-ただし、システム内の複数のユニットにフィードする新しいコード形式を具体的に追加する必要があります(つまり、元のテストに有効なブランチが含まれている場合にのみ問題が表示されます)
  • エンドツーエンドのテスト-ビジネスは、古いブランチコード形式と新しいブランチコード形式を組み込んだエンドツーエンドテストを実行する必要があります

十分なエンドツーエンドのテストがないことは、より心配です。システム変更のONLYまたはMAINテストとして単体テストに依存することはできません。新たにサポートされたブランチコード形式でレポートを実行する必要があるのは誰かのようです。


2

ランタイムに組み込まれたアサーションが助けになったかもしれません。例えば:

  1. のような関数を作成する bool isTestOnly(string branchCode) { ... }
  2. この機能を使用して、除外するレポートを決定します
  3. アサーション、ブランチ作成コードでその関数を再利用して、このタイプのブランチコードを使用してブランチが作成されていない(作成できない)ことを確認またはアサートします!
  4. このアサーションを実際のランタイムで有効にします(「デバッグ専用の開発者バージョンのコードを除き、最適化されていない」)!

こちらもご覧ください:


2

これの要点は、Fail Fastです。

コードがなく、コードに応じてテスト分岐プレフィックスであるかどうかを示すプレフィックスの例もありません。これだけです:

  • 089-100 =>テストブランチ
  • 10B、10C =>テストブランチ
  • <088 =>おそらく実際のブランチ
  • > 100 =>おそらく実際のブランチ

コードが数字と文字列を許可しているという事実は、少し奇妙です。もちろん、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の細かい当惑なしに問題を迅速に見つけることができます。


1

非常に多くの答えがあり、ダイクストラの引用も1つではありません。

テストでは、バグの存在ではなく存在を示しています。

したがって、それは依存します。コードが適切にテストされていれば、おそらくこのバグは存在しません。


-1

ここでの単体テストでは、そもそも問題が存在しないことを確認できたと思います。

あなたがbool IsTestData(string branchCode)関数を書いたと考えてください。

最初に記述するユニットテストは、nullと空の文字列に対して行う必要があります。次に、文字列の長さが正しくない場合、非整数文字列の場合。

これらのすべてのテストに合格するには、関数にパラメーターチェックを追加する必要があります。

10Aの可能性を考えずに「良い」データ001-> 999のみをテストする場合でも、パラメータのチェックにより、英数字の使用を開始するときに、例外がスローされるのを避けるために関数の書き換えが強制されます


1
これは役に立たなかったでしょう-関数は変更されず、同じテストデータが与えられてもテストは失敗し始めませんでした。誰かがテストを失敗させるためにテストを変更することを考えなければならなかっただろうが、もし彼らがそれを考えていたら、彼らはおそらく同様に機能を変更することも考えていただろう。
ハルク

(または、「パラメーターチェック」の意味がわからないため、おそらく何かが欠けています)
ハルク

関数は、単純なエッジケースユニットテストに合格するために、整数以外の文字列に対して例外をスローするように強制されます。したがって、英数字ブランチコードを具体的にプログラミングせずに使用し始めた場合、量産コードはエラーになります
ユアン

しかし、IsValidBranchCodeこのチェックを実行するために、関数は何らかの-関数を使用していないでしょうか?そして、この関数はおそらくIsTestData?を変更する必要なく変更されていただろう。したがって、「良いデータ」のみをテストしている場合、テストは役に立たなかったでしょう。エッジケーステストでは、失敗を開始するために、現在有効な分岐コード(まだ無効な分岐コードだけでなく)を含める必要があります。
ハルク

1
チェックがIsValidCodeにある場合、関数が独自の明示的なチェックなしでパスする場合、はい、それを逃す可能性があります。テスト番号」
ユアン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.