Creating In-Memory Versions of Spring Data JPA Repositories for Testing

This post shows how to easily create in-memory versions of your repositories. These in-memory repositories can then be injected into, for example, a service that you want to test.

I am not a big fan of mocking frameworks. The reason is that I believe that it is far too easy to oversimplify the interactions between objects when defining the return values of the mock objects. This means that you miss bugs that depend on complex interactions occurring only for some combinations of input.

Instead of mocking the dependencies of an object, I prefer to use real code as far as possible, using fake implementations of objects that are inconvenient or too slow to use during a quick test cycle. One example of this is using in-memory repositories instead of repositories that connect to a real database. Note that this is not the same as using a normal repository with an in-memory database—the in-memory database takes much longer to start than the in-memory repository.

Spring Data JPA is great for easily creating repository implementations without having to write any boilerplate code. Your normal repository interfaces, which often live in the domain layer, define the methods needed by the business logic. You then define another interface, in the infrastructure layer, that extends the repository interface and org.springframework.data.jpa.repository.JpaRepository. The actual implementation of the interface is created dynamically by Spring, using a combination of naming conventions and annotations in the JPA interface.

Getting Started

A sample project, rld-repositories-sample, accompanying this post can be found on GitHub.

Use the following commands to download and build the sample project:

$ mkdir reallifedeveloper
$ cd reallifedeveloper
$ git clone https://github.com/reallifedeveloper/rld-repositories-sample.git
$ cd rld-repositories-sample
$ mvn -DcheckAll clean install # Should end with BUILD SUCCESS

The command-line option -DcheckAll activates Maven profiles for running code quality checks using Checkstyle, FindBugs and JaCoCo.

If you want to look at the source code for rld-build-tools, that is also available on GitHub.

Example Code

Assume that we are working with entities for departments and employees:

Department.java

package com.reallifedeveloper.sample.domain;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name = "department")
public class Department {

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

    @Column(name = "name", unique = true, nullable = false)
    private String name;

    @OneToMany(mappedBy = "department")
    private Set<Employee> employees = new HashSet<>();

    public Department(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // Required by JPA.
    Department() {
    }

    public Long id() {
        return id;
    }

    public String name() {
        return name;
    }

    public Set<Employee> employees() {
        return employees;
    }

    public void addEmployee(Employee employee) {
        employees.add(employee);
    }

}

Employee.java

package com.reallifedeveloper.sample.domain;

import java.math.BigDecimal;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "employee")
public class Employee {

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

    @Column(name = "first_name", nullable = false)
    private String firstName;

    @Column(name = "last_name", nullable = false)
    private String lastName;

    @Column(name = "salary", nullable = false)
    private BigDecimal salary;

    @ManyToOne
    @JoinColumn(name = "department_id", nullable = false)
    private Department department;

    public Employee(Long id, String firstName, String lastName, BigDecimal salary, Department department) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.salary = salary;
        this.department = department;
    }

    // Required by JPA.
    Employee() {
    }

    public Long id() {
        return id;
    }

    public String firstName() {
        return firstName;
    }

    public String lastName() {
        return lastName;
    }

    public BigDecimal salary() {
        return salary;
    }

    public Department department() {
        return department;
    }

    @Override
    public String toString() {
        return "Employee{id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", salary=" + salary
                + ", department=" + department.name() + "}";
    }

}

We create repository interfaces for working with the entities:

DepartmentRepository.java

package com.reallifedeveloper.sample.domain;

import java.util.List;

public interface DepartmentRepository {

    Department findByName(String name);

    List<Department> findAll();

    <T extends Department> T save(T department);

}

EmployeeRepository.java

package com.reallifedeveloper.sample.domain;

import java.math.BigDecimal;
import java.util.List;

public interface EmployeeRepository {

    Employee findById(Long id);

    List<Employee> findByLastName(String lastName);

    List<Employee> findEmployeesWithSalaryAtLeast(BigDecimal salary);

    <T extends Employee> T save(T Employee);

}

The repository interfaces above define only the operations that are required by the business logic. For example, if you don’t need to delete employees in the system you are building, don’t add a delete method. This is the reason that the interfaces do not extend JpaRepository directly—that would mean that all methods from that interface would be available to code using our repositories.

Instead of having our repositories extend JpaRepository directly, we create subinterfaces that extend our repository interfaces as well as JpaRepository and contain all annotations specific to Spring Data Jpa.

JpaDepartmentRepository.java

package com.reallifedeveloper.sample.infrastructure.persistence;

import org.springframework.data.jpa.repository.JpaRepository;

import com.reallifedeveloper.sample.domain.Department;
import com.reallifedeveloper.sample.domain.DepartmentRepository;

public interface JpaDepartmentRepository extends DepartmentRepository, JpaRepository<Department, Long> {

}

JpaEmployeeRepository.java

package com.reallifedeveloper.sample.infrastructure.persistence;

import java.math.BigDecimal;
import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.reallifedeveloper.sample.domain.Employee;
import com.reallifedeveloper.sample.domain.EmployeeRepository;

public interface JpaEmployeeRepository extends EmployeeRepository, JpaRepository<Employee, Long> {

    @Override
    @Query("select emp from Employee emp where emp.salary >= :salary")
    List<Employee> findEmployeesWithSalaryAtLeast(@Param("salary") BigDecimal salary);

}

With proper configuration, Spring will automatically create classes that implement your repository interfaces to connect to a database. But what if you want to create in-memory implementations of the interfaces? That process is simplified by the base class InMemoryJpaRepository from rld-build-tools that implements the JpaRepository interface and also provides helper methods for finding entities based on a field’s value.

InMemoryDepartmentRepository.java

package com.reallifedeveloper.sample.test;

import com.reallifedeveloper.sample.domain.Department;
import com.reallifedeveloper.sample.infrastructure.persistence.JpaDepartmentRepository;
import com.reallifedeveloper.tools.test.database.inmemory.InMemoryJpaRepository;
import com.reallifedeveloper.tools.test.database.inmemory.LongPrimaryKeyGenerator;

public class InMemoryDepartmentRepository extends InMemoryJpaRepository<Department, Long>
        implements JpaDepartmentRepository {

    public InMemoryDepartmentRepository() {
        super(new LongPrimaryKeyGenerator());
    }

    @Override
    public Department findByName(String name) {
        return findByUniqueField("name", name);
    }

}

InMemoryEmployeeRepository.java

package com.reallifedeveloper.sample.test;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import com.reallifedeveloper.sample.domain.Employee;
import com.reallifedeveloper.sample.infrastructure.persistence.JpaEmployeeRepository;
import com.reallifedeveloper.tools.test.database.inmemory.InMemoryJpaRepository;
import com.reallifedeveloper.tools.test.database.inmemory.LongPrimaryKeyGenerator;

public class InMemoryEmployeeRepository extends InMemoryJpaRepository<Employee, Long>
        implements JpaEmployeeRepository {

    public InMemoryEmployeeRepository() {
        super(new LongPrimaryKeyGenerator());
    }

    @Override
    public Employee findById(Long id) {
        return findByUniqueField("id", id);
    }

    @Override
    public List<Employee> findByLastName(String lastName) {
        return findByField("lastName", lastName);
    }

    @Override
    public List<Employee> findEmployeesWithSalaryAtLeast(BigDecimal salary) {
        List<Employee> employeesWithSalaryAtLeast = new ArrayList<>();
        for (Employee employee : findAll()) {
            if (employee.salary().compareTo(salary) >= 0) {
                employeesWithSalaryAtLeast.add(employee);
            }
        }
        return employeesWithSalaryAtLeast;
    }

}

Notes

  • Methods from JpaRepository, such as findAll and save, are implemented by InMemoryJpaRepository, so you don’t have to implement them.
  • Methods that find entities based on the value of a single field, such as findById and findByLastName, are easily implemented using findByField or findByUniqueField. Use findByField if several entities can have the same value for the field in question, and findByUniqueField if there can be only one entity with a given value for the field.
  • Methods that do more complicated things, such as findEmployeesWithSalaryAtLeast, are implemented using custom code.
  • If you want to emulate the @GeneratedValue annotation for the id field of an entity, you need to provide a PrimaryKeyGenerator in the constructor of InMemoryJpaRepository. There are implementations of PrimaryKeyGenerator for working with integers and long integers, and it is easy to create other implementations.
  • Conclusion

    The base class InMemoryJpaRepository makes it easy to create in-memory versions of Spring Data JPA repositories. The reason to use such repositories when testing is that they can be created and destroyed very quickly.

    In a follow-up post, we will look at using DbUnit to test both normal repositories and in-memory repositories.

    RealLifeDeveloper

Published by

RealLifeDeveloper

I'm a software developer with 20+ years of experience who likes to work in agile teams using Specification by Example, Domain-Driven Design, Continuous Delivery and lots of automation.

Leave a Reply

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