Enterprise Spring boot testing: Part 2 ( General concepts, Domain testing, Unit test cases, Repository testing )

In this part of the Spring boot testing series, we are going to create the Domain classes, its unit tests, and the repository tests for the Customer entity. Please check the first part <link> to get the idea of the test cases, libraries, and the project structure that we are going to work on.

You can find the full project in the GitHub repository. Also, if you have not read part 1, I would recommend reading it first to get a general idea of what we are doing.

General concepts

Before we start writing test cases, it is useful to understand some of the concepts and the general structure of a test case that is being used in the below samples and the tutorial throughout.

Fixture

We need to have a standard object for the class or the classes that we are testing. These are called Fixtures and provide consistency in object creation and also reduce the effort of creating it everywhere.

Create a class CustomerFixture.java with a static method that returns the standard Customer object with the fields. You could create any number of static methods based on the nature of the data you need.

package com.microideation.tutorial.springtest.customers;

import com.microideation.tutorial.springtest.dictionary.CustomerStatus;
import com.microideation.tutorial.springtest.domain.Customer;

public class CustomerFixture {
    public static Customer standardCustomer() {
        return Customer.builder()
                .firstname("John")
                .lastname("Doe")
                .id(1l)
                .status(CustomerStatus.DISABLED)
                .build();
    }
}

You will see this class being used across almost all the test cases that we build across the layers.

Naming conventions

Test cases are Java methods annotated with @Test annotation. There are no hard and fast rules on how you name them. But, it is recommended that we follow a format that explains what is being tested. The format that I prefer is:

@Test
public void given<FunctionName>_when<Condition>_then<ExpectedResult> {
 // ...
}

Structure

Each test case could be generally segregated into 3 parts.

  1. Context prepration
    Prepare the data and other dependencies in a way to replicate a specific use case.
  2. Execution
    Executing the code to be tested within the above prepared context.
  3. Assertions and verifications
    Assert that the result is as expected and also verify that certain calls and operations were performed during the execution ( like interaction with dependencies etc ).

Domain class and the test cases

A domain in an enterprise architecture refers to the object that represents a business entity with a tight context along with its associated methods.

In our case, the domain is Customer and we are going to create the customer entity and associated domain methods that add behavior to this domain.

Create the Customer.java with the below code and put it under the domain package.

package com.microideation.tutorial.springtest.domain;

import com.microideation.tutorial.springtest.dictionary.CustomerStatus;
import lombok.*;
import org.springframework.util.ObjectUtils;

import javax.persistence.*;
import java.io.Serializable;

@Data
@ToString
@Entity
@Builder
@Table(name="CUSTOMERS")
@NoArgsConstructor
@AllArgsConstructor
public class Customer implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String firstname;

    private String lastname;

    @Enumerated(EnumType.STRING)
    private CustomerStatus status;


    public void validate() {

        if (ObjectUtils.isEmpty(firstname))
            throw new RuntimeException("Firstname is a required field");

        if (ObjectUtils.isEmpty(lastname))
            throw new RuntimeException("Lastname is a required field");

        if (ObjectUtils.isEmpty(status))
            throw new RuntimeException("Status is a required field");
    }

    public void activate() {
        setStatus(CustomerStatus.ACTIVE);
    }

    public void deactivate() {
        setStatus(CustomerStatus.DISABLED);
    }
}

You can see that it’s a pretty simple class with JPA annotations, 4 fields and only 3 domain methods:

  1. validate: Validates the object by checking the fields for null or empty and throwing exceptions when it’s not valid.
  2. activate: Updates the status to ACTIVE
  3. deactivate: Updates the status to DISABLED.

Note that the CustomerStatus is created as an ENUM and put under the dictionary package. I have also used the @Builder,@Data annotations of Lombok plugin for reducing some boilerplate code.

Domain Test cases ( Unit tests )

Now, let’s create the test cases of the domain methods in the Customer.java class. The objective is to ensure that it handles the specific cases and rules that we have added to the methods.

Test cases

Wherever applicable, we should try to test only for one rule validation in a single test case. Let’s create Domain tests under the customers folder of the test package. Let’s create the following test case file for the CustomerDomainTest.java. Note that we are not using any annotation at the class level as these are only unit tests without any other dependencies.

package com.microideation.tutorial.springtest.customers;

import com.microideation.tutorial.springtest.dictionary.CustomerStatus;
import com.microideation.tutorial.springtest.domain.Customer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import static com.microideation.tutorial.springtest.customers.CustomerFixture.standardCustomer;

public class CustomerDomainTest {
    // Test1
    @Test
    public void givenValidate_whenFirstNameEmpty_thenThrowsException() {
        Customer customer=standardCustomer();
        customer.setFirstname(null);
        Assertions.assertThrows(RuntimeException.class,() -> {
            customer.validate();
        });
    }
    // Test2
    @Test
    public void givenValidate_whenLastNameEmpty_thenThrowsException() {
        Customer customer=standardCustomer();
        customer.setLastname(null);
        Assertions.assertThrows(RuntimeException.class,() -> {
            customer.validate();
        });
    }
    // Test3
    @Test
    public void givenValidate_whenStatusEmpty_thenThrowsException() {
        Customer customer=standardCustomer();
        customer.setStatus(null);
        Assertions.assertThrows(RuntimeException.class,() -> {
            customer.validate();
        });
    }

    // Test4
    @Test
    public void givenActivate_whenTriggred_thenSetStatusToActive() {
        Customer customer=standardCustomer();
        customer.activate();

        // Validate that the status is active
        Assertions.assertEquals(CustomerStatus.ACTIVE,customer.getStatus());
    }

    // Test5
    @Test
    public void givenDeactivate_whenTriggred_thenSetStatusToDisabled() {
        Customer customer=standardCustomer();
        customer.deactivate();

        // Validate that the status is active
        Assertions.assertEquals(CustomerStatus.DISABLED,customer.getStatus());
    }


}

validate() test cases

Test 1, Test 2, and Test 3 cover the cases of the validate method.

Note the name used : givenValidate_whenFirstNameEmpty_thenThrowsException. This follows the convention we discussed above and gives a clear idea of what is to be expected.

  1. We start by creating the Customer object using Fixture
  2. Modifies the firstname field to null.
  3. Now to ensure that this throws an exception, we will be using the JUnit Jupiter’s Assertions.assertThrows() utility method. We wrap around the code to be executed ( in our case, customer.validate()) and the exception expected ( RuntimeException.class). This assertion will be valid when wrapped code throws the specified exception there by passing the test.
  4. The test fails if the exception is not thrown.

We can run the test by right-clicking on the test body and selecting the Run option. If you are not able to see the run option, make sure that the @Test annotation is used on the method.

Similarly, we do the test cases for the last name and the status fields. The logic and the checking are the same and the only difference is the field that we set as null.

Activate and Deactivate methods

Test 4 and Test 5 checks the activate()and deactivate() methods respectively. In these methods, we are using the Assertions.assertEqual()method to verify that we are having the proper status after the method is called. Assertionshas got a variety of methods for validations, equality checking, null check, etc. You can use the appropriate method for your case.

For running all the tests in a class, you could right-click on the class name CustomerDomainTest and select Run.

Repository class & Test using @DataJpaTest

Now that we have created the domain and the domain test cases, let’s proceed to the next layer that is the Data layer. We will be creating a simple repository class and the associated test class for it.

Repository class

Create the repository interface CustomerRepository under repository folder. This will be extending the JpaRepository interface that will provide all the basic functionality for finding, saving, etc. We only have one inferred method findByFirstnameAndLastname that finds the customer matching the provided firstname and lastname.

package com.microideation.tutorial.springtest.repository;

import com.microideation.tutorial.springtest.domain.Customer;
import org.springframework.data.jpa.repository.JpaRepository;


public interface CustomerRepository extends JpaRepository<Customer,Long> {
    Customer findByFirstnameAndLastname(String firstname,String lastName);
}

Repository Test cases

For the repository classes, we need to test using the Jpa layer. That is, create and run test cases only using the JPA layer and not the other layers ( service, Web, etc ). Spring boot provides the @DataJpaTest annotation that will wire up the JPA layer for the test cases and use TestEntityManager for interacting with the persistence layer.

@DataJpaTest

@DataJpaTest is the annotation that Spring supports for a JPA test that focuses only on JPA components. It will disable full auto-configuration and then, apply only enable configuration relevant to JPA tests. The list of the auto-configuration settings that are enabled can be found here.

By default, tests annotated  @DataJpaTest are transactional and roll back at the end of each test. If you don’t want it, you can disable transaction management for a test or for the whole class using @Transactional annotation:

TestEntityManager

The purpose of the EntityManager is to interact with the persistence context. Spring Data JPA abstracts you from the EntityManager through Repository interfaces. And TestEntityManager allows us to use EntityManager in tests.

With the above in mind, let’s create our CustomerRepositoryTest.java

package com.microideation.tutorial.springtest.customers;

import com.microideation.tutorial.springtest.domain.Customer;
import com.microideation.tutorial.springtest.repository.CustomerRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.util.Assert;

import static com.microideation.tutorial.springtest.customers.CustomerFixture.standardCustomer;

@DataJpaTest
public class CustomerRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private CustomerRepository customerRepository;

    @Test
    public void givenFindByFirstnameAndLastname_whenFirstnameAndLastnameMatch_thenFetchCustomer() {
        // Create a customer
        Customer customer = standardCustomer();
        customer.setId(null);
        customer = customerRepository.save(customer);

        // Find
        Customer searchResult = customerRepository.findByFirstnameAndLastname(customer.getFirstname(),customer.getLastname());

        // Assertions
        Assertions.assertNotNull(searchResult);
        Assertions.assertEquals(searchResult.getId(),customer.getId());

    }

}

We have annotated our class with @DataJpaTest and also autowired the TestEntityManager and the CustomerRespository

We only have one custom method in the Repository class for finding the Customer using the first name and the last name. The other methods are provided by built-in JpaRepository and you can skip testing those ( unless you have some custom logic while saving and want to get those included in the tests ).

The test case itself is simple.

  1. We are saving a Customer that is created using the Fixture
  2. Fetching from the repository using the first name and last name.
  3. Validate that the entry if fetched ( not null )
  4. Validate that the name matches the one we passed.

Again, here also we use the Assertions class utility methods for validating the results we want.

Similar to the other test cases, you could run the test by right-clicking on the test method or the class name. As a practice session, you could create test cases for save methods or other findBy methods provided by the JpaRepository and create the relevant test cases.

Conclusion

In the next part, we will see how to define and create the service layer test cases and use mocking of the dependencies.

You may also like...

3 Responses

  1. November 1, 2021

    […] Part 2: Concepts, Domain test cases & Repository test cases ( Unit tests ) […]

  2. November 5, 2021

    […] Enterprise Spring boot testing: Part 2 ( General concepts, Domain testing, Unit test cases, Reposito… […]

  3. November 11, 2021

    […] Enterprise Spring boot testing: Part 2 ( General concepts, Domain testing, Unit test cases, Reposito… […]

Leave a Reply

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