News

Writing integration tests with Spring Boot and Cassandra in 2 minutes

Recently, a client asked us to develop a mobile application served by a REST backend with a lot of data to be stored. To make a long story short, we ended up using Cassandra to answer those requirements. Following an Agile approach to development, we also used TDD, Test-Driven Development.

As we started development from scratch, it seemed natural to develop automated integration tests against the application database. Usually, this is easily done when using relational databases, e.g. testing against a in-memory database like H2, but we realized that such ability does not come out of the box with Spring Boot and Cassandra, at least in its community edition.

Olivier Antoine

…it seemed natural to develop automated integration tests against the application database. Usually, this is easily done when using relational databases, e.g. testing against a in-memory database like H2, but we realized that such ability does not come out of the box with Spring Boot and Cassandra…

Olivier Antoine, Senior Software Developer, Agile Practitioner

So we had to find a solution to start a Cassandra server inside just before the execution of our tests, and also to be able to inject some data for test purposes.

Introduction

The following document describes the solution we came up with, making use of the  cassandra-unit framework. We also describe how to configure your project accordingly, based on:

  • Spring Boot 1.3.5
  • Cassandra-unit 2.2.2.1
  • Cassandra driver 2.1.7.1

NB : For demonstration purpose, I wrote a basic log management application. It mainly focuses on providing a test and validating what is retrieved from the database. You can find the source code from our Github project available here https://github.com/Arexo/cassandra-unit-spring-demo

Goals of integration test

An integration test is the component software in which individual software modules are combined and tested as a group. It occurs after unit testing and before validation testing. In this example we will combine the REST server backend module and the database together.

During this how-to, you will go through the following steps :

  1. Configure your project to use SpringBoot and Cassandra-unit
  2. Writing a dataset to inject data into the cassandra database during the test
  3. Configureapplication.properties to use cassandra
  4. Writing the test itself
  5. Execute the test

A word on Cassandra-unit

Cassandra-unit, as indicated by its name, is a unit testing library which adds to your test the ability to start/stop a Cassandra database server and also inject a CQL dataset into it.

The project provides two modules:

  • cassandra-unit : the core, contains everything to start the server, injection of dataset, etc…
  • cassandra-unit-spring : A library which fills the gap between Spring-Boot dependency injection and the Cassandra related stuff. The second ones include the first.

More info on the project GitHub’s page

Let’s go!!!

Configure your project

For this example, I used Gradle as build management system. Add the following dependencies to your build.gradle file.

ext {
   springBootVersion = '1.3.5.RELEASE'
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
    compile("org.springframework.boot:spring-boot-starter-data-cassandra:${springBootVersion}")
    compile("com.datastax.cassandra:cassandra-driver-core:2.1.7.1")
    compile("com.datastax.cassandra:cassandra-driver-dse:2.1.7.1")
    testCompile("org.springframework.boot:spring-boot-starter-test:${springBootVersion}")
    testCompile('org.cassandraunit:cassandra-unit-spring:2.2.2.1')
}

Write a dataset

Create a dataset file, the CQL instructions present in that file will be played against the database which is loaded within your test.

The “dataset.cql” file:

CREATE KEYSPACE IF NOT EXISTS mykeyspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}  AND durable_writes = true;

DROP TABLE IF EXISTS mykeyspace.logs;

CREATE TABLE IF NOT EXISTS mykeyspace.logs (
    id text,
    query text,
    PRIMARY KEY (id)
);

INSERT into mykeyspace.logs(id, query) values ('1','cinema');

Add cassandra properties to application.properies

test.url=http://localhost

spring.data.cassandra.keyspace-name=mykeyspace
spring.data.cassandra.contact-points=localhost
spring.data.cassandra.port=9142

There is nothing magic here, just tell the Spring Boot Cassandra auto-configuration to connect on localhost and port 9142.
Warning! Cassandra-unit, by default,  starts Cassandra on port 9142 instead of 9042.

Write a test

package be.arexo.demos.cassandra.controller;

import be.arexo.demos.cassandra.DemoApplication;
import be.arexo.demos.cassandra.test.AbstractEmbeddedCassandraTest;
import org.cassandraunit.spring.CassandraDataSet;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;


@SpringApplicationConfiguration(classes = DemoApplication.class)
@CassandraDataSet(keyspace = "mykeyspace", value = {"dataset.cql"})
public class LogControllerTest extends AbstractEmbeddedCassandraTest {

    @Test
    public void testFindOne() throws Exception {

        ResponseEntity response = client.getForEntity("/logs/{id}", Log.class, 1);

        assertThat(response.getStatusCode()     , is(HttpStatus.OK));
        assertThat(response.getBody().getQuery(), is("cinema"));
    }
}

The annotation @CassandraDataSet is used to define the keyspace to use and also the cql requests to load into the database.

Go further and create an abstract test class

package be.arexo.demos.cassandra.test;

import org.cassandraunit.spring.CassandraUnitDependencyInjectionTestExecutionListener;
import org.cassandraunit.spring.EmbeddedCassandra;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriTemplateHandler;

import javax.annotation.PostConstruct;

@RunWith(SpringJUnit4ClassRunner.class)
@WebIntegrationTest(randomPort = true) // Pick a random port for Tomcat
@TestExecutionListeners(listeners = {
        CassandraUnitDependencyInjectionTestExecutionListener.class,
        DependencyInjectionTestExecutionListener.class}
)
@EmbeddedCassandra(timeout = 60000)
public class AbstractEmbeddedCassandraTest {

    @Value("${local.server.port}")
    protected int port;

    @Value("${test.url}")
    protected String url;

    protected RestTemplate client;

    @PostConstruct
    public void init() {
        DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler();
        handler.setBaseUrl(url + ":" + port);
        handler.setParsePath(true);

        client = new TestRestTemplate();
        client.setUriTemplateHandler(handler);
    }
}

Execute the test

Then execute it and you should see something like this as output:


016-10-19 21:21:28.415 INFO 40099 — [ main] b.a.d.c.controller.LogControllerTest : Started LogControllerTest in 3.461 seconds (JVM running for 14.008)
2016-10-19 21:21:28.627 INFO 40099 — [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet ‘dispatcherServlet’
2016-10-19 21:21:28.628 INFO 40099 — [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet ‘dispatcherServlet’: initialization started
2016-10-19 21:21:28.641 INFO 40099 — [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet ‘dispatcherServlet’: initialization completed in 13 ms
2016-10-19 21:21:28.854 INFO 40099 — [ main] c.d.d.c.p.DCAwareRoundRobinPolicy : Using data-center name ‘datacenter1′ for DCAwareRoundRobinPolicy (if this is incorrect, please provide the correct datacenter name with DCAwareRoundRobinPolicy constructor)
2016-10-19 21:21:28.854 INFO 40099 — [ main] com.datastax.driver.core.Cluster : New Cassandra host localhost/127.0.0.1:9142 added
2016-10-19 21:21:28.889 INFO 40099 — [edPool-Worker-2] o.a.cassandra.service.MigrationManager : Drop Keyspace ‘system_distributed’
2016-10-19 21:21:29.196 INFO 40099 — [edPool-Worker-3] o.a.cassandra.service.MigrationManager : Drop Keyspace ‘mykeyspace’
2016-10-19 21:21:29.489 INFO 40099 — [iceShutdownHook] o.apache.cassandra.thrift.ThriftServer : Stop listening to thrift clients
2016-10-19 21:21:29.489 INFO 40099 — [ Thread-3] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4372b9b6: startup date [Wed Oct 19 21:21:25 CEST 2016]; root of context hierarchy
2016-10-19 21:21:29.498 INFO 40099 — [iceShutdownHook] org.apache.cassandra.transport.Server : Stop listening for CQL clients
2016-10-19 21:21:29.498 INFO 40099 — [iceShutdownHook] org.apache.cassandra.gms.Gossiper : Announcing shutdown
2016-10-19 21:21:29.499 INFO 40099 — [iceShutdownHook] o.a.cassandra.service.StorageService : Node /127.0.0.1 state jump to normal
2016-10-19 21:21:29.506 ERROR 40099 — [-reconnection-0] c.d.driver.core.ControlConnection : [Control connection] Cannot connect to any host, scheduling retry in 1000 milliseconds
2016-10-19 21:21:30.509 ERROR 40099 — [-reconnection-0] c.d.driver.core.ControlConnection : [Control connection] Cannot connect to any host, scheduling retry in 2000 milliseconds
2016-10-19 21:21:31.502 INFO 40099 — [iceShutdownHook] o.apache.cassandra.net.MessagingService : Waiting for messaging service to quiesce
2016-10-19 21:21:31.503 INFO 40099 — [CEPT-/127.0.0.1] o.apache.cassandra.net.MessagingService : MessagingService has terminated the accept() thread

Conclusion

That ‘s all, as you can see, writing integration test with an embedded cassandra database is not so difficult. With this code, you have and example and you’re ready to go…

I hope you enjoyed this article. If you have any remarks, please feel free to contact me at olivier.antoine@arexo.be :-)

Troubleshooting

If you get this …


java.lang.NoSuchMethodError: com.codahale.metrics.Snapshot: method ()V not found
at com.codahale.metrics.UniformSnapshot.(UniformSnapshot.java:39) ~[metrics-core-3.1.0.jar:3.0.2]
at org.apache.cassandra.metrics.EstimatedHistogramReservoir$HistogramSnapshot.(EstimatedHistogramReservoir.java:77) ~[cassandra-all-2.2.2.jar:2.2.2]
at org.apache.cassandra.metrics.EstimatedHistogramReservoir.getSnapshot(EstimatedHistogramReservoir.java:62) ~[cassandra-all-2.2.2.jar:2.2.2]
at com.codahale.metrics.Histogram.getSnapshot(Histogram.java:54) ~[metrics-core-3.0.2.jar:3.0.2]
at com.codahale.metrics.Timer.getSnapshot(Timer.java:142) ~[metrics-core-3.0.2.jar:3.0.2]
at org.apache.cassandra.db.ColumnFamilyStore$3.run(ColumnFamilyStore.java:435) ~[cassandra-all-2.2.2.jar:2.2.2]

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_25]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_25]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_25]

Exclude the package com.codahale.metrics from the dependency configuration:

configurations {
    all*.exclude group: 'com.codahale.metrics'
}