Javaテストのベストプラクティス





十分なコードカバレッジを確保し、何かを壊すことを恐れずに新しい機能を作成し、古い機能をリファクタリングするには、テストが保守可能で読みやすいものでなければなりません。この記事では、私が長年にわたって収集した、Javaでユニットテストと統合テストを作成するための多くの手法について説明します。私は最新のテクノロジー(JUnit5、AssertJ、Testcontainers)に依存し、Kotlinも無視しません。ヒントのいくつかはあなたには明白に見えるでしょう、他のものはあなたがソフトウェア開発とテストについての本で読んだものに反するかもしれません。



一言で言えば



  • ヘルパー関数、パラメーター化、AssertJライブラリのさまざまなプリミティブを使用して、テストを簡潔かつ具体的に記述します。変数を乱用せず、テスト対象の機能に関連するものだけをチェックし、すべての非標準ケースを1つのテストに詰め込みません。
  • , ,
  • , -,
  • KISS DRY
  • , , , in-memory-
  • JUnit5 AssertJ —
  • : , , Clock - .




Given, When, Then (, , )



テストには、空白行で区切られた3つのブロックが含まれている必要があります。各ブロックはできるだけ短くする必要があります。ローカルメソッドを使用して、物事をコンパクトに保ちます。



与えられた/与えられた(入力):テストの準備、たとえば、データの作成や構成のモック。

いつ(アクション):テストされたメソッドを呼び出す

Then / To(出力):受信した値の正確さを確認する



// 
@Test
public void findProduct() {
    insertIntoDatabase(new Product(100, "Smartphone"));

    Product product = dao.findProduct(100);

    assertThat(product.getName()).isEqualTo("Smartphone");
}


プレフィックス「actual *」および「expected *」を使用します



// 
ProductDTO product1 = requestProduct(1);

ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(product1).isEqualTo(product2);



一致テストで変数を使用する場合は、これらの変数に「実際の」および「期待される」プレフィックスを追加します。これにより、コードの読みやすさが向上し、変数の目的が明確になります。また、比較するときに混乱しにくくなります。



// 
ProductDTO actualProduct = requestProduct(1);

ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(actualProduct).isEqualTo(expectedProduct); //   


ランダムな値の代わりにプリセット値を使用する



テストの入力にランダムな値を入力することは避けてください。これにより、テストの点滅が発生する可能性があり、デバッグが非常に困難になります。さらに、エラーメッセージにランダムな値が表示されている場合、エラーが発生した場所までさかのぼることはできません。



// 
Instant ts1 = Instant.now(); // 1557582788
Instant ts2 = ts1.plusSeconds(1); // 1557582789
int randomAmount = new Random().nextInt(500); // 232
UUID uuid = UUID.randomUUID(); // d5d1f61b-0a8b-42be-b05a-bd458bb563ad



すべてに異なる定義済みの値を使用します。このようにして、完全に再現可能なテスト結果を取得するだけでなく、エラーメッセージによってコード内の適切な場所をすばやく見つけることができます。



// 
Instant ts1 = Instant.ofEpochSecond(1550000001);
Instant ts2 = Instant.ofEpochSecond(1550000002);
int amount = 50;
UUID uuid = UUID.fromString("00000000-000-0000-0000-000000000001");



ヘルパー関数を使用して、さらに短く書くことができます(以下を参照)。



簡潔で具体的なテストを書く



可能な場合はヘルパー関数を使用する



反復コードをローカル関数に分離し、意味のある名前を付けます。これにより、テストがコンパクトになり、一目で読みやすくなります。



// 
@Test
public void categoryQueryParameter() throws Exception {
    List<ProductEntity> products = List.of(
            new ProductEntity().setId("1").setName("Envelope").setCategory("Office").setDescription("An Envelope").setStockAmount(1),
            new ProductEntity().setId("2").setName("Pen").setCategory("Office").setDescription("A Pen").setStockAmount(1),
            new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware").setDescription("A Notebook").setStockAmount(2)
    );
    for (ProductEntity product : products) {
        template.execute(createSqlInsertStatement(product));
    }

    String responseJson = client.perform(get("/products?category=Office"))
            .andExpect(status().is(200))
            .andReturn().getResponse().getContentAsString();

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}


// 
@Test
public void categoryQueryParameter2() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("1", "Office"),
            createProductWithCategory("2", "Office"),
            createProductWithCategory("3", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}


  • ヘルパー関数を使用して、データ(オブジェクト)(createProductWithCategory())および複雑なチェックを作成します。これらのパラメーターのみをこのテストに関連するヘルパー関数に渡します。残りの場合は、適切なデフォルトを使用します。Kotlinには、このためのデフォルトのパラメーター値があり、Javaでは、メソッド呼び出しチェーンとオーバーロードを使用して、デフォルトのパラメーターをシミュレートできます。
  • 可変長パラメータリストにより、コードがさらにエレガントになります(ìnsertIntoDatabase()
  • ヘルパー関数を使用して、単純な値を作成することもできます。Kotlinは、拡張機能を通じてそれをさらに改善します。


//  (Java)
Instant ts = toInstant(1); // Instant.ofEpochSecond(1550000001)
UUID id = toUUID(1); // UUID.fromString("00000000-0000-0000-a000-000000000001")


//  (Kotlin)
val ts = 1.toInstant()
val id = 1.toUUID()


Kotlinのヘルパー関数は次のように実装できます。



fun Int.toInstant(): Instant = Instant.ofEpochSecond(this.toLong())

fun Int.toUUID(): UUID = UUID.fromString("00000000-0000-0000-a000-${this.toString().padStart(11, '0')}")


変数を使いすぎないでください



プログラマーの条件付き反射は、頻繁に使用される値を変数に移動することです。



// 
@Test
public void variables() throws Exception {
    String relevantCategory = "Office";
    String id1 = "4243";
    String id2 = "1123";
    String id3 = "9213";
    String irrelevantCategory = "Hardware";
    insertIntoDatabase(
            createProductWithCategory(id1, relevantCategory),
            createProductWithCategory(id2, relevantCategory),
            createProductWithCategory(id3, irrelevantCategory)
    );

    String responseJson = requestProductsByCategory(relevantCategory);

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly(id1, id2);
}


残念ながら、これは非常にコードの過負荷です。さらに悪いことに、エラーメッセージの値を確認すると、エラーが発生した場所までさかのぼることができなくなります。

「KISSはDRYよりも重要です」


// 
@Test
public void variables() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("4243", "Office"),
            createProductWithCategory("1123", "Office"),
            createProductWithCategory("9213", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("4243", "1123");
}


できるだけコンパクトにテストを作成しようとしている場合(とにかく暖かくお勧めします)、再利用された値がはっきりと表示されます。コード自体がよりコンパクトになり、読みやすくなります。最後に、エラーメッセージにより、エラーが発生した正確な行が表示されます。



既存のテストを「もう1つ小さなものを追加する」ように拡張しないでください



// 
public class ProductControllerTest {
    @Test
    public void happyPath() {
        //   ...
    }
}


基本的な機能を検証する既存のテストに特別なケースを追加することは常に魅力的です。しかし、その結果、テストは大きくなり、理解しにくくなります。大きなコードシートに散在する特定のケースは見落としがちです。テストが失敗した場合、正確な原因をすぐに理解できない可能性があります。



// 
public class ProductControllerTest {
    @Test
    public void multipleProductsAreReturned() {}
    @Test
    public void allProductValuesAreReturned() {}
    @Test
    public void filterByCategory() {}
    @Test
    public void filterByDateCreated() {}
}


代わりに、説明的な名前を付けて新しいテストを作成し、テスト対象のコードにどのような動作が期待されるかをすぐに明らかにします。はい、キーボードでさらに文字を入力する必要があります(これに対して、ヘルパー関数が役立ちます)が、予測可能な結果が得られるシンプルで理解しやすいテストが得られます。ちなみに、これは新しい機能を文書化するための優れた方法です。



テストしたいものだけをチェックしてください



テストしている機能について考えてください。できるという理由だけで不要なチェックを行うことは避けてください。さらに、以前に作成されたテストですでにテストされたものを覚えておいて、再テストしないでください。テストはコンパクトで、期待される動作は明白で、不必要な詳細がない必要があります。



製品のリストを返すHTTPハンドルをテストするとします。テストスイートには、次のテストが含まれている必要があります



。1。データベースからのすべての値がJSON応答で正しく返され、正しい形式で正しく割り当てられていることを確認する1つの大きなマッピングテスト。メソッドを正しく実装すれば、AssertJパッケージの関数isEqualTo()(単一のアイテムの場合)またはcontainsOnly()(複数のアイテムの場合)を使用してこれを簡単に記述できます。equals()..。



String responseJson = requestProducts();

ProductDTO expectedDTO1 = new ProductDTO("1", "envelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED));
ProductDTO expectedDTO2 = new ProductDTO("2", "envelope", new Category("smartphone"), List.of(States.ACTIVE));
assertThat(toDTOs(responseJson))
        .containsOnly(expectedDTO1, expectedDTO2);


2.?カテゴリパラメータの正しい動作をチェックするいくつかのテスト。ここでは、以前に行ったため、プロパティ値ではなく、フィルタが正しく機能しているかどうかのみを確認します。したがって、受信した製品IDの一致を確認するだけで十分です。



String responseJson = requestProductsByCategory("Office");

assertThat(toDTOs(responseJson))
        .extracting(ProductDTO::getId)
        .containsOnly("1", "2");


3.特別なケースや特別なビジネスロジックをチェックする、さらにいくつかのテスト。たとえば、応答の特定の値が正しく計算されていることを確認します。この場合、JSON応答全体のいくつかのフィールドにのみ関心があります。したがって、この特別なロジックをテストで文書化しています。ここでは、これらのフィールド以外は必要ないことは明らかです。



assertThat(actualProduct.getPrice()).isEqualTo(100);


自己完結型テスト



関連するパラメータを非表示にしないでください(ヘルパー関数内)



// 
insertIntoDatabase(createProduct());
List<ProductDTO> actualProducts = requestProductsByCategory();
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));


データの生成や条件の確認にはヘルパー関数を使用すると便利ですが、パラメーターを指定して呼び出す必要があります。テスト内で意味があり、テストコードから制御する必要があるすべてのパラメータを受け入れます。テストの意味を理解するために、リーダーにヘルパー関数にジャンプするように強制しないでください。簡単なルール:テスト自体を見るとき、テストの意味は明確でなければなりません。



// 
insertIntoDatabase(createProduct("1", "Office"));
List<ProductDTO> actualProducts = requestProductsByCategory("Office");
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));


テストデータをテスト自体の中に保持する



すべてが中にあるはずです。一部のデータをメソッドに転送し、@Beforeそこから再利用するのは魅力的です。しかし、これにより、読者はファイルを前後にジャンプして、ここで何が起こっているのかを正確に理解する必要があります。繰り返しになりますが、ヘルパー関数は繰り返しを回避し、テストを理解しやすくするのに役立ちます。



継承の代わりに構成を使用する



複雑なテストクラス階層を構築しないでください。



// 
class SimpleBaseTest {}
class AdvancedBaseTest extends SimpleBaseTest {}
class AllInklusiveBaseTest extends AdvancedBaseTest {}
class MyTest extends AllInklusiveBaseTest {}


このような階層は理解を複雑にし、おそらく、基本テストの次の後続を書いていることにすぐに気付くでしょう。その中には、現在のテストではまったく必要のない多くのがらくたが縫い付けられています。これは読者の注意をそらし、微妙なエラーにつながります。継承は柔軟ではありません。クラスのすべてのメソッドを使用できますAllInclusiveBaseTestが、その親は使用できないと思いますか?AdvancedBaseTest?さらに、読者は全体像を理解するために、異なる基本クラス間を絶えずジャンプする必要があります。

「間違った抽象化を選択するよりも、コードを複製する方が良い」(Sandi Metz)



代わりにコンポジションを使用することをお勧めします。フィクスチャ関連のタスクごとに小さなスニペットとクラスを記述します(テストデータベースの開始、スキーマの作成、データの挿入、モックサーバーの開始)。これらのパーツをメソッドで再利用する@BeforeAllか、作成したオブジェクトをテストクラスのフィールドに割り当てます。このようにして、Legoパーツからのように、これらのブランクから新しいテストクラスをそれぞれ構築することができます。その結果、各テストには独自の理解可能なフィクスチャのセットがあり、それ以外のことは何も起こらないことが保証されます。テストには必要なものがすべて含まれているため、テストは自給自足になります。



// 
public class MyTest {
    //   
    private JdbcTemplate template;
    private MockWebServer taxService;

    @BeforeAll
    public void setupDatabaseSchemaAndMockWebServer() throws IOException {
        this.template = new DatabaseFixture().startDatabaseAndCreateSchema();
        this.taxService = new MockWebServer();
        taxService.start();
    }
}


//   
public class DatabaseFixture {
    public JdbcTemplate startDatabaseAndCreateSchema() throws IOException {
        PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine");
        db.start();
        DataSource dataSource = DataSourceBuilder.create()
                .driverClassName("org.postgresql.Driver")
                .username(db.getUsername())
                .password(db.getPassword())
                .url(db.getJdbcUrl())
                .build();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        SchemaCreator.createSchema(template);
        return template;
    }
}


もう一度:

「KISSはDRYよりも重要です」


簡単なテストが良いです。結果を定数と比較します



プロダクションコードを再利用しないでください



テストでは、製品コードを再利用するのではなく、検証する必要があります。テストで戦闘コードを再利用すると、テストを行っていないため、そのコードのバグを見逃す可能性があります。



// 
boolean isActive = true;
boolean isRejected = true;
insertIntoDatabase(new Product(1, isActive, isRejected));

ProductDTO actualDTO = requestProduct(1);


//   
List<State> expectedStates = ProductionCode.mapBooleansToEnumList(isActive, isRejected);
assertThat(actualDTO.states).isEqualTo(expectedStates);


代わりに、テストを作成するときは、入力と出力の観点から考えてください。テストはデータを入力にフィードし、出力を事前定義された定数と比較します。ほとんどの場合、コードの再利用は必要ありません。



// Do
assertThat(actualDTO.states).isEqualTo(List.of(States.ACTIVE, States.REJECTED));


ビジネスロジックをテストにコピーしないでください



オブジェクトマッピングは、テストが戦闘コードからロジックを自分自身にプルする場合の代表的な例です。テストにメソッドが含まれているとしますmapEntityToDto()。その結果を使用して、結果のDTOにテストの開始時にベースに追加された要素と同じ値が含まれていることを確認します。この場合、おそらくエラーが含まれている可能性のある戦闘コードをテストにコピーします。



// 
ProductEntity inputEntity = new ProductEntity(1, "envelope", "office", false, true, 200, 10.0);
insertIntoDatabase(input);

ProductDTO actualDTO = requestProduct(1);

 // mapEntityToDto()    ,   -
ProductDTO expectedDTO = mapEntityToDto(inputEntity);
assertThat(actualDTO).isEqualTo(expectedDTO);



正しい解決策はactualDTO、指定された値を持つ手動で作成された参照オブジェクトと比較することです。非常にシンプルでわかりやすく、潜在的なミスから保護します。



// 
ProductDTO expectedDTO = new ProductDTO("1", "envelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED))
assertThat(actualDTO).isEqualTo(expectedDTO);


参照オブジェクト全体の一致を作成して確認したくない場合は、子オブジェクトを確認するか、通常はテストに関連するオブジェクトのプロパティのみを確認できます。



あまり多くのロジックを書かないでください



テストは主に入力と出力に関するものであることを思い出してください。データを送信し、何が返されるかを確認します。テスト内に複雑なロジックを記述する必要はありません。テストにループと条件を導入すると、テストが理解しにくくなり、エラーが発生しやすくなります。検証ロジックが複雑な場合は、多くのAssertJ関数を使用して作業を行ってください。



戦闘のような環境でテストを実行する



コンポーネントの可能な限り完全なバンドルをテストします



一般に、モックを使用して各クラスを個別にテストすることをお勧めします。ただし、このアプローチには欠点があります。このように、クラス間の相互作用はテストされず、各内部クラスには独自のテストがあるため、一般エンティティのリファクタリングではすべてのテストが一度に中断されます。さらに、クラスごとにテストを作成すると、テストが多すぎます。





各クラスの個別ユニットテスト



代わりに、統合テストに焦点を当てることをお勧めします。「統合テスト」とは、(本番環境のように)すべてのクラスをまとめて収集し、インフラストラクチャコンポーネント(HTTPサーバー、データベース、ビジネスロジック)を含むバンドル全体をテストすることを意味します。この場合、実装ではなく動作をテストしています。このようなテストは、より正確で、現実の世界に近く、内部コンポーネントのリファクタリングに耐性があります。理想的には、1つのクラスのテストで十分です。





統合テスト(=すべてのクラスをまとめてバンドルをテストする)



テストにメモリ内データベースを使用しないでください





インメモリベース



を使用すると、コードが機能する別の環境でテストします。テストにインメモリベース(H2HSQLDBFongo)を使用すると、その有効性と範囲が犠牲になります。このようなデータベースは、多くの場合、動作が異なり、異なる結果を生成します。このようなテストは正常に合格する可能性がありますが、本番環境でのアプリケーションの正しい動作を保証するものではありません。さらに、ベースの動作や機能の特性がメモリ内データベースに実装されていないか、動作が異なるため、それらを使用またはテストできない状況に簡単に気付く可能性があります。



解決策:実際の操作と同じデータベースを使用してください。素晴らしいTestcontainersライブラリ テストコードから直接コンテナを管理できるようにするJavaアプリケーション用の豊富なAPIを提供します。



Java / JVM



使用する -noverify -XX:TieredStopAtLevel=1



JVM -noverify -XX:TieredStopAtLevel=1テストを実行するには、 常に構成にオプション追加してください。これにより、テストを開始する前に仮想マシンを起動するのに1〜2秒節約できます。これは、IDEから頻繁に実行するテストの初期段階で特に役立ちます。



Java 13は-noverify非推奨であるため、注意してください



ヒント:これらの引数をIntelliJ IDEAの「JUnit」構成テンプレートに追加して、新しいプロジェクトを作成するたびにこれを行う必要がないようにします。







AssertJを使用する



AssertJは、非常に強力で成熟したライブラリであり、豊富で安全なAPIに加えて、豊富な値検証関数と有益なテストエラーメッセージのセットを備えています。多くの便利な検証機能により、プログラマーはテストの本体で複雑なロジックを記述する必要がなくなり、テストを簡潔にすることができます。例えば:



assertThat(actualProduct)
        .isEqualToIgnoringGivenFields(expectedProduct, "id");

assertThat(actualProductList).containsExactly(
        createProductDTO("1", "Smartphone", 250.00),
        createProductDTO("1", "Smartphone", 250.00)
);

assertThat(actualProductList)
        .usingElementComparatorIgnoringFields("id")
        .containsExactly(expectedProduct1, expectedProduct2);

assertThat(actualProductList)
        .extracting(Product::getId)
        .containsExactly("1", "2");

assertThat(actualProductList)
        .anySatisfy(product -> assertThat(product.getDateCreated()).isBetween(instant1, instant2));

assertThat(actualProductList)
        .filteredOn(product -> product.getCategory().equals("Smartphone"))
        .allSatisfy(product -> assertThat(product.isLiked()).isTrue());


assertTrue()およびの使用避けるassertFalse()



単純なものを使用するassertTrue()assertFalse()、不可解なテストエラーメッセージが表示されます。



// 
assertTrue(actualProductList.contains(expectedProduct));
assertTrue(actualProductList.size() == 5);
assertTrue(actualProduct instanceof Product);

expected: <true> but was: <false>


代わりにAssertJ呼び出しを使用してください。これにより、箱から出してすぐに明確で有益なメッセージが返されます。



// 
assertThat(actualProductList).contains(expectedProduct);
assertThat(actualProductList).hasSize(5);
assertThat(actualProduct).isInstanceOf(Product.class);

Expecting:
 <[Product[id=1, name='Samsung Galaxy']]>
to contain:
 <[Product[id=2, name='iPhone']]>
but could not find:
 <[Product[id=2, name='iPhone']]>


ブール値を確認する必要がある場合は、as()AssertJメソッドを使用してメッセージをよりわかりやすくします。



JUnit5を使用する



JUnit5は、(ユニット)テスト用の優れたライブラリです。これは継続的に開発されており、パラメーター化されたテスト、グループ化、条件付きテスト、ライフサイクル制御などの多くの便利な機能をプログラマーに提供します。



パラメータ化されたテストを使用する



パラメータ化されたテストを使用すると、異なる入力値のセットを使用して同じテストを実行できます。これにより、余分なコードを記述せずに複数のケースをチェックできます。JUnit5では、このための優れたツールで@ValueSource@EnumSource@CsvSource@MethodSource



// 
@ParameterizedTest
@ValueSource(strings = ["§ed2d", "sdf_", "123123", "§_sdf__dfww!"])
public void rejectedInvalidTokens(String invalidToken) {
    client.perform(get("/products").param("token", invalidToken))
            .andExpect(status().is(400))
}

@ParameterizedTest
@EnumSource(WorkflowState::class, mode = EnumSource.Mode.INCLUDE, names = ["FAILED", "SUCCEEDED"])
public void dontProcessWorkflowInCaseOfAFinalState(WorkflowState itemsInitialState) {
    // ...
}


最小限の労力でより多くのケースをテストできるため、このトリックを最大限に活用することを強くお勧めします。



最後に、私はあなたに注意を喚起したい@CsvSource@MethodSourceあなたはパラメータのいずれかでそれを渡すことができます:あなたはまた、その結果を制御するために必要な、より複雑なパラメータ設定、のために使用することができます。



@ParameterizedTest
@CsvSource({
    "1, 1, 2",
    "5, 3, 8",
    "10, -20, -10"
})
public void add(int summand1, int summand2, int expectedSum) {
    assertThat(calculator.add(summand1, summand2)).isEqualTo(expectedSum);
}


@MethodSourceすべての必要なパラメータと期待される結果を含む個別のテストオブジェクトと組み合わせて特に効果的です。残念ながら、Javaでは、このようなデータ構造(いわゆるPOJO)の記述は非常に面倒です。したがって、Kotlinデータクラスを使用した例を示します。



data class TestData(
    val input: String?,
    val expected: Token?
)

@ParameterizedTest
@MethodSource("validTokenProvider")
fun `parse valid tokens`(data: TestData) {
    assertThat(parse(data.input)).isEqualTo(data.expected)
}

private fun validTokenProvider() = Stream.of(
    TestData(input = "1511443755_2", expected = Token(1511443755, "2")),
    TestData(input = "151175_13521", expected = Token(151175, "13521")),
    TestData(input = "151144375_id", expected = Token(151144375, "id")),
    TestData(input = "15114437599_1", expected = Token(15114437599, "1")),
    TestData(input = null, expected = null)
)


グループテスト



@NestedJUnit5の 注釈は、テストメソッドをグループ化するのに便利です。論理的に、それは一緒にグループに(例えば、テストの特定の種類の意味がInputIsXYErrorCases)か、グループ内の各試験方法(収集するGetDesignUpdateDesign)を。



public class DesignControllerTest {
    @Nested
    class GetDesigns {
        @Test
        void allFieldsAreIncluded() {}
        @Test
        void limitParameter() {}
        @Test
        void filterParameter() {}
    }
    @Nested
    class DeleteDesign {
        @Test
        void designIsRemovedFromDb() {}
        @Test
        void return404OnInvalidIdParameter() {}
        @Test
        void return401IfNotAuthorized() {}
    }
}






@DisplayNameKotlinでの読み取り可能なテスト名またはバッククォート



Javaでは、注釈@DisplayName使用して、テストに読みやすい名前を付けることができます



public class DisplayNameTest {
    @Test
    @DisplayName("Design is removed from database")
    void designIsRemoved() {}
    @Test
    @DisplayName("Return 404 in case of an invalid parameter")
    void return404() {}
    @Test
    @DisplayName("Return 401 if the request is not authorized")
    void return401() {}
}






Kotlinでは、関数名をバッククォートで囲むと、内部にスペースを含む関数名を使用できます。このようにして、コードの冗長性なしで結果を読みやすくします。



@Test
fun `design is removed from db`() {}


外部サービスをシミュレートする



HTTPクライアントをテストするには、アクセスするサービスをシミュレートする必要があります。私はこの目的のためにOkHttpのMockWebServerをよく使用します代替手段は、TestcontainersのWireMockまたはMockserverです



MockWebServer serviceMock = new MockWebServer();
serviceMock.start();
HttpUrl baseUrl = serviceMock.url("/v1/");
ProductClient client = new ProductClient(baseUrl.host(), baseUrl.port());
serviceMock.enqueue(new MockResponse()
        .addHeader("Content-Type", "application/json")
        .setBody("{\"name\": \"Smartphone\"}"));

ProductDTO productDTO = client.retrieveProduct("1");

assertThat(productDTO.getName()).isEqualTo("Smartphone");


待機性を使用して非同期コードをテストする



Awaitilityは、非同期コードをテストするためのライブラリです。テストが失敗したと宣言する前に、結果のチェックを再試行する回数を指定できます。



private static final ConditionFactory WAIT = await()
        .atMost(Duration.ofSeconds(6))
        .pollInterval(Duration.ofSeconds(1))
        .pollDelay(Duration.ofSeconds(1));

@Test
public void waitAndPoll(){
    triggerAsyncEvent();
    WAIT.untilAsserted(() -> {
        assertThat(findInDatabase(1).getState()).isEqualTo(State.SUCCESS);
    });
}


DIの依存関係を解決する必要はありません(春)



DIフレームワークは、テストを開始する前に初期化するのに数秒かかります。これにより、特に開発の初期段階で、フィードバックループが遅くなります。



したがって、統合テストではDIを使用しないようにしますが、必要なオブジェクトを手動で作成し、それらを「結び付け」ます。コンストラクタインジェクションを使用している場合、これが最も簡単です。通常、テストでは、ビジネスロジックを検証しますが、そのためにDIは必要ありません。



さらに、バージョン2.2以降、Spring BootはBeanの遅延初期化をサポートしているため、DIを使用したテストが大幅に高速化されます。



コードはテスト可能でなければなりません



静的アクセスは使用しないでください。決して



静的アクセスはアンチパターンです。まず、依存関係と副作用がわかりにくくなり、コード全体が読みにくくなり、微妙なエラーが発生しやすくなります。次に、静的アクセスがテストの邪魔になります。オブジェクトを置き換えることはできなくなりましたが、テストでは、異なる構成のモックまたは実際のオブジェクト(たとえば、テストデータベースを指すDAOオブジェクト)を使用する必要があります。



コードに静的にアクセスする代わりに、コードを非静的メソッドに入れ、クラスをインスタンス化し、結果のオブジェクトをコンストラクターに渡します。



// 
public class ProductController {
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = ProductDAO.getProducts();
        return mapToDTOs(products);
    }
}



// 
public class ProductController {
    private ProductDAO dao;
    public ProductController(ProductDAO dao) {
        this.dao = dao;
    }
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = dao.getProducts();
        return mapToDTOs(products);
    }
}


幸い、SpringのようなDIフレームワークは、私たちの関与なしにオブジェクトを自動的に作成してリンクすることにより、静的アクセスを不要にするツールを提供します。



パラメータ化



クラスの関連するすべての部分は、テスト側から構成可能である必要があります。このような設定は、クラスコンストラクターに渡すことができます。



たとえば、DAOのリクエストごとに1000オブジェクトの固定制限があるとします。この制限を確認するには、テストする前に1001個のオブジェクトをテストデータベースに追加する必要があります。コンストラクター引数を使用して、この値をカスタマイズ可能にすることができます。本番環境では1000のまま、テストでは2に減らします。したがって、制限の動作を確認するには、テストデータベースに3つのレコードのみを追加する必要があります。



コンストラクタインジェクションを使用する



フィールドインジェクションは悪であり、コードのテスト容易性が低下します。テストの前にDIを初期化するか、奇妙な反射魔法を実行する必要があります。したがって、テスト中に依存オブジェクトを簡単に制御するには、コンストラクタインジェクションを使用することをお勧めします。



Javaでは、少し余分なコードを書く必要があります。



// 
public class ProductController {

    private ProductDAO dao;
    private TaxClient client;

    public ProductController(ProductDAO dao, TaxClient client) {
        this.dao = dao;
        this.client = client;
    }
}


Kotlinでは、同じことがはるかに簡潔に書かれています。



// 
class ProductController(
    private val dao: ProductDAO,
    private val client: TaxClient
){
}


Instant.now() またはを使用しないでくださいnew Date()



この動作をテストする場合は 、呼び出しInstant.now()またはnew Date()実稼働コードで現在の時刻を取得する必要はありません



// 
public class ProductDAO {
    public void updateDateModified(String productId) {
        Instant now = Instant.now(); // !
        Update update = Update()
            .set("dateModified", now);
        Query query = Query()
            .addCriteria(where("_id").eq(productId));
        return mongoTemplate.updateOne(query, update, ProductEntity.class);
    }
}


問題は、かかる時間をテストで制御できないことです。得られた結果は常に異なるため、特定の値と比較することはできません。代わりにClockJavaのクラス使用してください



// 
public class ProductDAO {
    private Clock clock; 

    public ProductDAO(Clock clock) {
        this.clock = clock;
    }

    public void updateProductState(String productId, State state) {
        Instant now = clock.instant();
        // ...
    }
}


このテストでは、のモックオブジェクトを作成してClock渡しProductDAO、同時に返すようにモックオブジェクトを構成できます。呼び出し後、updateProductState()指定した値がデータベースに入力されていることを確認できます。



非同期実行を実際のロジックから分離する



非同期コードのテストには注意が必要です。Awaitilityのようなライブラリは非常に役立ちますが、プロセスは依然として複雑であり、テストが点滅する可能性があります。可能であれば、ビジネスロジック(通常は同期)と非同期インフラストラクチャコードを分離することは理にかなっています。



たとえば、ProductControllerにビジネスロジックを配置することで、同期的に簡単にテストできます。すべての非同期ロジックと並列ロジックはProductSchedulerに残り、個別にテストできます。



// 
public class ProductScheduler {

    private ProductController controller;

    @Scheduled
    public void start() {
        CompletableFuture<String> usFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.US));
        CompletableFuture<String> germanyFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.GERMANY));
        String usResult = usFuture.get();
        String germanyResult = germanyFuture.get();
    }
}


コトリン



私の記事「コトリンでのユニットテストのベストプラクティス」には、コトリン固有のユニットテスト手法が多数含まれています。(翻訳に注意してください:この記事のロシア語の翻訳に興味がある場合はコメントに書き込んでください)。



All Articles