Put Request in Spring - Testing

Test GET Request in Spring Boot Kotlin Application

In this article we will show you how to perform a GET request in Spring using RestTemplate. We will take you through a simple integration test example to be able to test a GET endpoint in a Spring Boot application.

If you are interested in an example using Java, you can find the equivalent article in Java here.

Let’s start!

Setup

For the examples shown throughout this article, we will be using the same codebase we’ve shown in our previous Spring articles. You can see the latest version in our article “Exception Handling in Spring Rest Services in Kotlin”.

In this article will look at our GET endpoint only, and write an integration test for it. Our GET endpoint looks like this:

    @GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE])
    fun findCustomer(@PathVariable("id") id: Long): ResponseEntity<Customer> {
        return repository.findById(id)
            .onFailureDo { e ->
                when (e.exception()) {
                    is CustomerNotFound -> ResponseEntity.notFound().build()
                    else -> ResponseEntity.internalServerError().build()
                }
            }.response() as ResponseEntity<Customer>
    }

As you can see in this example, we are using a custom Either monad to handle the responses, but for the purpose of this article you won’t have to worry about implementation details. Integration testing is a kind of black box test, we only care about the response, so the internals of the service should be completely opaque for the purposes of testing.

One more thing you’ll need in your application to be able to test it, is the Spring Boot test dependencies. You can import them by importing the Spring Boot starter test package in your build.gradle file:

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation("org.springframework.boot:spring-boot-starter-web")
        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

You will also notice that we are not specifying the version in our dependency import, this is because we have previously defined the Spring Boot version in our plugins section:

plugins {
	id("org.springframework.boot") version "2.7.6"
	id("io.spring.dependency-management") version "1.0.15.RELEASE"
	kotlin("jvm") version "1.6.21"
	kotlin("plugin.spring") version "1.6.21"
}

Now that we have the very basics of our Spring Boot application, we can now start with out test. If for any reason you need help to set up your initial application, you can check our article “How to Bootstrap a Spring Boot Application”.

Let’s start looking at our integration test then!

Test Get Request in Spring

There are different ways to test a GET request in Spring, depending on what your needs are. We will start by looking at getForEntity method, let’s see how it works.

Using GetForEntity method

The getForEntity method allows us to easily test a GET endpoint in our application by sending a simple GET request in Spring, although it has some limitations. Let’s see how it works:

package com.theboreddev.springbootapp

import org.assertj.core.api.AssertionsForClassTypes.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CustomerControllerIntegrationTest {
    @LocalServerPort
    private val port = 0

    @Autowired
    private lateinit var template: TestRestTemplate;

    @Test
    fun shouldReturn404WhenCustomerForAGivenIdDoesNotExist() {
        val response: ResponseEntity<Customer> = template.getForEntity(
            String.format("http://localhost:%d/api/customers/4", port),
            Customer::class.java
        )
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND)
    }
}

You will see that initially we have just added an integration test to check that, when the customer does not exist, we receive the expected 404 (NOT_FOUND) response.

Once we have this initial test passing, we now need to test that an existing customer can be retrieved. To do so, we’ll provide a fake customer repository that will always contain a predefined number of customers, just for the purpose of easily testing our GET endpoint in this article. In a real application we’d normally populate data using our POST endpoint first, and the utilise a GET request in Spring to check that the data has been persisted.

Our fake customer repository will look something like this:

@Component("inMemoryCostumersRepository")
class InMemoryCustomerRepository(val configuration: DatabaseConfiguration): CustomersRepository {

    private val customers: MutableMap<Long?, Customer> = mutableMapOf(
        1L to Customer(1L, "John Smith", 33),
        2L to Customer(2L, "Leonard Randolph", 42),
        3L to  Customer(3L, "Margaret McGuinness", 27)
    )

    override fun create(customer: Customer): Either<Exception, Customer> {
        val created = customer.copy(id = customers.keys.size.toLong())
        customers[created.id] = created
        return created.success() as Either<Exception, Customer>
    }

    override fun findById(id: Long): Either<Exception, Customer> {
        return ((
                customers[id]?.success() ?:
                    CustomerNotFound("Could not found customer $id", id).failure()
                ) as Either<Exception, Customer>)
    }
}

As you can see, we initialise our map of customers with three customers that we will use for our tests to make things simpler.

Now that we have some pre-populated data, we can proceed with our next test!

    @Test
    fun shouldReturn200WhenCustomerForAGivenIdExists() {
        val response = template.getForEntity(
            String.format("http://localhost:%d/api/customers/3", port),
            Customer::class.java
        )
        assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
        assertThat(response.body).isEqualTo(Customer(3L, "Margaret McGuinness", 27))
    }

In this test, we are fetching a customer by a customer ID we know is already present in our “database”. What we expect in this case is a status code in our response of 200 (OK) and a response body containing the corresponding JSON for our customer.

We mentioned before that getForEntity has some limitations, one of them is the inability to include headers in our request. In cases where we need to pass headers in our integration test, we can use exchange method present in TestRestTemplate.

At the moment we support JSON format only, what if we wanted to support a different media type like XML? How would our GET request in Spring look like?

GET Request in Spring - Kotlin
Photo by Ferenc Almasi on Unsplash

Using Exchange Method

To be able to understand how to use this method, we are going to change our example slightly. We are going to modify our GET endpoint so it can produce either JSON or XML.

To achieve this, we’ll just have to add XML to the produces argument in our GetMapping annotation:

@GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE])

This means that, in order to test both JSON and XML responses, we’ll need to send an Accept header in our test. Let’s see how to do this!

    @Test
    fun shouldReturn200WhenCustomerForAGivenIdExists() {
        val headers = HttpHeaders()
        headers.add("Accept", "application/json")
        val response = template.exchange(
            String.format("http://localhost:%d/api/customers/3", port), HttpMethod.GET, HttpEntity<HttpHeaders>(headers),
            Customer::class.java
        )
        assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
        assertThat(response.body).isEqualTo(Customer(3L, "Margaret McGuinness", 27))
        assertThat(response.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON)
    }

You can notice how now we’re using exchange method in our test. On top of that, we are now checking Content-Type HTTP header to test that the response is in JSON format.

What if we want to test that we can request XML instead? This is as simple as the following, let’s add a new test:

    @Test
    public void shouldReturn200WhenCustomerForAGivenIdExistsInXmlFormat() {

        final HttpHeaders headers = new HttpHeaders();
        headers.add("Accept", MediaType.APPLICATION_XML_VALUE);

        final ResponseEntity<Customer> response = template.exchange(String.format("http://localhost:%d/api/customers/3", port), HttpMethod.GET, new HttpEntity<>(headers), Customer.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isEqualTo(new Customer(3L, "Margaret McGuinness", 27));
        assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_XML);
    }

If we run our test, it will fail initially with a server error.

Why is that? This is because Spring cannot find a converter to map from our object to XML format. How can we fix this then? Let’s see how!

The only thing we will have to do to fix this issue, is to add a Jackson dependency:

implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.1")

This will include the right converter to support XML in our GET request in Spring Boot! If you run the test again, you will see how it will now pass.

Conclusion

In this article we’ve learned the two ways we have to write a simple integration test for a GET request in a Spring Boot Kotlin application. We’ve also seen how to support two different content types in our endpoints in a very easy manner.

This is all from us today! We hope you’ve found this article useful and easy to read.

Looking forward to seeing you with us very soon again! Please follow us if you want to subscribe to more of our content!

Thanks for reading us!