Testing Spring Data + Spring Boot applications with Arquillian (Part 1)
Spring Data’s mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store. It provides integration with several backend technologies such as JPA, Rest, MongoDB, Neo4J or Redis to cite a few.
So if you are using Spring (Boot) then Spring Data is the right choice to deal with persistence layer.
In next example you can see how simple is to use Spring Boot and Spring Data Redis.
@Controller @EnableAutoConfiguration public class PingPongController { @Autowired StringRedisTemplate redisTemplate; @RequestMapping("/{ping}") @ResponseBody List<String> getPong(@PathVariable("ping") String ping) { final ListOperations<String, String> stringStringListOperations = redisTemplate.opsForList(); final Long size = stringStringListOperations.size(ping); return stringStringListOperations.range(ping, 0, size); } @RequestMapping(value="/{ping}", method = RequestMethod.POST) ResponseEntity<?> addPong(@PathVariable("ping") String ping, @RequestBody String pong) { final ListOperations<String, String> stringStringListOperations = redisTemplate.opsForList(); stringStringListOperations.leftPushAll(ping, pong); URI location = ServletUriComponentsBuilder .fromCurrentRequest() .buildAndExpand(ping).toUri(); return ResponseEntity.created(location).build(); } public static void main(String[] args) { SpringApplication.run(PingPongController.class, args); } }
@Configuration public class RedisConfiguration { @Bean StringRedisTemplate template(final RedisConnectionFactory connectionFactory) { return new StringRedisTemplate(connectionFactory); } }
It is important to notice that by default Spring Data Redis is configured to connect to localhost and port 6379, but you can override those values by setting system properties (spring.redis.host and spring.redis.port) or environment variables (SPRING_REDIS_HOST and SPRING_REDIS_PORT).
But now it is time to write a test for this piece of code. The main problem you might get is that you need a Redis server installed in all machines that need to execute these tests such as developers machine or Jenkins slaves.
This is not a problem per se but when you start working on more and more projects you’ll need more and more databases installed on the system, and what even can be worst not exactly the same version as required on production.
o avoid this problem, one possible solution is using Docker and containers. So instead of relaying on having each database installed on the system, you only depends on Docker. Then the test just starts the repository container, in our case Redis, executes the test(s) and finally stops the container.
And this is where Arquillian (and Arquillian Cube) helps you on automating everything.
Arquillian Cube is an Arquillian extension that can be used to manager Docker containers from Arquillian.
To use Arquillian Cube you need a Docker daemon running on a computer (it can be local or not), but probably it will be at local.
By default the Docker server uses UNIX sockets for communicating with the Docker client. Arquillian Cube will attempt to detect the operating system it is running on and either set docker-java to use UNIX socket on Linux or to Boot2Docker/Docker-Machine on Windows/Mac as the default URI, so your test is portable across several Docker installations and you don’t need to worry about configuring it, Arquillian Cube adapts to what you have installed.
Arquillian Cube offers three different ways to define container(s).
- Defining a docker-compose file.
- Defining a Container Object.
- Using Container Object DSL.
For this post, Container Object DSL approach is the one used. To define a container to be started before executing tests and stopped after you only need to write next piece of code.
@ClassRule public static ContainerDslRule redis = new ContainerDslRule("redis:3.2.6") .withPortBinding(6379);
In this case a JUnit Rule is used to define which image should be used in the test (redis:3.2.6) and add as binding port the Redis port (6379).
The full test looks like:
@RunWith(SpringRunner.class) @SpringBootTest(classes = PingPongController.class, webEnvironment = RANDOM_PORT) @ContextConfiguration(initializers = PingPongSpringBootTest.Initializer.class) public class PingPongSpringBootTest { @ClassRule public static ContainerDslRule redis = new ContainerDslRule("redis:3.2.6") .withPortBinding(6379); @Autowired TestRestTemplate restTemplate; @Test public void should_get_pongs() { // given restTemplate.postForObject("/ping", "pong", String.class); restTemplate.postForObject("/ping", "pung", String.class); // when final List<String> pings = restTemplate.getForObject("/ping", List.class); // then assertThat(pings) .hasSize(2) .containsExactlyInAnyOrder("pong", "pung"); } public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { EnvironmentTestUtils.addEnvironment("testcontainers", configurableApplicationContext.getEnvironment(), "spring.redis.host=" + redis.getIpAddress(), "spring.redis.port=" + redis.getBindPort(6379) ); } } }
Notice that it is a simple Spring Boot test using their bits and bobs, but Arquillian Cube JUnit Rule is used in the test to start and stop the Redis image.
Last important thing to notice is that test contains an implementation of ApplicationContextInitializer so we can configure environment with Docker data (host and binding port of Redis container) so Spring Data Redis can connect to correct instance.
Last but not least build.gradle file defines required dependencies, which looks like:
buildscript { repositories { jcenter() mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE") } } plugins { id "io.spring.dependency-management" version "1.0.2.RELEASE" } apply plugin: 'java' apply plugin: 'org.springframework.boot' repositories { jcenter() } project.version = '1.0.0' dependencyManagement { imports { mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.13.Final' } } dependencies { compile "org.springframework.boot:spring-boot-starter-web:1.5.2.RELEASE" compile 'org.springframework.boot:spring-boot-starter-data-redis:1.5.2.RELEASE' testCompile 'org.springframework.boot:spring-boot-starter-test:1.5.2.RELEASE' testCompile 'junit:junit:4.12' testCompile 'org.arquillian.cube:arquillian-cube-docker-junit-rule:1.2.0' testCompile 'org.assertj:assertj-core:3.6.2' }
Reference: | Testing Spring Data + Spring Boot applications with Arquillian (Part 1) from our JCG partner Alex Soto at the One Jar To Rule Them All blog. |