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.
- Context prepration
Prepare the data and other dependencies in a way to replicate a specific use case. - Execution
Executing the code to be tested within the above prepared context. - 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:
- validate: Validates the object by checking the fields for null or empty and throwing exceptions when it’s not valid.
- activate: Updates the status to ACTIVE
- 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.
- We start by creating the Customer object using Fixture
- Modifies the firstname field to
null
. - 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. - 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. Assertions
has 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.
- We are saving a Customer that is created using the Fixture
- Fetching from the repository using the first name and last name.
- Validate that the entry if fetched ( not null )
- 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.
3 Responses
[…] Part 2: Concepts, Domain test cases & Repository test cases ( Unit tests ) […]
[…] Enterprise Spring boot testing: Part 2 ( General concepts, Domain testing, Unit test cases, Reposito… […]
[…] Enterprise Spring boot testing: Part 2 ( General concepts, Domain testing, Unit test cases, Reposito… […]