web-dev-qa-db-ja.com

例外メッセージの表現をテストする

コードレビューを行っているときに、ユニットテストで次のアサーションに遭遇しました。

assertThatThrownBy(() -> shoppingCartService.payForCart(command))
  .isInstanceOfSatisfying(PaymentException.class, 
    exception -> assertThat(exception.getMessage()).isEqualTo(
      "Cannot pay for ids [" + item.getId() +"], status is not WAITING"));

例外のリテラルテキストをテストすることは悪い習慣だと思いますが、著者を納得させることができません。 (注:これはローカライズされたテキストではなく、アイテムIDをパラメーターとして持つコード内の文字列リテラルです。)私の推論:

  • それを決定した場合、たとえばアイテムのIDは重要な情報でした。メッセージにIDが含まれていることをテストすることは理にかなっています。ただし、この場合は、例外クラスのフィールドとしてIDを含めることをお勧めします。
  • ログを自動的に読み取り、特定の文字列を検索するある種の外部システムがある場合(そうではありません)、はい、このテストは正当化されます。
  • メッセージで重要なのは、問題が何であるかを明確に伝えることです。しかし、そのようなメッセージを作成する方法は何百もあるはずであり、テストライブラリでは、プレーンテキストの人間の言語の文字列が「明確」または「有用」であるかどうかを判断できません。
  • したがって、これは、例えば、翻訳用の単体テストを作成する-最善の方法は、テストでメッセージファイルを複製することになるので、無意味です。

ここでのベストプラクティスは何ですかそしてその理由

33
Kricket

例外のリテラルテキストをテストすることは悪い習慣だと思います

なし例外メッセージの表現を解釈しようとします。これらは、誰かが変更するのが簡単すぎるほど簡単です。

特定の例外のハンドラーが特定のデータを必要とする場合は、これらをカスタム例外クラスのカスタムプロパティとして追加する必要があります(ハンドラーは特にキャッチする可能性があります)。処理コードは、必要な情報を明確かつ確実に取得できます。

メッセージhappensに同じ情報のtextual表現を含める場合は問題ありませんが、アプリケーションコードmust notはそれに依存します。

exceptionクラスのフィールドとしてIDを含めることをお勧めします

はいぜったいに。

ログを自動的に読み取る何らかの外部システムがあった場合

別のシステムで検査されていたり、別のユーザーグループが利用できるようにした例外メッセージにデータを入れていた場合は、送信するデータを正確に[非常に]注意してください-誰かの名前と日付を「記録」同じ例外メッセージで出産すると、最近[非常に]お湯に入る可能性があります。 (GDPR)

メッセージで重要なことは、問題が何であるかを明確に伝えることです

次の場合のみそのメッセージはユーザーの前に表示されます。

私にとって、構造化例外処理はallHandlingについてです-各例外で「有用な」何かをしています。このコンテキストでは、アプリケーションをユーザーの前で放棄して「あきらめる」ことは、「便利」の非常に緩い定義です。最後の手段。

理想的には、[コード]で例外を処理し、ユーザーが気付く前に例外を「非表示」にすることが理想的です。

33
Phill W.

例外メッセージの内容をテストする主な目的は、right例外がスローされることを確認することです。

payForCart()PaymentExceptionをスローする理由はいくつかあります。

つまり、正確な表現ではなく、「ID A9dr6Lの支払いができない、ステータスがWAITINGではない」、「ID A9dr6Lの支払いができない、残高が不足している」ではないかどうかです。代わりに、throwステートメントごとに異なる例外クラスを使用することもできます。

55

私はあなたの要点を理解しています。また、例外メッセージを比較しないようにしています。 Michael Borgwardtがすでに述べたように、専用の例外タイプが役立つ場合があります。

ただし、すべての種類の例外に例外タイプを追加することは実用的ではないため、明示的にチェックする例外タイプの数と、それが全体的な設計にどのように適合するかによって異なります。

個人的には、妥協案を解決し、メッセージテキストの同等性をチェックするのではなく、メッセージcontains特定のキーワードまたはフレーズかどうかをチェックします。 「ステータスはWAITINGではありません。」

ベストプラクティスについて、必要な情報で例外タイプPaymentExceptionを拡張することをお勧めします。たとえば、可能な値(ReasonInvalidStatusなど)を持つ列挙型のプロパティInsufficientFundsを追加します。このようにして、エラー処理コードに影響を与えることなく例外メッセージをローカライズおよび変更でき、例外タイプの数は管理可能なままであり、コードのスローと処理の間の最終的な不一致はコンパイル時に検出されます。

7
helb

メッセージがコードにとって重要ではないが、トラブルシューティングプログラマにとって重要である状況があります。メッセージからIDを解析するコードを追加したくない場合もありますが、プログラマが最初に知りたいのは、その失敗のトラブルシューティングでどのIDが失敗したかを知ることです。

つまり、エラーメッセージに特定の情報が含まれていることは、要件の一部である場合があります。エラーメッセージが必要な場合は、少なくとも他の種類の出力をテストするのと同じレベルでテストする必要があります。ログに「IDの支払いができませんItemArray@0xdeadbeef "?

5
Karl Bielefeldt

例によるテスト駆動開発 で、ケントベックは「2つの単純なルール」を導入しました。それらのルールの最初

anyコードを記述する前に、失敗した自動テストを記述します

(強調が追加されました。)

例外の構成とそのメッセージは確かにコードとしてカウントされるため、このルールが適用されることを受け入れると、必ず一致するテストが存在するはずです。

Michael Feathersは レガシーコードを効果的に使用する で、優れたユニットテストにはこれらの資質があると書いています

  1. 彼らは速く走ります
  2. 問題を特定するのに役立ちます

ここで説明するテストは、明らかに速度の問題ではありません(payForCartよりも明らかに高価ではありません)。実際には、同じ情報のメモリ表現の違いだけを調べています。

「問題のローカライズにご協力ください」というのはちょっと面白いです。実際には、ここで評価される異なる決定がたくさんあります。これは、この設計がより多くのモジュールを必要とすることを示している可能性があります( Parnas 1971 を参照)。

つまり、正しい例外がスローされたことを確認するテストを行うことができます。

assertThatThrownBy(() -> shoppingCartService.payForCart(command))
    .isEqualTo(
        PaymentException.forItem(
            item
        )
    );

そして、他の場所で他のテストを記述して、PaymentException :: forIdが適切に動作することを確認します。

もちろん、このように分割することは、テストと設計の両方でmoreの投資を意味します。そして、あなたは投資された努力が返されたビジネス価値によってバランスをとられることを確実にしたいのです。

メッセージで重要なのは、問題が何であるかを明確に伝えることです。しかし、そのようなメッセージを作成する方法は何百もあるはずであり、テストライブラリでは、プレーンテキストの人間の言語の文字列が「明確」または「有用」であるかどうかを判断できません。

ああ-あなたはマイケル・ボルトンとジェームズバックのいくつかの読書を テストvsチェック にしたいかもしれません。ここにある自動化されたものはcheck;です。今日の動作がプログラマーによって文書化された動作と一致することの自動化された確認です。

testは、人間(プログラマーであれ、レビュアーであれ)がメッセージを見て、それが明確か有用かを判断するときです。

それがあなたが測定したいものであるなら、私たちはおそらくテストを少し異なるように書くでしょう

assertThatThrownBy(() -> shoppingCartService.payForCart(command))
    .isInstanceOfSatisfying(
        PaymentException.class,
        exception -> 
            assertThat(
                 exception.getMessage()
            )
            .isEqualTo(
                "Cannot pay for ids [12345], status is not WAITING"
            )
    );

この場合、意図的な動作をより明確にするために意図的にテストに複製を追加し、レビューでの評価を容易にします。

同じアイデアを表現する別の方法。人間は柔軟なパターンマッチングマシンですが、マシンはそうではありません。したがって、これら2つの異なる制約を考慮して、インターフェースを設計する必要があります。これは、やや異なるテスト設計を示唆しています(テストはでない柔軟なパターンマッチングマシンであるため)。

assertThatThrownBy(() -> shoppingCartService.payForCart(command))
    .isInstanceOfSatisfying(
        PaymentException.class,
        exception -> 
            assertThat(
                 PaymentException.cast(exception).ids()
            )
            .isEqualTo(
                List.of(12345)
            )
    );

そして、人間が読めるメッセージのチェックを別の場所に実装します。

メッセージファイル自体が、観察可能な動作を損なうことなく変更したい実装の詳細である限り、「テストでメッセージファイルを複製する」ことは特にマイナス面とは見なしません。これは特に、マシンが使用するメッセージファイルがテンプレートの束になる場合に当てはまります。つまり、人間の可読性を犠牲にしています。

実際には、レビュー中にこの例をコメントなしでスライドさせます。はい、もっと良いかもしれませんが、害を及ぼす可能性は低いです

  • これはテストコードであるため、本番環境にまったく影響を与えません。
  • テストは高速で分離されているため、開発プロセスに追加の抵抗が加えられることはありません
  • 例外メッセージングの動作を頻繁に変更することはおそらくないので、特定の動作を過度に調整しても、ドラッグが追加されることはほとんどありません。

要するに、実装されたテストはプラトニックの理想を達成しませんが、ほとんど無害であり、私たちの注意は他の場所に投資するほうがよいです。

1
VoiceOfUnreason

この重要な情報は、例外として単に文字列として保存されるべきではないと私は主張します。例外はこの情報をフィールドとして保持する必要があります。これにより、例外をキャッチするすべてのコードがメッセージ文字列を解析せずにアクセスできるようになります。

public class PaymentException extends RuntimeException {
    private int itemID;
    private Reason reason;

    public static enum Reason {
        INCORRECT_STATUS, NOT_ENOUGH_FUNDS, BAD_PAYMENT_METHOD, ITEM_NOT_FOUND;
    }

    public PaymentException(int itemID, Reason reason) {
        super("Error processing payment for item #"+itemID+": "+reason);
        this.itemID = itemID;
        this.reason = reason;
    }

    public int getItemID() {
        return itemID;
    }

    public Reason getReason() {
        return reason;
    }
}
1
vikarjramun

tl; dr例外メッセージを比較する代わりに、コードは同じコンストラクタから重複例外を生成し、次に呼び出す必要があります一貫性を確認するための重複例外。


例外メッセージのテストは薄っぺらに見えます。

代わりに:

  1. 例外のパブリッククラスを定義します。

  2. スローアーがスローしたい場合、スローする例外を生成するために標準コンストラクターを使用する必要があります。

  3. キャッチャーがチェックしたいとき、それはしなければなりません:

    1. 同じ標準コンストラクターを使用して、重複例外を生成します。

    2. キャッチされた例外と重複した例外を比較するには、標準の等価性/一貫性チェック方法を使用します。


これは基本的に、コードが既に実行しているものと同じですが、次の点が異なります。

  1. 例外実装の詳細を抽象化します。

    • 以前:スローアーとキャッチャーの両方が、同じ例外メッセージを構成する方法の低レベルの詳細を知る必要がありました。

    • 後:スローアーとキャッチの両方が同じ例外コンストラクターを呼び出す方法を知っている必要があります。

  2. コードはより変更可能です。

    • 以前:例外メッセージが変更される場合、メンテナーはそれを参照するすべてのスローアーとキャッチャーを追跡し、それらを同じ方法で更新する必要があります。

    • 後:例外メッセージが変更される場合、メンテナは一度だけ変更できます。


討論

私には、コードがすでに重複例外を生成していて、等価性をチェックしているように見えます。

ロジックは次のようです。

  1. Duplicate-expressionを生成します。

  2. Duplicate-expressionをシリアル化します(つまり、例外メッセージを取得します)。

  3. キャッチされた式をシリアル化します(つまり、例外メッセージを取得します)。

  4. シリアル化を比較して、等しいかどうかを確認します。

しかし、これらをオブジェクト指向のロジックに入れる代わりに、これらのステップはすべて、すべての呼び出しサイトにインライン化されます。このマニュアルのインライン化は、コードの匂いにつながります。

Exceptionを生成して、それを(投げるのではなく)単にチェックするだけでは不思議に思うかもしれませんが、何も問題はありません。

1
Nat
assertThatThrownBy(() -> shoppingCartService.payForCart(command))
        .isInstanceOf(PaymentException.class)
        .hasMessage("Cannot pay for ids [" + item.getId() + "], status is not WAITING");

とにかく、メッセージをテストするためのよりAssertJ風の方法でしょう...

何かが入っていることを確認するだけで、メッセージ全体は気にしない場合は、hasMessageContainingを使用することもできます

0
Kevin Van Dyck