Testing Spring boot with ScalaTest
Introduction
Spring boot tests provides great support for testing which combined with the sugar syntax form Scala makes testing more pleasant and less verbose.
In this article we will build a Rest Controller build in Java with Spring Boot and we will learn how to test it with ScalaTest.
Dependencies
We will need to add the following dependencies to our project: Spring boot, Scala and ScalaTest.
As we are building this project using Maven, the pom file looks like this:
[xml]
[/xml]
As recommended by the ScalaTest documentation, we should disable the surefire plugin.
Sample Controller and service
In order to have a controller to test, we will create a simple Customer Rest controller which, given a customer id, calls the findCustomer method in the CustomerService.
Here is the CustomerController:
[java]
@RestController
@RequiredArgsConstructor
public class CustomerController {
private final CustomerService customerService;
@GetMapping(“/customers/{id}”)
public Customer getCustomer(@PathVariable Long id){
return customerService.findCustomer(id);
}
}
[/java]
In a real case scenario the CustomerService will use a repository to query the customer from a database.
In order to make this article as simple as possible the CustomerService will always return the same customer with id=1 and name=”Bob”.
[java]
@Service
public class CustomerService {
public Customer findCustomer(Long id) {
return new Customer(id, “Bob”);
}
}
[/java]
Unit testing the controller with @WebMvcTest
The first thing that we may want to test is the Controller in isolation. As you can see in the CustomerController, this class has a dependency to the CustomerService.
For the unit test we are going to mock the CustomerService using the @MockBean annotation provided by Spring.
Spring provides the @WebMvcTest annotation which is very useful when you are only interested in instantiating the web layer and not the whole Spring context.
This annotation will only load configurations relevant to MVC tests (@Controller, @RestController., @ControllerAdvice…) but will disable full auto-configuration (@Component, @Service or @Repository won’t be instantiated).
Here are the steps that I followed:
- Annotate the class with: @RunWith(classOf[SpringRunner]) and @WebMvcTest(Array(classOf[CustomerControler]))
- Autowired MockMvc. Note that @WebMvcTest auto-configures MockMvc without needing to start a web container. By default, @WebMvcTest loads all the controllers but in this case I am just interested in loading 1 controller (CustomerController), so I added the name of the controller to the annotation.
- Mock the customer service. Due to the fact that @WebMvcTest doesn’t load any @Service configuration, we need to mock the CustomerService by using the @MockBean annotation.
- Load the application context by defining a TestContextManager instance. TestContextManager is the main entry point into the Spring TestContext Framework, which provides support for loading and accessing application contexts, dependency injection of test instances, transactional execution of test methods, etc.
and here is the code:
[java]
package com.ignaciosuay.springscalatests.resource
import com.ignaciosuay.springscalatests.model.Customer
import com.ignaciosuay.springscalatests.service.CustomerService
import org.junit.runner.RunWith
import org.mockito.Mockito._
import org.scalatest.{FunSuite, GivenWhenThen}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.test.context.TestContextManager
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.{content, status}
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.hamcrest.Matchers.is
@RunWith(classOf[SpringRunner]) //1
@WebMvcTest(Array(classOf[CustomerController]))
class CustomerControllerMvcTest extends FunSuite with GivenWhenThen {
@Autowired //2
var mvc: MockMvc = _
@MockBean //3
val customerService: CustomerService = null
//4
new TestContextManager(this.getClass).prepareTestInstance(this)
test(“find a customer”) {
Given(“a customer id”)
val id = 1l
val expectedCustomer = new Customer(id, “Bob”)
when(customerService.findCustomer(id)).thenReturn(expectedCustomer)
When(“a request to /customers/{id} is sent”)
val result = mvc.perform(get(s”/customers/$id”).contentType(“application/json”))
Then(“expect a customer”)
result
.andExpect(status.isOk)
.andExpect(jsonPath(“$.id”, is(1)))
.andExpect(jsonPath(“$.name”, is(expectedCustomer.getName)))
}
}
[/java]
Integration testing with SpringBootTests
In this section, we will go over creating an integration test using SpringBootTest and TestRestTemplate.
TestRestTemplate is a convenient alternative of RestTemplate that is suitable for integration tests.
The difference between SpringBootTest and WebMvcTest is that while SpringBootTest will start your full application context and the application server (tomcat), WebMvcTest will only load the controllers you have defined and therefore it is much faster.
Please follow these steps:
1) Annotate the test class with: @RunWith(classOf[SpringRunner]) and @SpringBootTest(webEnvironment = RANDOM_PORT).
As you can see in the annotation we will start the Tomcat application server using a random port.
2) Autowired TestRestTemplate.
3) Because we are starting tomcat using a random port we need to inject the port using the @LocalServerPort annotation.
4) Build the url using the random port value.
5) Test the endpoint using testRestTemplate.
In this case, I am using the getForEntity method which retrieves a Customer by doing a GET request on the specified URL.
and the code:
[java]
package com.ignaciosuay.springscalatests.resource
import com.ignaciosuay.springscalatests.model.Customer
import org.junit.runner.RunWith
import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.embedded.LocalServerPort
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.test.context.TestContextManager
import org.springframework.test.context.junit4.SpringRunner
@RunWith(classOf[SpringRunner])
@SpringBootTest(webEnvironment = RANDOM_PORT)
class CustomerControllerIT extends FeatureSpec with GivenWhenThen with Matchers {
@Autowired
var testRestTemplate: TestRestTemplate = _
new TestContextManager(this.getClass).prepareTestInstance(this)
@LocalServerPort
val randomServerPort: Integer = null
val baseUrl = s”http://localhost:$randomServerPort”
feature(“Customer controller”) {
scenario(“Find customer by id”) {
Given(“a customer id”)
val id = 1
When(“a request to /customers/{id} is sent”)
val url = s”$baseUrl/customers/$id”
val response = testRestTemplate.getForEntity(url, classOf[Customer])
Then(“we get a response with the customer in the body”)
response.getBody.getId shouldBe 1
response.getBody.getName shouldBe “Bob”
}
}
}
[/java]
Summary
In this post, we implemented a unit test and an integration test using Spring boot and Scala Test.
I personally find writing tests with Java too verbose and I like the sugar syntax that other languages like Scala, Groovy or Kotlin provide.
One of the main reasons I prefer to use any other JVM language is because they allow me to create fixtures with named and default parameters. For instance, I could create a fixture for my customer, such as:
[java]
object CustomerFixture {
def aCustomer(id: Long = 1, name: String = “Bob”) = new Customer(id, name)
}
[/java]
which then I could use in any of my tests, such as:
[java]
aCustomer() //returns a customer with the default values (id = 1 and name = 100)
aCustomer(name = “Mike”) //returns a customer with id 1 and name Mike
aCustomer(100, “Mike”) // returns a customer with id 100 and name Mike
[/java]
Code
Please find all the code used in this post in github
Recent Comments