Boundary

In a MicroService architecture, the components are the services themselves. There are two typical interface in our service:

  • Controllers - handle http request

  • Kafka Listeners - handle Kafka message

By writing component tests for these two kind of interface, we can easily cover each business flow in our service which not able to guarantee in unit test.

Component tests usually take more time to write and execute since it start a Micronaut server with local dependencies(DB, Kafka and Stub servers).

Where to put the component tests

under the folder /test

How to Write Component Test

Create Test Class

Run Micronaut server with local dependencies

  • Component test should be annotated with @MicronautTest , it starts a Micronaut server using configuration application-test.yml

  • Stub external service(if it calls external service) via https://wiremock.org/

  • Run Kafka container(if it interacts with Kafka) via annotation @UseKafka (it start Kafka container only once and expose environment variable KAFKA_URL)

  • Run Temporal container(if it involve temporal workflow) via annotation @UseTemporal (it starts temporalite container and expose environment variable TEMPORAL_URL)

Verify business flow

  • Test cooperation of each layers instead of business logic which already covered by unit tests

  • Send http request to the endpoint or Kafka message for the listener under testing

  • Assertion

Notice: The annotation @MicronautTest has to be placed before @DBRider

@UseTemporal
@UseKafka
@WireMockStubFor(service = "thought-machine", exposedUrl = "TM_URL")
@WireMockStubFor(service = "card-manager", exposedUrl = "SAFI_CARD_MANAGER_URL")
@MicronautTest
@DBRider
@DataSet(skipCleaningFor = ["flyway_schema_history"])
class CardTransferTransactionControllerTest(private val embeddedServer: EmbeddedServer) {
    @Test
    @ExpectedDataSet("after-transfer-success.yml")
    fun `should return transaction status success when TM accept transfer transaction`() {
        val transactionId = "4c5c51fa-5ae5-4827-bbf5-2a6636b2ead6"
        schedulePostingApiResponseEvent(buildPostingEvent(transactionId, true, TRANSFER))

        Given {
            body(buildTransferTransactionBody(transactionId))
            contentType(ContentType.JSON)
            header(Header("idempotencyKey", "4c5c51fa-5ae5-4827-bbf5-2a6636b2ead6"))
        }.When {
            post("${embeddedServer.url}/transaction/card-transfer/sync")
        }.Then {
            statusCode(HttpStatus.CREATED.code)
            body("transactionId", equalTo(transactionId))
            body("status", equalTo(SUCCESS.name))
            wireMockServers["thought-machine"]!!.verify(1, postRequestedFor(urlEqualTo("/posting-instruction-batches:asyncCreate")))
        }
    }
}

Note:

  • theres a bug with the @DBRider annotation for the data setup due to micronaut use this instead: import ph.safibank.common.testutils.dbrider.DBRider

  • important: dont forget to add dbunit.xml

  • if you are encountering a class cast exception when using http client that has something like unable to cast to scala… <TODO: INSERT STACK HERE>
    SOLUTION:

        testImplementation("ph.safibank.common:test-utils:2.20230106-075056") {
            // TODO figure out how to resolve the serialization issue due to the jackson-scala module
            exclude("net.mguenther.kafka", "kafka-junit")
        }

Attachments: