Enterprise Java

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'
}
You can read more about Arquillian Cube at http://arquillian.org/arquillian-cube/
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button