Overview:
This article looks at how mix JPA with Spring Boot and write integration tests that only loads the persistence layer and exercises the repository implementation.
Also, I have tried to demonstrate the usage of JPA Specifications without having to convolute the repository API with JPA query method definitions to perform numerous search operations.
You can find the sample code for this application
here.
Prerequisite:
To run the application, you need the following installed.
Java 7+
Maven 3
The pom.xml
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.4.RELEASE</version>
</parent>
<groupId>org.fazlan.blogger</groupId>
<artifactId>jpa-sample</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<skip.utests>false</skip.utests>
<skip.itests>true</skip.itests>
<testng.version>6.9.4</testng.version>
<assertj.version>2.1.0</assertj.version>
<hibernate-jpamodelgen.version>4.3.10.Final</hibernate-jpamodelgen.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--test related dependencies-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<!--sample specification metadata generator-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate-jpamodelgen.version}</version>
</dependency>
<!-- database for integration tests-->
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
<compilerArguments>
<processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/annotations/</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${skip.utests}</skipTests>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>integration-test</goal>
</goals>
<configuration>
<skipTests>${skip.itests}</skipTests>
<includes>
<include>**/*ITSpec.class</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>integration</id>
<properties>
<skip.utests>true</skip.utests>
<skip.itests>false</skip.itests>
</properties>
</profile>
</profiles>
</project>
Defining the Entity classes
Following is a partial view of the City entity annotated.
package org.fazlan.blogger.sample.domain.entity;
import javax.persistence.*;
import java.io.Serializable;
@Entity
@Table(name = "CITIES")
public class City implements Serializable {
@Id
@GeneratedValue
@Column(name = "CITY_ID", nullable = false, unique = true)
private Long id;
@Column(name = "NAME", nullable = false)
private final String name;
@Column(name = "STATE", nullable = false)
private final String state;
@Column(name = "COUNTRY", nullable = false)
private final String country;
// rest of the code is omitted for clarity
}
Then we construct a City instance using Builder patter as follows,
package org.fazlan.blogger.sample.domain.entity;
import javax.persistence.*;
import java.io.Serializable;
@Entity
@Table(name = "CITIES")
public class City implements Serializable {
// rest of the code is omitted for clarity
@SuppressWarnings("unused")
private City() {
this(builder(null, null));
}
private City(Builder builder) {
this.name = builder.name;
this.state = builder.state;
this.country = builder.country;
}
// getter/setter code is omitted for clarity
public static Builder builder(String name, String country) {
return new Builder(name, country);
}
public static class Builder {
private String name;
private String state;
private String country;
public Builder(String name, String country) {
this.name = name;
this.country = country;
}
public Builder state(String state) {
this.state = state;
return this;
}
public City build() {
return new City(this);
}
}
}
Using Builder pattern it is easy to construct immutable objects without having lengthy constructor arguments.
Defining the Repository classes
Our Repository extends
org.springframework.data.jpa.repository.JpaRepository and
org.springframework.data.jpa.repository.JpaSpecificationExecutor;
package org.fazlan.blogger.sample.domain.repo;
import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
@Repository
public interface CityRepository
extends JpaRepository<City, Long>, JpaSpecificationExecutor<City> {
}
Writing an Integration Test Specification
Defining our integration properties
application-integration.properties - where "integration" is the profile under test ( application-{profile}.properties )
spring.datasource.url: jdbc:hsqldb:mem:integration
Since we're only interested to test the repositories and the associated entities, let's load only the context related to persistence, Doing so, we are not loading the entire application context, which can help to speed up our tests at least my one nanosecond.
package org.fazlan.blogger.sample.domain;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@EnableJpaRepositories(basePackages = "org.fazlan.blogger.sample.domain.repo")
@EntityScan(basePackages = "org.fazlan.blogger.sample.domain.entity")
@Import({ DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class })
public class DomainTestConfig {
}
Above, ensures only the repositories defined in
"org.fazlan.blogger.sample.domain.repo" are loaded, and entities defined in are
"org.fazlan.blogger.sample.domain.entity" loaded.
To setup the data required for out test, lets create a Test Fixture (much like an Object Mother). This will be used by our test.
package org.fazlan.blogger.sample.domain.repo;
import org.fazlan.blogger.sample.domain.entity.City;
class CityRepositoryFixture {
static final String CITY_MELBOURNE = "Melbourne";
static final String CITY_SYDNEY = "Sydney";
static final String STATE_NSW = "NSW";
static final String STATE_VICTORIA = "Victoria";
static final String COUNTRY_AUSTRALIA = "Australia";
static City getMelbourne() {
return City.builder(CITY_MELBOURNE, COUNTRY_AUSTRALIA).state(STATE_VICTORIA).build();
}
static City getSydney() {
return City.builder(CITY_SYDNEY, COUNTRY_AUSTRALIA).state(STATE_NSW).build();
}
static City getAustralianCities() {
return City.builder(null, COUNTRY_AUSTRALIA).build();
}
}
Now that we have the text fixture, lets write a test to exercise our wiring of entities with repositories.
package org.fazlan.blogger.sample.domain.repo;
import org.fazlan.blogger.sample.domain.DomainTestConfig;
import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringApplicationConfiguration(classes = DomainTestConfig.class)
public class CityRepositoryITSpec
extends AbstractTransactionalTestNGSpringContextTests {
@Autowired
private CityRepository repository;
private City melbourne;
@BeforeMethod
public void beforeEachTest() {
melbourne = CityRepositoryFixture.getMelbourne();
}
@Test
public void creates_city_given_name_and_state_country() {
Long cityId = repository.save(melbourne).getId();
assertThat(repository.getOne(cityId)).isEqualTo(melbourne);
}
}
To run the tests, simply run
mvn clean integration-test -P integration
Above tests if a city gets created as expected.
Now lets try to test retrieval of cities. Lets assume we need to retrieve a city given its name and country. Also, all cities in a given country.
This could have achieved via JPA Query methods such as follows,
package org.fazlan.blogger.sample.domain.repo;
import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CityRepository extends JpaRepository<City, Long>, JpaSpecificationExecutor<City> {
City findByNameAndCountry(String name, String country);
List<City> findByCountry(String country);
}
Doing so, works fine. However, it convolutes the Repository interface when we need to add more query methods to retrieve cities based on different criteria. Better alternative to this would be to use method that accepts a common interface and later can be passed with different implementations to do the same ( Strategy Pattern ).
org.springframework.data.jpa.repository.JpaSpecificationExecutor interface allows exactly that as follows,
package org.springframework.data.jpa.repository;
. . .
import org.springframework.data.jpa.domain.Specification;
public interface JpaSpecificationExecutor<T> {
T findOne(Specification<T> spec);
List<T> findAll(Specification<T> spec);
// rest omitted for clarity
}
We only have to extend
org.springframework.data.jpa.domain.Specification in our repository to access these methods. Once extends, we can write our custom search specifications without changing the Repository interface.
To generate metadata model required by the Specification to ensure type safety of fields, we use the following maven dependency and plugin in our pom.xml
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate-jpamodelgen.version}</version>
</dependency>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
<compilerArguments>
<processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
</compilerArguments>
</configuration>
</plugin>
This generates the metadata model denoted by <Entity>_.java (e.i: City_.java , which is later used in the Specifications as follows)
Defining custom search specifications.
Our search specifications
package org.fazlan.blogger.sample.domain.spec;
import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.data.jpa.domain.Specification;
abstract class CitySpec implements Specification<City> {
protected final City criteria;
protected CitySpec(City city) {
this.criteria = city;
}
}
class CityByCountry extends CitySpec {
public CityByCountry(City city) {
super(city);
}
@Override
public Predicate
toPredicate(Root<City> city, CriteriaQuery<?> cq, CriteriaBuilder cb) {
return cb.equal(city.get(City_.country), criteria.getCountry());
}
}
class CityByNameCountry extends CitySpec {
CityByNameCountry(City city) {
super(city);
}
@Override
public Predicate
toPredicate(Root<City> city, CriteriaQuery<?> cq, CriteriaBuilder cb) {
return cb.and(
cb.equal(city.get(City_.name), criteria.getName()),
cb.equal(city.get(City_.country), criteria.getCountry())
);
}
}
NOTE: We have made the implementations to package access level. So, we have to create a factory to return different creteria via a public interface.
package org.fazlan.blogger.sample.domain.spec;
import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.data.jpa.domain.Specification;
public class CitySpecFactory {
private CitySpecFactory() {}
public static Specification<City> byNameAndCountry(City city) {
return new CityByNameCountry(city);
}
public static Specification<City> byCountry(City city) {
return new CityByCountry(city);
}
}
Now we can write test to exercise the retrieval of cities
@SpringApplicationConfiguration(classes = DomainTestConfig.class)
public class CityRepositoryITSpec
extends AbstractTransactionalTestNGSpringContextTests {
//rest are omitted for clarity
@Test
public void finds_city_given_name_and_country() {
repository.save(melbourne);
City result = repository.
findOne(CitySpecFactory.
byNameAndCountry(melbourne));
assertThat(result).isEqualTo(melbourne);
}
@Test
public void finds_all_cities_given_country() {
City sydney = CityRepositoryFixture.getSydney();
repository.save(Arrays.asList(melbourne, sydney));
List<City> results = repository.
findAll(CitySpecFactory.
byCountry(CityRepositoryFixture.
getAustralianCities()));
assertThat(results).containsAll(Arrays.asList(melbourne, sydney));
}
}
Summary
This article looks at how mix JPA with Spring Boot and write integration tests that only loads the persistence layer and exercises the repository implementation.
Also, I have tried to demonstrate the usage of JPA Specifications without having to convolute the repository API with JPA query method definitions to perform numerous search operations.
You can find the sample code for this application
here.