Written by Patryk Kurczyna
Kotlin/Java Developer
Published November 27, 2023

Integration Testing Deep Dive Part I

 

1. Overview

Professionals well-versed in  The Testing Pyramid recognize the pivotal role Integration Tests play in contemporary backend (micro)services development. In this article I will try to make a deep dive into one of my favourite ways of setting up Integration Tests suite for Java/Kotlin applications. It encompasses a comprehensive exploration of setup methodologies, essential tools, best practices, and the seamless integration with various third-party entities often encountered in the development landscape.

As an illustrative example, the primary emphasis will be on the Spring Boot framework, but I believe many of the concepts presented in the article can be used in other technologies too. Personally, I have garnered experience in setting it up with Ktor and Docker Compose as well.

I have opted for Kotlin as my language of choice and Spock framework (based on Groovy) for testing, as I am a big fan of it. However, it’s important to note that you are free to leverage alternative tools aligned with your preferences. If you want to learn more about Spock read my previous article about it.

2. Preparation

All of the code in this article can be found in this GitHub repository

Project setup

Let’s start with the project setup and dependencies. This is the build.gradle.kts file to setup basic Spring Boot application, plus unit and integration test suites. We shall go through the most important parts of it in detail.

Plugins

Here is the list of the plugins you need with a short description.

plugins {
    id("org.springframework.boot") version "3.1.1" // Spring Boot
    id("io.spring.dependency-management") version "1.1.0" // Spring Dependency Management
    id("org.jlleitschuh.gradle.ktlint") version "11.4.2" // Kotlin linter https://pinterest.github.io/ktlint/1.0.0/
    id("com.adarshr.test-logger") version "3.2.0" // Plugin for printing beautiful logs on the console while running tests. https://github.com/radarsh/gradle-test-logger-plugin
    kotlin("jvm") version "1.8.22" // Kotlin plugin
    kotlin("plugin.spring") version "1.8.22" // Kotlin Spring support
    idea // Intelij Idea plugin
    groovy // Groovy plugin (needed for Spock)
}

Integration tests task setup

Adhering to best practices, I do recommend separating the unit test suite from the integration test suite.

That is why I opted for having a separate Gradle task for integration tests. This deliberate separation extends to declaring different dependencies for each suite, fostering a high degree of decoupling.

sourceSets {
    create("itest") {
        compileClasspath += sourceSets.main.get().output
        compileClasspath += sourceSets.test.get().output
        runtimeClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.test.get().output
    }
}

idea.module {
    testSourceDirs = testSourceDirs + project.sourceSets["itest"].allJava.srcDirs + project.sourceSets["test"].allJava.srcDirs
}

configurations["itestRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get())

Firstly, let’s create a new `sourceSet` for integration tests and help IntelliJ IDE to find it. The unit tests will be stored in `src/test/groovy` directory and the integration tests located in `src/itest/groovy` directory.

Secondly, let’s create a gradle task `itest` which will inherit from the `Test` task and become responsible for running integration tests.

// ITests
val itest = task<Test>("itest") {
    description = "Runs integration tests."
    group = "verification"

    testClassesDirs = sourceSets["itest"].output.classesDirs
    classpath = sourceSets["itest"].runtimeClasspath

    shouldRunAfter("test")
}

Now, you should plug-in the task into the standard Gradle `check` task.

tasks.check {
    dependsOn(tasks.ktlintCheck)
    dependsOn(tasks.test)
    dependsOn(itest)
}

Last but not least, the next step is to create a helper method for adding dependencies in `itest` scope only.

val itestImplementation: Configuration by configurations.getting {
    extendsFrom(configurations.implementation.get())
}

At this point, defining all the dependencies required for a given project becomes feasible.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.spockframework:spock-core:$spockVersion")
    testImplementation("org.apache.groovy:groovy-all:$groovyVersion")

    itestImplementation("org.springframework.boot:spring-boot-starter-test")
    itestImplementation("org.spockframework:spock-core:$spockVersion")
    itestImplementation("org.spockframework:spock-spring:$spockVersion")
    itestImplementation("org.apache.groovy:groovy-all:$groovyVersion")
}

This is all we need to run our first integration test.

3. First Integration Test

Simple Spring Boot application

Let’s write a very basic Spring Boot application and our first integration test.

@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

Due to the inclusion of `spring-boot-starter-actuator` to the classpath, the application can expose management endpoints such as `health`. To enable the creation of our initial integration test, specific configurations in the `application.yml` file are necessary. This setup allows the test to initiate the application and confirm the proper functioning of the health endpoint.

server:
  port: 8080

management:
  endpoints:
    web:
      base-path: /_
      exposure:
        include: health
  endpoint:
    health:
      show-details: always
      show-components: always
  server:
    port: 8081

It’s typically advantageous to create a common base for all the tests, encapsulating any boilerplate code and incorporating common configurations. Here’s how the `IntegrationTestBase` class could be written for Spring Boot.

@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles('itest')
@ContextConfiguration
abstract class IntegrationTestBase extends Specification

We can break it down into details:

  • `@SpringBootTest` annotation tells Spring to run this class as an integration test, which means it will start the whole application.
  • `webEnvironment = RANDOM_PORT` makes sure the application runs on a random available port on our machine
  • `@ActiveProfiles(‘itest’) `activates Spring `itest` profile which we will need later to make custom configuration for our application

As you can see – the test base is simple, but its subsequent stages will be expanded later.

First Integration Test

Let’s write our first test.

class ITestManagement extends IntegrationTestBase {

    @LocalManagementPort
    int mgmtPort

    def "should return 200 status for management path: health"() {
        when:
        HttpURLConnection connection = new URL("http://localhost:$mgmtPort/_/health").openConnection() as HttpURLConnection

        then:
        connection.getResponseCode() == 200
    }
}

This concise yet powerful test case perfectly exemplifies the ease with which one can commence in terms of a testing journey.

This single test case showcased Spock’s framework syntax and structure. Additionally, it injects `@LocalManagementPort`, the port where our application exposes all management endpoints. The objective here is to execute a straightforward HTTP call to one such endpoint – the `health` endpoint – enabling everyone to assert its successful response.

When executing all application’s tests (both unit and integration) from IntelliJ, the result is a concise and informative summary.

test-results

Furthermore, upon building the project with Gradle, it becomes evident that there are two separate tasks allocated for both unit and integration tests.

Starting a Gradle Daemon (subsequent builds will be faster)

> Task :test

pl.kurczyna.springit.SimpleSpec

  Test should add 2 numbers PASSED

SUCCESS: Executed 1 tests in 543ms


> Task :itest

OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
pl.kurczyna.springit.ITestManagement

  Test should return 200 status for management path: health PASSED

SUCCESS: Executed 1 tests in 2.5s


BUILD SUCCESSFUL in 14s
16 actionable tasks: 16 executed

4. Testing REST endpoints

If the application exposes REST endpoints, Spring Boot offers convenient tools to test them in the integration test context. In the block below, we have a simple REST controller featuring a single endpoint for calculating a square root of a given number.

@RestController
@RequestMapping("/api/math")
class MathController {

    @GetMapping("/sqrt/{number}")
    fun squareRoot(@PathVariable("number") number: Double): ResponseEntity<Double> {
        return ResponseEntity.ok(sqrt(number))
    }
}

To double check current endeavours, write a test that spins up the Spring Boot application and verifies whether the endpoint works properly.

In order to do that, you should enhance the `IntegrationTestBase` first.

@LocalServerPort
int appPort

@Autowired
TestRestTemplate restTemplate

`@LocalServerPort `injects the randomly selected port number, on which the application is running, into the variable required for subsequent use“`
@Autowired
TestRestTemplate restTemplate“` injects the Spring Boot utility `RestTemplate` which can be used for making REST calls to the application.

The test can take the following form:

class ITestMath extends IntegrationTestBase {

    def "should calculate square root"() {
        given:
        double number = 9.0

        when:
        def result = restTemplate.getForEntity("/api/math/sqrt/$number", Double.class)

        then:
        result.statusCode == HttpStatus.OK
        result.body == 3.0
    }
}

An actual `GET` request to the `/api/math/sqrt/9.0` endpoint is initiated using `TestRestTemplate`. Following this, several assertions can be conducted, including the verification of the response status code, response headers, or the body.

5. Database testing

One of the most common integrations for a number of applications is SQL database. In this section, let’s cover tools and techniques that one can use to be able to test this integration in a simple and effective fashion.

Required dependencies

We have to add some dependencies to our `build.gradle.kts` file:

implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("org.postgresql:postgresql:$postgresqlVersion")
implementation("org.liquibase:liquibase-core:$liquibaseVersion")

Database schema

You should now create a database schema for the application, initially requiring a single table named `users`. Liquibase, a widely utilised database schema change management tool, will be employed to execute the DB migration during application startup.

The sole task required is to specify migration scripts (`.yml` files) in the proper place, so they will be automatically picked up by Spring Boot when `liquibase-core` dependency is on our classpath.

The two files need to be created:

  • `src/main/resources/db/changelog/db.changelog-master.yaml`
    databaseChangeLog:
      - include:
          file: db/changelog/db.changelog-users.yaml
  • `src/main/resources/db/changelog/db.changelog-users.yaml`
    databaseChangeLog:
      - changeSet:
          id: create-table-users
          author: patrykkurczyna
          preConditions:
            - onFail: MARK_RAN
              not:
                tableExists:
                  tableName: users
          changes:
            - createTable:
                tableName: users
                columns:
                  - column:
                      autoIncrement: true
                      constraints:
                        nullable: false
                        primaryKey: true
                        primaryKeyName: user_pkey
                      name: id
                      type: BIGINT
                  - column:
                      constraints:
                        nullable: false
                      name: name
                      type: VARCHAR(255)
    

Users table has two columns:

  • `id` – primary key
  • `name` – varchar

The rows can be represented by this object:

data class User(
    val id: Long,
    val name: String
)

Upon the subsequent application startup, Spring Boot will attempt to execute this migration, thereby creating the `users` table.

Controller and repository

Let’s implement a simple CRUD API for the application. The API will be responsible for managing user entries in the SQL database. For illustrative purposes, PostgresQL will be used, but you can use any RDBMS of your choice.

UserRestController

@RestController
@RequestMapping("/api/users")
class UserController(private val repository: UserRepository) {

    @GetMapping
    fun getAllUsers(): ResponseEntity<List<User>> {
        val users = repository.getAll()
        return ResponseEntity.ok(users)
    }

    @PostMapping
    fun addOrUpdate(@RequestBody user: User): ResponseEntity<Unit> {
        repository.addOrUpdate(user)
        return ResponseEntity.status(HttpStatus.CREATED).build()
    }
}

The controller exposes two endpoints:

  • `GET /api/users ` – returns the list of all users from the DB
  • `POST /api/users` – adds the user to the DB when it does not exist, or updates existing entry

UserRepository

interface UserRepository {
    fun getAll(): List<User>
    fun addOrUpdate(user: User)
}

class DefaultUserRepository(private val jdbcTemplate: NamedParameterJdbcTemplate) : UserRepository {
    private companion object {
        const val GET_ALL_USERS_QUERY = "SELECT * FROM users;"
        const val UPSERT_USER_QUERY = """              
                    INSERT INTO users (id, name) values (:id, :name)
                    ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name
        """
    }

    override fun getAll(): List<User> =
        jdbcTemplate.query(GET_ALL_USERS_QUERY) { rs, _ -> User(rs.getLong("id"), rs.getString("name")) }

    override fun addOrUpdate(user: User) {
        jdbcTemplate.update(UPSERT_USER_QUERY, mapOf("id" to user.id, "name" to user.name))
    }
}

Thanks to the `spring-boot-starter-jdbc` you can use `NamedParamJdbcTemplate` to make database calls using simple queries and map the results to the User object.

Integration Test

Testing scenarios

Multiple testing scenarios can be defined for the two methods that our controller provides:

  1. List all users from the database
    • Prepare database so the table contains some users
    • Call the` GET /api/users` endpoint
    • Verify that the response body contains all the users
  2. Add new user
    • Call the `POST /api/users` endpoint
    • Verify the response status
    • Verify if the user is added to the DB
  3. Update the user
    • Prepare database so the table contains user with id `x`
    • Call the `POST /api/users` endpoint with a new user `name`
    • Verify the response status
    • Verify if the user name in the DB is updated

Database test setup

To execute the test scenarios described above, additional setup is required.

First, the task involves spinning up the actual PostgresQL database server to which our application connects. We could set up the server locally or use the database on our development server, however the primary goal of this setup is to ensure independence from the testing infrastructure. This is why Testcontainers will be employed. It’s a library that provides lightweight, throwaway containerised instances of common services, like databases, message buses or basically anything that can run in a Docker container.

  • It requires Docker to be installed
  • It supports many JVM testing frameworks like JUnit4, JUnit5 and Spock

The setup is very straightforward. You need one more dependency (Testcontainers Postgres Module) in the `itest` scope added in our Gradle script:

itestImplementation("org.testcontainers:postgresql:$testcontainersVersion")

In addition to that, you need to configure your application so it connects to the proper database server. This can be done by specifying the url in the `application-itest.yml` file. This approach allows for the separation of the application’s production configuration – where it should connect to the ‘real’ production database – from the test configuration.

spring:
  datasource:
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
    url: jdbc:tc:postgresql:15.3:///springit

The specification of a database driver (to utilise the `testcontainers` one) and the database URL is required. `springit` serves as the name of the database that is intended to be used.

Typically, when using Testcontainers, the lifecycle of the containers needs to be managed – start and stop them at a specific time, usually when the application starts and stops. However, the PostgresQL Testcontainers Module is a bit special and needs nothing beyond the earlier definitions. When setting up the DB driver and the url that points to Testcontainers Postgres, it is enough for Spring Boot to know that it should start the container before the application startup and close it after it shuts down.

Additional testing tools

Considering the testing scenarios we’ve outlined, it’s evident that a tool is required to assist in preparing the database before the tests and validating the database state after the tests. Various approaches can be employed for this purpose. I opted for creating a small utility class in Groovy that uses `NamedParameterJdbcTemplate` to make database calls.

You can ask why we cannot use the already existing `UsersRepository`. It can be done, but I prefer not to use application components in isolation, when I run integration tests. In fact, `UsersRepository` is indeed one of the components “under test”.

First, let’s inject the `jdbcTemplate` in the `IntegrationTestBase`:

@Autowired
private NamedParameterJdbcTemplate jdbcTemplate

DbTestClient dbTestClient

def setup() {
    dbTestClient = new DbTestClient(jdbcTemplate)
}

And here’s our `DbTestClient`:

class DbTestClient {

    @Language("PostgreSQL")
    private static final String INSERT_USER = """
            INSERT INTO users (id, name) VALUES (:id, :name)
        """

    @Language("PostgreSQL")
    private static final String GET_USER_BY_ID =
            /
            SELECT *
            FROM users
            WHERE id = :id
        /

    private static def USER_ROW_MAPPER = new RowMapper() {
        @Override
        User mapRow(ResultSet rs, int rowNum) throws SQLException {
            return new User(
                    rs.getObject("id", Long.class),
                    rs.getString("name")
            )
        }
    }

    NamedParameterJdbcTemplate template

    DbTestClient(NamedParameterJdbcTemplate template) {
        this.template = template
    }

    void insertUser(Map args = [:]) {
        template.update(INSERT_USER,
                new MapSqlParameterSource([
                        id   : args.id ?: nextLong(),
                        name : args.name ?: 'John Doe'
                ])
        )
    }

    User getUserById(Long id) {
        try {
            return template.queryForObject(GET_USER_BY_ID, ['id' : id], USER_ROW_MAPPER) as User
        } catch (EmptyResultDataAccessException ignored) {
            return null
        }
    }
}

The details of this utility class won’t be explored, but the most important parts are the two methods it exposes:

  • `getUserById(Long id)` – fetches the user from the DB by its `id`
  • `insertUser(Map args = [:])` – inserts the user to the db, the input is a map of arguments for the columns of the table

Test implementation

class ITestUsers extends IntegrationTestBase {

    def "should retrieve user list"() {
        given: 'There is one user in the DB'
        Long id = nextLong()
        String name = 'Arthur Morgan'
        dbTestClient.insertUser(id: id, name: name)

        when: 'We fetch users from the API'
        def result = restTemplate.exchange('/api/users', HttpMethod.GET, null,
                new ParameterizedTypeReference<List<User>>() {})

        then: 'List containing one element is returned'
        result.statusCode == HttpStatus.OK
        result.body.size() == 1
        result.body.first() == new User(id, name)
    }

    def "should add new user"() {
        given: 'There is a new user to be added'
        Long id = nextLong()
        User user = new User(id, 'Micah Bell')

        when: 'We call the API to insert user'
        def result = restTemplate.postForEntity('/api/users', new HttpEntity(user), Unit.class)

        then: 'result is success'
        result.statusCode == HttpStatus.CREATED

        and: 'There is a new user in the DB'
        User inDb = dbTestClient.getUserById(id)
        inDb == user
    }

    def "should update user name"() {
        given: 'There is one user in the DB'
        Long id = nextLong()
        String name = 'Arthur Morgan'
        dbTestClient.insertUser(id: id, name: name)

        when: 'We call the API to update user'
        User user = new User(id, 'John Marston')
        def result = restTemplate.postForEntity('/api/users', new HttpEntity(user), Unit.class)

        then: 'result is success'
        result.statusCode == HttpStatus.CREATED

        and: 'Name of the user is updated'
        User inDb = dbTestClient.getUserById(id)
        inDb.name == 'John Marston'
    }
}

This represents the implementation of the defined testing scenarios. Spock allows the addition of comments for specific test sections, improving readability and making the code self-explanatory.

A critical point to keep in mind is that this is the interaction with an authentic database. Its lifecycle spans the whole integration tests suite run, meaning that DB tables, and more importantly its contents, are carried over from one test case to another and from one test class to another. Always be mindful of that particular aspect. For instance, you should not assume that the database is empty before the test execution; it might still contain some data from the previous runs. That’s why it’s considered a good practice to use random identifiers for your test entities. Hence, it is recommended to use `RandomUtils.nextLong()` every time you need to generate user id in the tests. Therefore, the rows will almost certainly not interfere with each other between different test runs.

6. External REST services

Another common integration point for most of the applications are external REST APIs. It would be highly beneficial to simulate and test various scenarios for the external calls made – from the happy path to different error scenarios, such as internal server errors or service being unavailable. To achieve this, it is advisable to mock the responses from external REST API, and there are several ways to accomplish this.

Firstly, there are services like: https://smartmock.io or https://mockable.io that provide very powerful tools for mocking and stubbing http requests, along with its own servers, therefore they can even be used from your applications running on DEV or STAGING environments.

There is, however, a more convenient utility that we can use in the integration tests context and it’s called WireMock. It’s an open-source tool that provides many different distributions, but also libraries that allow creating mock APIs.

Payment Service

In the illustrative scenario, to test a REST service for processing payments via Stripe, consider the following setup:

data class Payment(
    val amount: Long,
    val currency: Currency,
    val paymentMethod: String
)

interface PaymentService {
    fun makePayment(payment: Payment)
}

@Service
class StripePaymentService(
        private val template: RestTemplate,
        @Value("\${payment.url}") private val paymentServiceUrl: String
): PaymentService {
    override fun makePayment(payment: Payment) {
        template.postForEntity("$paymentServiceUrl/api/pay", HttpEntity(payment), Unit::class.java)
    }
}

Required bean definition:

@Bean
fun restTemplate(restTemplateBuilder: RestTemplateBuilder): RestTemplate =
    restTemplateBuilder
        .messageConverters(MappingJackson2HttpMessageConverter(jacksonObjectMapper()))
        .build()

The url (`payment.url`) for the service is defined in the `application.yml`:

payment:
  url: https://example.com

Integration Test

Testing scenarios

You could define two example testing scenarios for the payment service:

  1. Successful “make payment” call
    • Mock Stripe API to return success status code (e.g. HTTP 202)
    • Call `makePayment` method
    • Verify that Stripe API has been called exactly once
    • Verify that NO exception has been thrown
  2. Failed “make payment” call
    • Mock Stripe API to return error status code (e.g. HTTP 500)
    • Call `makePayment` method
    • Verify that Stripe API has been called exactly once
    • Verify that expected exception has been thrown

Test setup

For the application, only a single dependency is required to use WireMock.

itestImplementation("com.github.tomakehurst:wiremock-jre8-standalone:$wiremockVersion")

Now, it’s important to start the WireMock server before the tests are run. It can be done in the `IntegrationTestBase` class.

import static org.springframework.test.util.TestSocketUtils.findAvailableTcpPort

private static WireMockServer stripeServer
static int stripePort = findAvailableTcpPort() // WireMock server will run on the randomly chosen available port

// this method will run before all tests
def setupSpec() {
   stripeServer = new WireMockServer(stripePort)
   stripeServer.start() // starting the server
}

// this method will run after each test case
def cleanup() {
   stripeServer.resetAll() // reset all mappings (mocks)
}

// this method will run after all test cases
def cleanupSpec() {
   stripeServer.stop() // stop WireMock server
}

WireMock server is now up and running, but how to “tell” the application that it should use it? The accurate step includes configuring the `payment.url` property to be pointing to the WireMock server. There are multiple approaches to achieve this, but my preferred method is to use the port placeholder in `application-itest.yml`:

payment:
  url: http://localhost:${wiremock.stripePort}

You may ask, how does Spring Boot know what is the value of the `wiremock.stripePort` when it’s about to run the application? We can populate this property in the `IntegrationTestBase` using `TestPropertySourceUtils`:

static class PropertyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        void initialize(ConfigurableApplicationContext applicationContext) {
            String[] properties = ["wiremock.stripePort=$stripePort"]
            TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
                    applicationContext,
                    properties
            )
        }
    }

Also, ensure that `PropertyInitializer` is picked up by Spring during context initialization by adding `ContextConfiguration` annotation.

@ContextConfiguration(initializers = PropertyInitializer)
abstract class IntegrationTestBase extends Specification {
...

Now, the application is configured to use the WireMock server.

Defining mocks

To decouple WireMock internals from the integration test logic, create an utility class for mocking all Stripe requests. Let’s call it `StripeMock`.

class StripeMock {
    WireMockServer server
    ObjectMapper mapper

    StripeMock(WireMockServer server) {
        this.server = server
        this.mapper = new ObjectMapper()
    }

    def payRespondWithSuccess(Payment payment) {
        server.stubFor(post(urlEqualTo("/api/pay"))
                .withRequestBody(equalTo(mapper.writeValueAsString(payment)))
                .willReturn(aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withStatus(ACCEPTED.value())))
    }

    def payRespondWithFailure(Payment payment) {
        server.stubFor(post(urlEqualTo("/api/pay"))
                .withRequestBody(equalTo(mapper.writeValueAsString(payment)))
                .willReturn(aResponse()
                        .withStatus(INTERNAL_SERVER_ERROR.value())))
    }

    def verifyPayCalled(Payment payment, int times = 1) {
        server.verify(times, postRequestedFor(urlEqualTo("/api/pay"))
                .withRequestBody(equalTo(mapper.writeValueAsString(payment)))
        )
        true
    }
}

It offers 3 methods:

  • `payRespondWithSuccess `- creates a successful stub (HTTP 202 Accepted response) for the `POST /api/pay` call, we also assume the request body to be the `Payment payment` object serialised to json
  • `payRespondWithFailure `- creates a failure stub (HTTP 500 Internal Server Error response) for the `POST /api/pay` call, we also assume the request body to be the `Payment payment` object serialised to json
  • `verifyPayCalled `- verifies that the call to `POST /api/pay` has been made exactly x (variable `times`) times, with the proper body

You can now initialise the mock class in the `IntegrationTestBase`, so it can be used by all child test classes.

StripeMock stripeMock

def setup() {
   dbTestClient = new DbTestClient(template)
   stripeMock = new StripeMock(stripeServer) // initializing StripeMock with the WireMock server we created earlier
}

Test implementation

Having all those utilities in place makes it fairly easy to implement the integration test.

class ITestPayments extends IntegrationTestBase {

    @Autowired
    PaymentService paymentService

    def "should make a successful payment using Stripe"() {
        given: 'Payment is prepared'
        Payment payment = new Payment(100, Currency.getInstance('PLN'), "VISA")

        and: 'Stripe responds with success on new payment call'
        stripeMock.payRespondWithSuccess(payment)

        when: 'We make a payment'
        paymentService.makePayment(payment)

        then: 'No exception is thrown'
        noExceptionThrown()

        and: 'Stripe pay endpoint was called once with specific data'
        stripeMock.verifyPayCalled(payment, 1)
    }

    def "should throw exception when Stripe API call fails"() {
        given: 'Payment is prepared'
        Payment payment = new Payment(100, Currency.getInstance('PLN'), "VISA")

        and: 'Stripe responds with failure'
        stripeMock.payRespondWithFailure(payment)

        when: 'We make a payment'
        paymentService.makePayment(payment)

        then: 'An exception is thrown'
        thrown(HttpServerErrorException.InternalServerError)

        and: 'Stripe pay endpoint was called once with specific data'
        stripeMock.verifyPayCalled(payment, 1)
    }
}

7. Conclusion

This article implemented many integration tests covering the most common external services that most applications use, including databases and REST APIs.

We also looked at the exact setup that is needed to create an efficient integration tests suite for your application, using Gradle, Spock and Spring Boot.

However, this is certainly not everything, that this mighty setup empowers you to do. There is many more things that can be tested in a similar fashion, such as:

  • Kafka
  • Google Cloud Storage
  • AWS S3
  • AWS SQS
  • AWS SNS
  • AWS DynamoDB
  • Email Sending
  • Elasticsearch
  • … and many more

I will cover some of them in the Part II of this article, it will be coming soon!

I’m not saying goodbye, stay tuned! 🙂

Useful links

Written by Patryk Kurczyna
Kotlin/Java Developer
Published November 27, 2023