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.
To be an Arexo employee is also to be an actor of the evolution of the company, its mindset and its values. As I often say, pleasure in the job gives perfection in the work!
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 pro_ject 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 :
- Configure your project to use SpringBoot and Cassandra-unit
- Writing a dataset to inject data into the cassandra database during the test
- Configure_application.properties_ to use cassandra
- Writing the test itself
- 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'
}