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 version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ignaciosuay</groupId>
    <artifactId>spring-scala-tests</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-scala-tests</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <scala.version>2.11.8</scala.version>
        <scala-test.version>3.0.1</scala-test.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- test dependencies -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.scalatest</groupId>
            <artifactId>scalatest_2.11</artifactId>
            <version>${scala-test.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.scala-tools</groupId>
                <artifactId>maven-scala-plugin</artifactId>
                <version>2.15.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.scalatest</groupId>
                <artifactId>scalatest-maven-plugin</artifactId>
                <version>1.0</version>
                <executions>
                    <execution>
                        <id>test</id>
                        <goals>
                            <goal>test</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- disable surefire for scala tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.7</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

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:

@RestController
@RequiredArgsConstructor
public class CustomerController {

    private final CustomerService customerService;

    @GetMapping("/customers/{id}")
    public Customer getCustomer(@PathVariable Long id){
        return customerService.findCustomer(id);
    }
}

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”.

@Service
public class CustomerService {

    public Customer findCustomer(Long id) {
        return new Customer(id, "Bob");
    }
}

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:

  1. Annotate the class with: @RunWith(classOf[SpringRunner]) and @WebMvcTest(Array(classOf[CustomerControler]))
  2. 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.
  3. 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.
  4. 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.
  5. and here is the code:

    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)))
      }
    
    }
    

    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:

    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"
        }
      }
    }
    

    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:

    object CustomerFixture {
    
    def aCustomer(id: Long = 1, name: String = "Bob") = new Customer(id, name)
    
    }
    

    which then I could use in any of my tests, such as:

    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
    

    Code

    Please find all the code used in this post in github

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.