Integration testing driving seamless software performance

This article provides an in-depth look at the role of integration testing within the software development process, particularly within the banking sector. It emphasizes the strategic importance of integration testing in managing the complexities of evolving banking systems.

By examining how integration testing can serve as a competitive differentiator and enhance customer experiences, the discussion highlights its value. Additionally, the article explores how integration testing contributes to the wider goals of digital transformation by facilitating the seamless integration of various digital platforms.

As a QA Engineer within a bank and actively engaged in integration testing alongside my team, we’ve come to realize that integration testing is not merely a quality assurance measure but rather a strategic imperative.

With the ongoing evolution of banking systems to embrace digital advancements and expand service offerings, the intricacy of software architecture intensifies.

Integration testing plays a pivotal role in ensuring the seamless operation of interconnected systems, thereby enhancing customer experiences and optimizing banking processes for greater efficiency.

In my experience, within the competitive realm of banking, I’ve observed that integration testing offers a distinct competitive edge. Through my involvement in rigorously testing the integration of new features or updates, I’ve witnessed firsthand how banks can swiftly respond to evolving market demands while upholding the stability and security of their services.

This proactive stance effectively mitigates the risk of service disruptions, ultimately safeguarding the bank’s reputation and fostering trust among customers.

Furthermore, I’ve found that integration testing is in harmony with the overarching objective of digital transformation in the banking sector. It facilitates the smooth integration of online banking platforms, mobile apps, and other digital channels, empowering customers to conveniently and securely manage their finances.

As I’ve witnessed, as customer expectations continue to evolve, a bank’s capability to provide a seamless, integrated banking experience becomes paramount for both retaining existing customers and attracting new ones in a fiercely competitive market.

In the forthcoming example, I aim to illustrate these principles by demonstrating the integration of WireMock, Kotlin, and Serenity REST.

Wiremock

Imagine you’re working on a software development project and find yourself in the midst of integration testing, and one of the external services your system interacts with isn’t ready for testing yet. However, you need to proceed with your tests to validate the system’s functionality.

This is where WireMock comes into play because we can simulate the behavior of that external service accurately and in a controlled manner, allowing us to continue our integration tests without relying on the availability of the real service.

It’s like having our own testing lab isolated from the outside world, where we can test our system anytime, regardless of whether external services are available or not. It’s truly a lifesaver in situations like this.

Reasons to use WireMock:

Standalone Service: WireMock can be used as a standalone service and can be configured via its RESTful API. This makes it language-agnostic and usable in any test setup.

Fault Injection: WireMock allows you to test your service’s resilience by simulating faults like network errors, slow responses, etc.

Stateful Behavior: WireMock can be configured to simulate stateful behavior, allowing you to test more complex scenarios.

Easy Integration with JUnit: WireMock provides a JUnit rule that spins up the server before each test method and shuts it down afterward.

Flexible Request Matching: WireMock allows you to match requests based on URL, headers, and body content.

Dynamic Responses: WireMock allows you to dynamically generate responses, enabling you to simulate almost any kind of system behavior.

Let’s create a stub for a banking API that returns details about a customer’s savings account. First, we’ll create a JSON file for the response body. This file could be in wiremock/__files/savings_account.json and could have content like this:

{
    "id": "123",
    "balance": 1000.0
}

This file represents a savings account with an id of “123” and a balance of 1000.0.

Then, we’ll set up a stub in wiremock/mappings/savings_account.json that responds to GET requests to the URL “/savings_account/123” with a 200 HTTP response and the content of the savings_account.json file as the response body.

{
    "request": {
     "method": "GET",
     "url": "/savings_account/123"
    },
    "response": {
      "status": 200,
      "bodyFileName": "savings_account.json",
      "headers": {
       "Content-Type": "application/json"
      }
   }
}

With this setup, when your application makes a GET request to “/savings_account/123”, WireMock will respond as if it were the actual banking service, returning the details of the savings account with id 123. This can be useful for testing how your application handles interaction with the banking service without having to interact with the actual service.

Kotlin

Here’s an example of a Kotlin class that uses SerenityRest to consume the stub. We’ll use JUnit 5’s @ParameterizedTest to test multiple scenarios.

data class SavingsAccount(val id: String, val balance: Double)

Next, we’ll create a test class that uses SerenityRest to consume the stub:

import net.serenitybdd.rest.SerenityRest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

class SavingsAccountTest {

    @ParameterizedTest
    @ValueSource(strings = ["123", "456", "789"])
    fun `test get savings account`(accountId: String) {
        val expectedBalance = 1000.0

        val response = SerenityRest.given()
            .baseUri("http://localhost:8080") 
            .basePath("/savings_account/$accountId")
            .get()

        assertEquals(200, response.statusCode)

        val savingsAccount = response.`as`(SavingsAccount::class.java)

        assertEquals(accountId, savingsAccount.id)
        assertEquals(expectedBalance, savingsAccount.balance)
    }
}

In this example, the test get savings account function is a parameterized test that runs once for each account ID provided in the @ValueSource annotation. It uses SerenityRest to send a GET request to the savings account endpoint for the given account ID, and then it asserts that the response status code is 200 and that the returned savings account has the expected ID and balance.

The SavingsAccountTest class contains a parameterized test that sends a GET request to the “/savings_account/{accountId}” endpoint for each account ID provided in the @ValueSource annotation. The test then asserts that the response status code is 200 and that the returned savings account has the expected ID and balance.

If the stub is correctly set up to return the expected savings account details for the account ID “123”, then the test for this account ID will pass. The output for this test will not specifically mention this account ID, but you will see that one test has passed.

However, if the stub is not set up to handle the account IDs “456” and “789”, then the tests for these account IDs will fail. The output for these tests will indicate that the tests have failed and will include the account IDs for which the tests failed.

Here’s an example of what the output might look like:

[ERROR] Tests run: 3, Failures: 2, Errors: 0, Skipped: 0, Time elapsed: 0.032 s <<< FAILURE! - in SavingsAccountTest
[ERROR] test get savings account[1]  Time elapsed: 0.005 s  <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <200> but was: <404>
[ERROR] test get savings account[2]  Time elapsed: 0.005 s  <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <200> but was: <404>

This output indicates that the test was run 3 times and that 2 of the test runs failed. The failed test runs are for the account IDs “456” and “789”. The test for the account ID “123” passed, but this is not explicitly mentioned in the output. The output shows that the expected HTTP status code was 200, but the actual status code returned by the stub was 404 for the account IDs “456” and “789”.

How could we structure our Kotlin code by following good practices?

We can divide the code into three classes: SavingsAccount, SavingsAccountService, and SavingsAccountTest.

First, the SavingsAccount class that represents a savings account:

data class SavingsAccount(
    val id: String,
    val balance: Double
)

Then, the SavingsAccountService class that interacts with the endpoint:

import net.serenitybdd.rest.SerenityRest

class SavingsAccountService {

    fun getSavingsAccount(accountId: String): SavingsAccount {
        val response = SerenityRest.given()
        [...]

        return response.`as`(SavingsAccount::class.java)
    }
}

Finally, the SavingsAccountTest class that contains the tests:

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvFileSource

class SavingsAccountTest {

    private val service = SavingsAccountService()

    @ParameterizedTest
    @CsvFileSource(resources = ["/account_ids.csv"])
    fun `test get savings account`(accountId: String, expectedBalance: Double) {
        val savingsAccount = service.getSavingsAccount(accountId)

        assertEquals(accountId, savingsAccount.id)
        assertEquals(expectedBalance, savingsAccount.balance)
    }
}

In this code, the test logic has been separated from the endpoint interaction logic, which makes the code easier to read and maintain.

In JUnit 5, you can use the @CsvSource or @CsvFileSource annotation to provide values to a parameterized test from an external source. Here’s how you can do it with @CsvFileSource. First, you’ll need a CSV file with the account IDs. For example, you could have an account_ids.csv file with the following content:

23,1000.0
456,2000.0
789,3000.0

Each line of the file represents a set of parameters for the test. In this case, the first value on each line is the account ID and the second value is the expected balance for the account.

Finally, if you wish to disseminate the report artifact subsequent to the execution of the class in Azure Pipelines, you may proceed with the following steps:

  1. Ensure your test is set up to generate a report. This can vary depending on the testing framework you’re using. In the case of JUnit, for example, you can configure the Maven Surefire plugin to generate an XML report.

  2. In your Azure Pipelines configuration file (e.g., azure-pipelines.yml), you’ll need to add a task to publish the report artifact. You can do this using the PublishPipelineArtifact@1 task.

Here’s an example of what your azure-pipelines.yml file might look like:

trigger:
- master
 pool:
 vmImage: 'ubuntu-latest'
 steps:
 - task: Maven@3
 inputs:
 mavenPomFile: 'pom.xml'
 mavenOptions: '-Xmx3072m'
 javaHomeOption: 'JDKVersion'
 jdkVersionOption: '1.11'
 jdkArchitectureOption: 'x64'
 publishJUnitResults: true
 testResultsFiles: '**/surefire-reports/TEST-*.xml'
 goals: 'test'
 - task: PublishPipelineArtifact@1
 inputs:
 targetPath: '$(Pipeline.Workspace)/target/surefire-reports'
 artifact: 'TestReport'
 publishLocation: 'pipeline'

Conclusion

Integration testing emerges not just as a quality assurance measure but as a strategic imperative within the evolving landscape of banking systems. As digital advancements reshape customer expectations and banking services, the intricate interconnectedness of systems demands meticulous testing practices. Integration testing, exemplified by tools like WireMock and practices in Kotlin, stands at the forefront, ensuring seamless operation and resilience in the face of dynamic market demands.

By embracing these practices, banks not only safeguard their reputation and foster customer trust but also pave the way for sustained innovation and growth. As we navigate the complexities of modern banking, integration testing remains a cornerstone, driving the pursuit of excellence and delivering unparalleled customer experiences in an ever-changing financial ecosystem.

Thanks for reading.
Now let's get to know each other.

What we do

WAES supports organizations with various solutions at every stage of their digital transformation.

Discover solutions

Work at WAES

Are you ready to start your relocation to the Netherlands? Have a look at our jobs.

Discover jobs

Let's shape our future

Work at WAES

Start a new chapter. Join our team of Modern Day Spartans.

Discover jobs

Work with WAES

Camilo Parra Gonzalez

Camilo Parra Gonzalez

Account Manager