1. 概要

このチュートリアルでは、KotlinベースのSpringBootアプリケーションのいくつかのテスト手法について説明します。

まず、リポジトリ、コントローラー、サービスなどのいくつかのコンポーネントを使用して、基本的なKotlinベースのSpringBootアプリを作成しましょう。 次に、ユニットと統合テストの手法について説明します。

2. 設定

Kotlinを使用してSpringBootアプリのMaven依存関係をブラッシュアップしましょう。

まず、最新のspring-boot-starter-webおよびspring-boot-starter-data-jpa Maven依存関係をpom.xmlに追加して、WebおよびJPAをサポートします

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>2.6.1</version>
</dependency>

次に、永続化のためにh2組み込みデータベースを追加しましょう。

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    <version>2.0.202</version>
</dependency>

次に、Kotlinコードのソースディレクトリを定義できます。

<build>
    <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
        // ...
    </plugins>
</build>

最後に、 kotlin-maven-pluginプラグインをセットアップして、SpringおよびJPAサポート用のall-openやno-argなどのコンパイラプラグインを提供します

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <configuration>
        <args>
            <arg>-Xjsr305=strict</arg>
        </args>
        <compilerPlugins>
            <plugin>spring</plugin>
            <plugin>jpa</plugin>
            <plugin>all-open</plugin>
            <plugin>no-arg</plugin>
        </compilerPlugins>
        <pluginOptions>
            <option>all-open:annotation=javax.persistence.Entity</option>
            <option>all-open:annotation=javax.persistence.Embeddable</option>
            <option>all-open:annotation=javax.persistence.MappedSuperclass</option>
        </pluginOptions>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>1.6.10</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-noarg</artifactId>
            <version>1.6.10</version>
        </dependency>
    </dependencies>
</plugin>

3. SpringBootアプリケーション

これでセットアップの準備が整いました。 ユニットと統合テストのために、SpringBootアプリにいくつかのコンポーネントを追加しましょう。

3.1. 実在物

まず、bankCodeやaccountNumberなどのいくつかのプロパティを使用してBankAccountエンティティを作成します。

@Entity
data class BankAccount (
    var bankCode:String,
    var accountNumber:String,
    var accountHolderName:String,
    @Id @GeneratedValue var id: Long? = null
)

3.2. リポジトリ

次に、 BankAccountRepositoryクラスを作成して、SpringDataのCrudRepositoryを使用してBankAccountエンティティにCRUD機能を提供しましょう。

@Repository
interface BankAccountRepository : CrudRepository<BankAccount, Long> {}

3.3. サービス

さらに、addBankAccountgetBankAccountなどのいくつかのメソッドを使用して、BankAccountServiceクラスを作成します。

@Service
class BankAccountService(var bankAccountRepository: BankAccountRepository) {
    fun addBankAccount(bankAccount: BankAccount): BankAccount {
        return bankAccountRepository.save(bankAccount);
    }
    fun getBankAccount(id: Long): BankAccount? {
        return bankAccountRepository.findByIdOrNull(id)
    }
}

3.4. コントローラ

最後に、 BankControllerクラスを作成して、/ api/bankAccountエンドポイントを公開しましょう。

@RestController
@RequestMapping("/api/bankAccount")
class BankController(var bankAccountService: BankAccountService) {

    @PostMapping
    fun addBankAccount(@RequestBody bankAccount:BankAccount) : ResponseEntity<BankAccount> {
        return ResponseEntity.ok(bankAccountService.addBankAccount(bankAccount))
    }

    @GetMapping
    fun getBankAccount(@RequestParam id:Long) : ResponseEntity<BankAccount> {
        var bankAccount: BankAccount? = bankAccountService.getBankAccount(id);
        if (bankAccount != null) {
            return ResponseEntity(bankAccount, HttpStatus.OK)
        } else {
            return ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }
}

そのため、 BankAccount オブジェクトをそれぞれ読み取り、作成するために、エンドポイントをGETおよびPOSTマッピングで公開しました。

4. セットアップのテスト

4.1. JUnit5

まず、JUnitのビンテージサポートを除外します。これは、spring-boot-starter-test依存関係の一部です。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

次に、JUnit5サポートMaven依存関係を含めましょう。

<dependency> 
    <groupId>org.junit.jupiter</groupId> 
    <artifactId>junit-jupiter-engine</artifactId> 
    <version>5.8.1</version> 
    <scope>test</scope> 
</dependency>

4.2. MockK

また、サービスおよびリポジトリコンポーネントのテストに便利なモック機能をテストに追加する必要があります。

ただし、 Mockito の代わりに、Kotlinにより適したMockKライブラリを使用します。

したがって、spring-boot-starter-test依存関係に付属するmockito-core依存関係を除外しましょう。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

次に、最新の mockkMaven依存関係をpom.xmlに追加できます。

<dependency>
    <groupId>com.ninja-squad</groupId>
    <artifactId>springmockk</artifactId>
    <version>3.0.1</version>
    <scope>test</scope>
</dependency>

5. ユニットテスト

5.1. MockKを使用したテストサービス

まず、 BankAccountServiceTest クラスを作成し、MockKを使用してBankAccountRepositoryをモックします。

class BankAccountServiceTest {
    val bankAccountRepository: BankAccountRepository = mockk();
    val bankAccountService = BankAccountService(bankAccountRepository);
}

次に、すべてのブロックを使用してBankAccountRepository の応答をモックし、getBankAccountメソッドの結果を確認できます。

@Test
fun whenGetBankAccount_thenReturnBankAccount() {
    //given
    every { bankAccountRepository.findByIdOrNull(1) } returns bankAccount;

    //when
    val result = bankAccountService.getBankAccount(1);

    //then
    verify(exactly = 1) { bankAccountRepository.findByIdOrNull(1) };
    assertEquals(bankAccount, result)
}

5.2. @WebMvcTestを使用してコントローラーをテストする

単体テスト用にSpringMVCインフラストラクチャを自動的に構成する@WebMvcTestアノテーションを使用できます。

まず、 MockMvc Beanを挿入し、@MockkBeanアノテーションを使用してBankAccountServiceをモックします。

@WebMvcTest
class BankControllerTest(@Autowired val mockMvc: MockMvc) {

    @MockkBean
    lateinit var bankAccountService: BankAccountService
}

次に、 BankAccountServiceの応答をモックし、 MockMvc Beanのインスタンスを使用してGETリクエストを実行し、JSONの結果を確認します

@Test
fun givenExistingBankAccount_whenGetRequest_thenReturnsBankAccountJsonWithStatus200() {
    every { bankAccountService.getBankAccount(1) } returns bankAccount;

    mockMvc.perform(get("/api/bankAccount?id=1"))
      .andExpect(status().isOk)
      .andExpect(content().contentType(MediaType.APPLICATION_JSON))
      .andExpect(jsonPath("$.bankCode").value("ING"));
}

同様に、不正なリクエストをもたらすGETリクエストを確認できます。

@Test
fun givenBankAccountDoesntExist_whenGetRequest_thenReturnsStatus400() {
    every { bankAccountService.getBankAccount(2) } returns null;

    mockMvc.perform(get("/api/bankAccount?id=2"))
      .andExpect(status().isBadRequest());
}

また、 MockMvc Beanを使用して、 BankAccount JSONをリクエスト本文としてPOSTリクエストを実行し、結果を確認できます。

@Test
fun whenPostRequestWithBankAccountJson_thenReturnsStatus200() {
    every { bankAccountService.addBankAccount(bankAccount) } returns bankAccount;

    mockMvc.perform(post("/api/bankAccount").content(mapper.writeValueAsString(bankAccount)).contentType(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk)
      .andExpect(content().contentType(MediaType.APPLICATION_JSON))
      .andExpect(jsonPath("$.bankCode").value("ING"));
}

6. 統合テスト

6.1. @DataJpaTestを使用してリポジトリをテストします

@DataJpaTestアノテーションを使用して、永続性レイヤー の標準セットアップを提供し、リポジトリをテストできます。

まず、 BankAccountRepositoryTest クラスを作成し、テストが終了すると実行全体をロールバックするTestEntityManagerBeanを挿入します。

@DataJpaTest
class BankAccountRepositoryTest {
    @Autowired
    lateinit var entityManager: TestEntityManager
            
    @Autowired
    lateinit var bankAccountRepository: BankAccountRepository
}

次に、 TestEntityManager のインスタンスを使用して、BankAccountRepositoryfindByIdOrNull拡張メソッドをテストしましょう。

@Test
fun WhenFindById_thenReturnBankAccount() {
    val ingBankAccount = BankAccount("ING", "123ING456", "JOHN SMITH");
    entityManager.persist(ingBankAccount)
    entityManager.flush()
    val ingBankAccountFound = bankAccountRepository.findByIdOrNull(ingBankAccount.id!!)
    assertThat(ingBankAccountFound == ingBankAccount)
}

6.2. @SpringBootTestを使用してアプリをテストする

@SpringBootTestアノテーションを使用して、サンドボックスWeb環境でアプリを起動できます

@SpringBootTest(
  classes = arrayOf(KotlinTestingDemoApplication::class),
  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class KotlinTestingDemoApplicationIntegrationTest {

    @Autowired
    lateinit var restTemplate: TestRestTemplate
}

また、 TestRestTemplate Beanを注入して、アプリによって公開されたRESTfulエンドポイントをテストしました。

それでは、 / api /bankAccountエンドポイントでGETリクエストをテストしてみましょう。

@Test
fun whenGetCalled_thenShouldBadReqeust() {
    val result = restTemplate.getForEntity("/api/bankAccount?id=2", BankAccount::class.java);

    assertNotNull(result)
    assertEquals(HttpStatus.BAD_REQUEST, result?.statusCode)
}

同様に、 TestRestTemplateインスタンスを使用して、 / api /bankAccountエンドポイントでPOSTリクエストをテストできます。

@Test
fun whePostCalled_thenShouldReturnBankObject() {
    val result = restTemplate.postForEntity("/api/bankAccount", BankAccount("ING", "123ING456", "JOHN SMITH"), BankAccount::class.java);

    assertNotNull(result)
    assertEquals(HttpStatus.OK, result?.statusCode)
    assertEquals("ING", result.getBody()?.bankCode)
}

7. 結論

この記事では、SpringBootアプリとKotlinのユニットおよび統合テストの手法について説明しました。

まず、エンティティ、リポジトリ、サービス、コントローラーを備えたKotlinベースのSpringBootアプリを開発しました。 次に、そのようなコンポーネントをテストするさまざまな方法を検討しました。

いつものように、コードはGitHubから入手できます。