例によるテスト
公開: 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行ずつ分割してみましょう。
-
@Test
アノテーションにより、JUnitフレームワークはどのメソッドがテストとして実行されることを意図しているかを知ることができます。 テストではないprivate
メソッドがテストクラスにあるのは完全に正常です。 - これは、テスト環境を準備するテストの調整フェーズです。 このテストに必要なのは、
Calculator
インスタンスを用意することだけです。 - これは、テストする動作をトリガーする動作フェーズです。
- これは、何が起こったかを検査し、すべてが期待どおりに解決したかどうかを検査するアサートフェーズです。
assertThat(result)
メソッドはAssertJライブラリの一部であり、複数のオーバーロードがあります。
各オーバーロードは、特殊なAssert
オブジェクトを返します。 返されるオブジェクトには、 assertThat
メソッドに渡したオブジェクトにとって意味のあるメソッドが含まれています。 この場合、そのオブジェクトは、整数をテストするためのメソッドを持つAbstractIntegerAssert
です。 isEqualTo(3)
は、 result == 3
かどうかをチェックします。 そうである場合、テストは合格し、そうでない場合は失敗します。
このブログ投稿では、実装に焦点を当てません。
Arrange 、 Act 、 Assertについての別の考え方は、 Given 、 When 、 Thenです。
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のチームに参加することに興味がありますか?
シニアフロントエンド開発者のオープンポジションをチェックしてください!