The single most useful skill in software development, is writing effective Tests?

Olsi Seferi
10 min readOct 22, 2020
Stock image by Pexels

Yeah, writing clean and maintainable code is important, knowing what algorithms and tech stack to use where is also important but when I first saw Vlad’s post it got me very curious so I started thinking…
How much time does automated testing save on the overall development time?

According to testim.io research:

“Automation testing is five times faster (maybe more depending on the test scenario) and more efficient than manual testing.”

so we can safely say that writing effective unit and integration tests is a pretty useful skill 😁.

Now, let’s clear the confusion a little, there a lot of automated test types, and each one of them has a certain goal :

Unit Tests

The goal for this type of test is to ensure that individual components (hence units) of the software are tested and work as they should. These tests are usually written during the development phase.

Smoke Tests

If you ask anyone that has been involved in plumbing about the term “smoke tests”, they will tell you that this is something that they do to the pipes to ensure that they are ready to be installed. More precisely, Smoke testing is the process of injecting artificially produced smoke into a blocked off pipeline segment to see where the smoke emerges, this way they can ensure there are no leaks before the installation.

Functional Tests

Functional testing involves almost all the parts of the software like UI (User Interface, API, Database, Security, etc. The purpose is to test each function of the software application and making sure that what the system does is align with that the business requirements are.

Integration Tests

Putting together application modules and testing them as a group, would be the goal for this type of testing. These tests make sure that the components are integrated correctly and there’s no problem with the data communication between one another.

Regression tests

To put it simply, regression tests make sure that nothing is broken after changing/implementing code. Regression testing is usually carried out after:

  • New features are added.
  • Bug Fixes
  • Refactoring and code addition or optimization.

Alright, enough theory, let’s get to the good part.

Before we begin testing we need to create a spring boot project on which our test suite will be based on. We will use a simple event management software which will be based on Rest API. Since we are going to focus on unit and integration tests only for the backend part there is no need to create a front-end application.

First things first, project structure:

There three main layers:

  • Controller (which will handle the HTTP mapping)
  • Service (which contains the business logic)
  • Repository (which handles the database communication)
    Also, we have created an additional package called Exception, which will contain our custom build EventException which extends java.lang.RuntimeException

Dependencies:

  • Mapstruct: Used to automatically create interface-based mappers to map values between DTO and Models
  • Lombok: Useful plugin that generates boilerplate code like getter, setters, constructors, etc. using annotations
  • PostgreSQL : Production database
  • H2: Test environment database (in-memory)
  • Spring Boot Data JPA: creates ready-made API for the repository layer, based on the Entity Model.
  • Spring Boot Starter Jersey: for building RESTful web applications using JAX-RS and Jersey.
  • JUnit Jupiter: Unit testing framework. (It is included on spring boot)

For dependency management we will use Maven and below you will find all the dependencies used on this project:

<dependencies><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>${mapstruct.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jersey</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies>

Also since we are using code generators like MapStruct and Lombok we need to add some extra configurations:

<build><plugins><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>${jacoco.version}</version><executions><execution><id>default-prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>default-report</id><phase>prepare-package</phase><goals><goal>report</goal></goals></execution></executions><configuration><excludes><!-- Exclude class from test coverage --><exclude>**/*Model.*</exclude><exclude>**/*Dto.*</exclude><exclude>**/service/mapper/**</exclude><exclude>TestsuiteApplication.*</exclude></excludes></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><annotationProcessorPaths><path><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${mapstruct.version}</version></path><path><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></path></annotationProcessorPaths></configuration></plugin></plugins></build>

It’s a good practice to save the versions as properties and access them using selectors:

<properties><java.version>14</java.version><mapstruct.version>1.4.0.Final</mapstruct.version><jacoco.version>0.8.6</jacoco.version></properties>

For the properties configuration we are going to use three files:

  • application.yml (global configurations)
  • application-dev.yml (configurations only for dev profile)
  • application-test.yml (configurations only for test profile)

So It’s time to design the Entity Model:

EventModel.java

In this example we will be using a custom sequence which will create the Ids automatically for us, it will start at 6 and increment by 1. (We will create the first 5 rows manually and import them at the initial startup. They will be used for integration tests)

Then we create the repository layer by extending the JpaRepository interface and passing the Entity class (EventModel.java) and the ID class (Long.class) as parameters:

IEventRepository.java

Since it's a bad practice to expose the EntityModel to the world we will create a DTO (Data Transfer Object) and a mapper that will map the values between models and DTOs.

EventDto.java
CreateEventDto.java
IEventMapper.java — *Note: in case the two fields have the same name the mapping will be done automatically, but for tutorial’s sake we mapped all the fields.

Remember to annotate the mapper with the required annotation and pass the correct component model when using Spring:

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)

Next, we have the service layer, the place where the magic happens 😁 :

We annotate the class with @Service annotation and auto-wire all the required dependencies on the constructor:

EventServiceImpl.java

and here’s an example of how to use our custom mappers and custom Exception in the method:

findById method uses our custom mapper (IEventMapper) and throws our custom exception (EventException)

Finally, the last layer which is the EventController, the root endpoint structure will be:

/api/v1/events

And the endpoints are as follow:

  • GET /api/v1/events | (Find all events)
  • GET /api/v1/events/{id} | (Find event)
  • POST /api/v1/events | (Create event)
  • PUT /api/v1/events/{id} | (Update event)
  • PATCH /api/v1/events/{id}/start | (Start event) (Partial Update)
  • PATCH /api/v1/events /{id}/finish| (Finish event) (Partial Update)
  • DELETE /api/v1/events/{id} | (Delete event)
EventController.java

Now we are ready to start writing our unit and integration tests, you can take a look at the full project on Github.

Now let’s proceed with testing.

The first test type that we will examine is Unit Testing, these are usually written in the development phase so they are also the first tests to be run.

Writing a Unit Test for REST Controllers

  • First, we create a class named EventControllerTest
  • We annotate it with the corresponding annotation provided by Spring
@WebMvcTest(controllers = EventController.class)
public class EventControllerTest {

What this annotation does is, it creates a context with the minimum required classes for the REST layer to work and bootstrap the controllers passed as parameter.

  • Next, we need a client that is capable of invoking the endpoint and for that, we are going to use MockMvc.class from org.springframework.test.web.servlet and we are going to auto-wire it.
@Autowiredprivate MockMvc mockMvc;
  • We will also need ObjectMapper.class to convert the objects to JSON strings so we can use it on the HTTP requests.
@Autowiredprivate ObjectMapper objectMapper;
  • Lastly, we need to mock the below layer which the Service, so it completely isolated.
@MockBeanprivate IEventService eventService;

What this does is inject a class in which we have complete control of, hence a Mock.

Ok, now we are ready for the test case. Every test case method should be annotated with @Test and must be public void.

A full test would look like this:

Where :

  • when is static method imported from static org.mockito.Mockito.when; . It used for stubbing methods, meaning when you want the mock to return a particular value when a particular method is called.
when(eventService.createEvent(any(CreateEventDto.class))).thenReturn(new EventDto(1L, "Created", "Created", null, null));

OR

when(x method is called).thenReturn(y a hardcoded value)

  • any(Class) is also a static method that matches any object of a given type, excluding nulls. [used as a generic parameter for method stubs]
  • mockMvc.perform(method) invokes the method which is mapped to that URL ‘/api/v1/events’ for that HTTP method ‘POST’, [EventController.findAllEvents()]
  • content(body) set the Requests body
  • objectMapper.writeValueAsBytes() returns the object as byte array [that we can later use on the content method]
  • andExpect as the name suggests, performs an expectation, meaning when the request is performed it will produce some expected results like status code.
  • status().isCreated() used inside andExpect() method, it evaluates that the request will return status code ‘201 Created’ if it’s performed correctly otherwise the test will fail right there.
  • objectMapper.readValue(byteArray, class), will read the byte array and map the values to the class passed as parameter.
  • assertNotNull(object) much like andExpect this performs an expectation or in other words an assertion. It checks that the return object is not null and it fails the test otherwise.

There are many assertions that JUnit offers like

assertEquals, assertFalse, assertNull, assertThrows, etc. you can find the full list here.

  • Lastly, verify(), a static method used to verify that a certain method was stubbed.

To run unit tests using maven you can run the following command:

mvn clean install test

OR, you can run them using your IDE, right-click on the class > Run as> Junit Test (Eclipse)

Running a unit test from eclipse.
JUnit tests results (Eclipse)

Test Results will be available under /target/site/jacoco

Writing Integration Tests for Rest Controllers

When writing integration tests I like to create my own custom annotation that contains the spring configurations, this way I can group them all to a single place and make the tests a little more readable. So let’s begin:

  • Create a custom annotation IntegrationTestConfig (remember annotation syntax is @interface )
  • Add the needed spring configurations:
Custom Annotation containing all the spring configurations needed to run a test.

The reason we are not using @WebMvcTest annotation is that it comes bundled with a lot of other unnecessary configurations, also this way we can bootstrap it using our application-test.yml properties.

  • Next, we create our Integration Test Class and put our custom annotation :
@IntegrationTestConfigpublic class EventControllerIT {
  • Now we need to inject our dependencies, for this integration test we will need two: an Http Client, and an Object to JSON string converter. We will use the same ones as we did on the unit test.
Integration Test dependencies
  • It's time to write our test cases. We will use the event update API, and there are two main scenarios:
  1. The user updates the event successfully and the correct object is returned.
  2. The event by that ID does not exist and an exception is thrown.

Now, remember when we designed the model? We started the id sequence from 6, and that's because the first 5 rows will be imported by a script on startup and we are going to use this data for testing. This way we have complete control over the database data.

  • To achieve this create a file mock-data.sql under /src/main/resources and insert the following values:
INSERT INTO public.events(id, name, description, started_at, finished_at)VALUES (1, 'Thinktech IoT Hackathon #1','ThinkTech IoT Hackathon organized by Landmark Technologies and Moveo Albania.', null, null);INSERT INTO public.events(id, name, description, started_at, finished_at)VALUES (2, 'Thinktech IoT Hackathon #2','ThinkTech IoT Hackathon organized by Landmark Technologies and Moveo Albania.This is the second Edition.', null, null);INSERT INTO public.events(id, name, description, started_at, finished_at)VALUES (3, 'Thinktech IoT Hackathon #3','ThinkTech IoT Hackathon organized by Landmark Technologies and Moveo Albania.This is the third Edition.', null, null);INSERT INTO public.events(id, name, description, started_at, finished_at)VALUES (4, 'Thinktech IoT Hackathon #4','ThinkTech IoT Hackathon organized by Landmark Technologies and Moveo Albania.This is the fourth Edition.', null, null);INSERT INTO public.events(id, name, description, started_at, finished_at)VALUES (5, 'Thinktech IoT Hackathon #5','ThinkTech IoT Hackathon organized by Landmark Technologies and Moveo Albania.This is the fifth Edition.', CURRENT_TIMESTAMP, null);
  • To include the file declare it on the application-test.yml :
spring.datasource.data: classpath:mock-data.sql

You can find the full configuration file on GitHub.

  • After that, we can write our test case with predefined values

For the first case we will retrieve an Event Dto, update its values and then make an update request. After that we are going to compare the values of the EventDto that the server returns.

  • So lets create an event with the values specified on mock-data.sql and check that the server retrieves them correctly.
Instant timestamp = Instant.now();EventDto expectedBeforeUpdate = new EventDto(3L, "Thinktech IoT Hackathon #3","ThinkTech IoT Hackathon organized by Landmark Technologies and Moveo Albania.This is the third Edition.",null, null);MvcResult resultBeforeUpdate = mockMvc.perform(get("/api/v1/events/3").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andReturn();EventDto eventBeforeUpdate = objectMapper.readValue(resultBeforeUpdate.getResponse().getContentAsByteArray(),EventDto.class);assertNotNull(eventBeforeUpdate);assertEquals(expectedBeforeUpdate, eventBeforeUpdate);
  • Then lets create the object with the updated values, make the update request and then compare the results with the object we expect:
EventDto event = new EventDto(3L, "Updated", "Updated", timestamp, timestamp);MvcResult result = mockMvc.perform(put("/api/v1/events/3").content(objectMapper.writeValueAsBytes(event)).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andReturn();EventDto eventAfterUpdate = objectMapper.readValue(result.getResponse().getContentAsByteArray(),EventDto.class);assertNotNull(eventAfterUpdate);assertEquals(event, eventAfterUpdate);

The whole test would look like this:

To run integration tests using maven you can run the following command:

mvn failsafe:integration-test

OR, you can run them using your IDE, right-click on the class > Run as> Junit Test (Eclipse)

Integration Test results

You can find the full source on GitHub. If you have any questions please comment on this post.

--

--

Olsi Seferi

Love to create things. Love to break things.. just to see how they work.