例によるテスト

公開: 2022-02-15

テストに関連するすべてについて、一連のブログを続けています。 このブログでは、実際の例に焦点を当てています。

この投稿の例はJUnit5とAssertJを使用して記述されていますが、レッスンは他の単体テストフレームワークにも適用できます。

JUnitは、Javaで最も人気のあるテストフレームワークです。 AssertJは、開発者がより表現力豊かなテストを作成するのに役立つJavaライブラリです。

基本的なテスト構造

ここで取り上げるテストの最初の例は、2つの数値を加算するための単純な計算機です。

 クラスCalculatorShould{

     @Test // 1
     void sum(){
         電卓calculator=new Calculator(); // 2
         int結果=calculator.sum(1、2); // 3
         assertThat(result).isEqualTo(3); // 4
     }
}

テストを作成するときは、すべてのメソッド名でshouldまたはtestを繰り返さないように、 ClassShould命名規則を使用することをお勧めします。 あなたはここでそれについてもっと読むことができます。

上記のテストは何をしますか?

テストを1行ずつ分割してみましょう。

  1. @Testアノテーションにより、JUnitフレームワークはどのメソッドがテストとして実行されることを意図しているかを知ることができます。 テストではないprivateメソッドがテストクラスにあるのは完全に正常です。
  2. これは、テスト環境を準備するテストの調整フェーズです。 このテストに必要なのは、 Calculatorインスタンスを用意することだけです。
  3. これは、テストする動作をトリガーする動作フェーズです。
  4. これは、何が起こったかを検査し、すべてが期待どおりに解決したかどうかを検査するアサートフェーズです。 assertThat(result)メソッドはAssertJライブラリの一部であり、複数のオーバーロードがあります。

各オーバーロードは、特殊なAssertオブジェクトを返します。 返されるオブジェクトには、 assertThatメソッドに渡したオブジェクトにとって意味のあるメソッドが含まれています。 この場合、そのオブジェクトは、整数をテストするためのメソッドを持つAbstractIntegerAssertです。 isEqualTo(3)は、 result == 3かどうかをチェックします。 そうである場合、テストは合格し、そうでない場合は失敗します。

このブログ投稿では、実装に焦点を当てません。

ArrangeActAssertについての別の考え方は、 GivenWhenThenです。

sum実装を記述した後、いくつかの質問をすることができます。

  • このテストをどのように改善できますか?
  • カバーすべきテストケースは他にもありますか?
  • 正の数と負の数を追加するとどうなりますか? 2つの負の数? 1つはポジティブでもう1つはネガティブ?
  • 整数値をオーバーフローした場合はどうなりますか?

これらのケースを追加して、既存のテスト名を少し改善してみましょう。

実装ではオーバーフローを許可しません。 sumがオーバーフローした場合、代わりにArithmeticExceptionをスローします。

 クラスCalculatorShould{

     プライベート電卓calculator=new Calculator();

     @テスト
     void sumPositiveNumbers(){
         int sum =calculator.sum(1、2);
         assertThat(sum).isEqualTo(3);
     }

     @テスト
     void sumNegativeNumbers(){
         int sum =calculator.sum(-1、-1);
         assertThat(sum).isEqualTo(-2);
     }

     @テスト
     void sumPositiveAndNegativeNumbers(){
         int sum =calculator.sum(1、-2);
         assertThat(sum).isEqualTo(-1);
     }

     @テスト
     void failWithArithmeticExceptionWhenOverflown(){
         assertThatThrownBy(()->calculator.sum(Integer.MAX_VALUE、1))
             .isInstanceOf(ArithmeticException.class);
     } 

}

JUnitは、各@Testメソッドを実行する前に、 CalculatorShouldの新しいインスタンスを作成します。 つまり、各CalculatorShouldには異なるcalculatorがあるため、すべてのテストでインスタンス化する必要はありません。

shouldFailWithArithmeticExceptionWhenOverflownテストは、異なる種類のアサーションを使用assertます。 コードの一部が失敗したことを確認します。 assertThatThrownByメソッドは、提供されたラムダを実行し、失敗したことを確認します。 すでに知っているように、すべてのassertThatメソッドは特殊なAssertを返し、どのタイプの例外が発生したかを確認できます。

これは、期待どおりにコードが失敗することをテストする方法の例です。 いずれかの時点でCalculatorをリファクタリングし、オーバーフロー時にArithmeticExceptionをスローしない場合、テストは失敗します。

ObjectMotherデザインパターン

次の例は、Personインスタンスが有効であることを確認するためのバリデータークラスです。

 クラスPersonValidatorShould{

    private PersonValidator validator = new PersonValidator();

    @テスト
    void failWhenNameIsNull(){
        Person person = new Person(null、20、new Address(...)、...);

        assertThatThrownBy(()-> validator.validate(person))
            .isInstanceOf(InvalidPersonException.class);
    }

    @テスト
    void failWhenAgeIsNegative(){
        Person person = new Person( "John"、-5、new Address(...)、...);

        assertThatThrownBy(()-> validator.validate(person))
            .isInstanceOf(InvalidPersonException.class);

    }
}

ObjectMotherデザインパターンは、インスタンス化の詳細をテストから隠すために複雑なオブジェクトを作成するテストでよく使用されます。 複数のテストで同じオブジェクトが作成されても、異なるものがテストされる場合があります。

テスト#1はテスト#2と非常によく似ています。 検証をプライベートメソッドとして抽出し、不正なPersonインスタンスを渡して、すべてが同じように失敗することを期待して、 PersonValidatorShouldをリファクタリングできます。

 クラスPersonValidatorShould{

     private PersonValidator validator = new PersonValidator();

     @テスト
     void failWhenNameIsNull(){
         shouldFailValidation(PersonObjectMother.createPersonWithoutName());
     }

     @テスト
     void failWhenAgeIsNegative(){
         shouldFailValidation(PersonObjectMother.createPersonWithNegativeAge());
     }

     private void shouldFailValidation(Person invalidPerson){
         assertThatThrownBy(()-> validator.validate(invalidPerson))
             .isInstanceOf(InvalidPersonException.class);
   
     }
 }

ランダム性のテスト

コードのランダム性をどのようにテストする必要がありますか?

ランダムなPersonインスタンスを生成するためのgenerateRandomを持つPersonGeneratorがあるとします。

まず、次のように記述します。

 クラスPersonGeneratorShould{

     プライベートPersonGeneratorジェネレーター=newPersonGenerator();

     @テスト
     void generateValidPerson(){
         Person person = generator.generateRandom();
         assertThat(person)。
    }
}

そして、私たちは自分自身に尋ねるべきです:

  • ここで何を証明しようとしていますか? この機能は何をする必要がありますか?
  • 生成された人がnull以外のインスタンスであることを確認する必要がありますか?
  • ランダムであることを証明する必要がありますか?
  • 生成されたインスタンスはいくつかのビジネスルールに従う必要がありますか?

依存性注入を使用してテストを簡略化できます。

 パブリックインターフェイスRandomGenerator{
     文字列generateRandomString();
     int generateRandomInteger();
}

PersonGeneratorには、そのインターフェースのインスタンスも受け入れる別のコンストラクターがあります。 デフォルトでは、java.Randomを使用してランダムな値を生成するJavaRandomGenerator実装を使用しjava.Random

ただし、テストでは、別のより予測可能な実装を作成できます。

 @テスト
 void generateValidPerson(){
     RandomGenerator randomGenerator = new PredictableGenerator( "John Doe"、20);
     PersonGeneratorジェネレーター=newPersonGenerator(randomGenerator);
     Person person = generator.generateRandom();
     assertThat(person).isEqualTo(new Person( "John Doe"、20));
}

このテストは、 RandomGeneratorが、 PersonGeneratorの詳細に立ち入ることなく、 RandomGeneratorによって指定されたランダムインスタンスを生成することを証明します。

java.Randomをテストしても、 JavaRandomGeneratorの単純なラッパーであるため、実際には何の値も追加されません。 それをテストすることにより、基本的にJava標準ライブラリからjava.Randomをテストすることになります。 明らかなテストを作成しても、追加のメンテナンスにつながるだけで、メリットはほとんどありません。

PredictableGeneratorなどのテスト目的で実装を記述しないようにするには、Mockitoなどのモックライブラリを使用する必要があります。

PredictableGeneratorを作成したとき、実際にはRandomGeneratorクラスを手動でスタブしました。 Mockitoを使用してスタブすることもできます。

 @テスト
 void generateValidPerson(){
     RandomGenerator randomGenerator = mock(RandomGenerator.class);
     when(randomGenerator.generateRandomString())。thenReturn( "John Doe");
     when(randomGenerator.generateRandomInteger())。thenReturn(20);

     PersonGeneratorジェネレーター=newPersonGenerator(randomGenerator);
     Person person = generator.generateRandom();
     assertThat(person).isEqualTo(new Person( "John Doe"、20));
 }

テストを作成するこの方法は、より表現力があり、特定のテストの実装が少なくなります。

Mockitoは、モックとスタブを作成するためのJavaライブラリです。 簡単にインスタンス化できない外部ライブラリに依存するコードをテストする場合に非常に便利です。 これにより、これらのクラスを直接実装せずに、これらのクラスの動作を記述できます。

Mockitoでは、モックを作成して注入するための別の構文を使用して、慣れているものと同様のテストが複数ある場合にボイラープレートを減らすこともできます。

 @ExtendWith(MockitoExtension.class)// 1
 クラスPersonGeneratorShould{

     @モック//2
     RandomGenerator randomGenerator;

     @InjectMocks // 3
     プライベートPersonGeneratorジェネレーター。

     @テスト
     void generateValidPerson(){
         when(randomGenerator.generateRandomString())。thenReturn( "John Doe");
         when(randomGenerator.generateRandomInteger())。thenReturn(20);

         Person person = generator.generateRandom();
         assertThat(person).isEqualTo(new Person( "John Doe"、20));
     }
}

1. JUnit 5は、「拡張機能」を使用してその機能を拡張できます。 この注釈により、注釈を介してモックを認識し、適切に挿入することができます。

2. @Mockアノテーションは、フィールドのモックされたインスタンスを作成します。 これは、テストメソッド本体にmock(RandomGenerator.class)を記述するのと同じです。

3. @InjectMocksアノテーションは、 PersonGeneratorの新しいインスタンスを作成し、 generatorインスタンスにモックを挿入します。

JUnit 5拡張機能の詳細については、こちらを参照してください。

Mockitoインジェクションの詳細については、こちらを参照してください。

@InjectMocksを使用する際の落とし穴が1つあります。 オブジェクトのインスタンスを手動で宣言する必要がなくなる可能性がありますが、コンストラクターのコンパイル時の安全性が失われます。 ある時点で誰かがコンストラクターに別の依存関係を追加した場合、ここでコンパイル時エラーは発生しません。 これは、検出が容易ではないテストの失敗につながる可能性があります。 @BeforeEachを使用して、インスタンスを手動でセットアップすることを好みます。

 @ExtendWith(MockitoExtension.class)
クラスPersonGeneratorShould{

     @モック
     RandomGenerator randomGenerator;

     プライベートPersonGeneratorジェネレーター。

     @BeforeEach
     void setUp(){
         ジェネレータ=newPersonGenerator(randomGenerator);
     }

     @テスト
     void generateValidPerson(){
         when(randomGenerator.generateRandomString())。thenReturn( "John Doe");
         when(randomGenerator.generateRandomInteger())。thenReturn(20);

         Person person = generator.generateRandom();
         assertThat(person).isEqualTo(new Person( "John Doe"、20));
     }
}

時間に敏感なプロセスのテスト

コードの一部はタイムスタンプに依存することが多く、 System.currentTimeMillis()などのメソッドを使用して現在のエポックタイムスタンプを取得する傾向があります。

これは問題ないように見えますが、クラスが内部で決定を下すときに、コードが正しく機能するかどうかをテストして証明することは困難です。 そのような決定の例は、現在の日が何であるかを決定することです。

 クラスIndexerShould{
     プライベートインデクサーインデクサー=新しいインデクサー();
     @テスト
     void generateIndexNameForTomorrow(){
         String indexName = indexer.tomorrow( "my-index");
         //このテストは今日は機能しますが、明日はどうでしょうか?
        assertThat(indexName)
           .isEqualTo( "my-index.2022-02-02");
     }
}

インデックス名を生成する日を「制御」できるようにするには、依存性注入を再度使用する必要があります。

Javaには、このようなユースケースを処理するためのClockクラスがあります。 ClockのインスタンスをIndexerに渡して時間を制御できます。 デフォルトのコンストラクターは、下位​​互換性のためにClock.systemUTC()を使用できます。 System.currentTimeMillis()呼び出しをclock.millis()に置き換えることができるようになりました。

Clockを挿入することで、クラスで予測可能な時間を強制し、より良いテストを作成できます。

ファイル作成方法のテスト

  • 出力をファイルに書き込むクラスをどのようにテストする必要がありますか?
  • これらのファイルをOSで動作させるには、どこに保存する必要がありますか?
  • ファイルがまだ存在していないことを確認するにはどうすればよいですか?

次の例に示すように、ファイルを処理するときに、これらの懸念に自分で対処しようとすると、テストを作成するのが難しい場合があります。 次のテストは、疑わしい品質の古いテストです。 DogToCsvWriterが犬をシリアル化してCSVファイルに書き込むかどうかをテストする必要があります。

 クラスDogToCsvWriterShould{

     プライベートDogToCsvWriterwriter= new DogToCsvWriter( "/ tmp / dogs.csv");
     
     @テスト
     void convertToCsv(){
         writer.appendAsCsv(new Dog(Breed.CORGI、Color.BROWN、 "Monty"));
         writer.appendAsCsv(new Dog(Breed.MALTESE、Color.WHITE、 "Zoe"));

         文字列csv=Files.readString( "/ tmp / dogs.csv");

         assertThat(csv).isEqualTo( "Monty、corgi、brown \ nZoe、maltese、white");
     }
}

シリアル化プロセスは書き込みプロセスから切り離す必要がありますが、テストの修正に焦点を当てましょう。

上記のテストの最初の問題は、Windowsユーザーがパス/tmp/dogs.csvを解決できないため、Windowsでは機能しないことです。 もう1つの問題は、上記のテストの実行時にファイルが削除されないため、ファイルがすでに存在する場合は機能しないことです。 CI / CDパイプラインでは問題なく機能する可能性がありますが、複数回実行するとローカルでは機能しません。

JUnit 5には、フレームワークによって作成および削除される一時ディレクトリーへの参照を取得するために使用できる注釈があります。 一時ファイルを作成および削除するメカニズムはフレームワークごとに異なりますが、考え方は同じです。

 クラスDogToCsvWriterShould{

     @テスト
     void convertToCsv(@TempDir Path tempDir){
         パスdogsCsv=tempDir.resolve( "dogs.csv");
         DogToCsvWriter writer = new DogToCsvWriter(dogsCsv);
         writer.appendAsCsv(new Dog(Breed.CORGI、Color.BROWN、 "Monty"));
         writer.appendAsCsv(new Dog(Breed.MALTESE、Color.WHITE、 "Zoe"));

         文字列csv=Files.readString(dogsCsv);

         assertThat(csv).isEqualTo( "Monty、corgi、brown \ nZoe、maltese、white");
     }
}

この小さな変更により、絶対パスを気にすることなく、上記のテストがWindows、macOS、およびLinuxで機能することが確実になりました。 また、テスト後に作成されたファイルが削除されるため、複数回実行して、毎回予測可能な結果を​​得ることができます。

コマンドとクエリのテスト

コマンドとクエリの違いは何ですか?

  • コマンド:値を返さずに効果を生み出すアクションを実行するようにオブジェクトに指示します(voidメソッド)
  • クエリ:オブジェクトにアクションを実行して結果または例外を返すように要求します

これまで、主に、値を返すメソッドを呼び出したり、動作フェーズで例外をスローしたりするクエリをテストしてきました。 voidメソッドをテストして、他のクラスと正しく相互作用するかどうかを確認するにはどうすればよいですか? フレームワークは、これらの種類のテストを作成するためのさまざまなメソッドのセットを提供します。

これまでにクエリに対して記述したアサーションは、 assertThatで始まりました。 コマンドテストを作成するときは、クエリで行ったようにメソッドの直接の結果を検査しなくなったため、別のメソッドセットを使用します。 私たちのメソッドがシステムの他の部分と行った相互作用を「検証」したいと思います。

 @ExtendWith(MockitoExtension.class)
 クラスFeedMentionServiceShould{

     @モック
     プライベートFeedRepositoryリポジトリ。

     @モック
     プライベートFeedMentionEventEmitterエミッター;

     プライベートFeedMentionServiceサービス。

     @BeforeEach
     void setUp(){
         service = new FeedMentionService(repository、emitter);
     }

     @テスト
     void insertMentionToFeed(){
         long feedId = 1L;
         言及=...;

         when(repository.upsertMention(feedId、言及))
             .thenReturn(UpsertResult.success(feedId、言及));

         FeedInsertionEvent event = new FeedInsertionEvent(feedId、言及);
         言及サービス.insertMentionToFeed(イベント);

         verify(emitter).mentionInsertedToFeed(feedId、言及);
         verifyNoMoreInteractions(emitter);
     }
}

このテストでは、最初にリポジトリをモックして、フィードで言及をアップサートするように求められたときにUpsertResult.successで応答しました。 ここでは、リポジトリのテストには関心がありません。 リポジトリメソッドは、 FeedRepositoryShouldでテストする必要があります。 この動作をあざけることで、実際にはリポジトリメソッドを呼び出しませんでした。 次回呼び出されたときの対応方法を説明しただけです。

次に、 mentionServiceにこのメンションをフィードに挿入するように指示しました。 フィードにメンションが正常に挿入された場合にのみ、結果を出力する必要があることがわかっています。 verifyメソッドを使用することにより、メソッド言及InsertedToFeedが言及とフィードで呼び出され、 mentionInsertedToFeedを使用して再度呼び出されていないことを確認できverifyNoMoreInteractions

最終的な考え

品質テストを書くことは経験から来ます、そして学ぶための最良の方法はすることです。 このブログに書かれているヒントは、実践から来ています。 落とし穴に遭遇したことがなければ、いくつかの落とし穴を見つけるのは困難です。うまくいけば、これらの提案によってコード設計がより堅牢になるはずです。 信頼性の高いテストを行うことで、コードをデプロイするたびに汗をかくことなく、物事を変更する自信が高まります。

Mediatoolkitのチームに参加することに興味がありますか?
シニアフロントエンド開発者のオープンポジションをチェックしてください!