Cassandra is one of the emerging NoSQL technologies in the Big Data world. If you decided to use it in your project, you might have a good reason for it, and it is also important to ensure that it is well tested locally before proceeding further, in your deployment pipeline, into an integration environment.

Using Mockito to mock Cassandra is always a possibility though a bit tricky, and it does not provide the same level of confidence as testing against Cassandra itself.

Thankfully, cassandra-unit project was created and hosted in github (by @jsevellec). This utility is fully oriented to JUnit testing. But sometimes, either because you prefer TestNG, or by company policies, or just because everything is already in TestNG, you have to create your tests in TestNG.

This post provides a simple example on how to test your Cassandra application with TestNG using an embedded Cassandra cluster through cassandra-unit utility.

The Dependencies

Let’s say you are using Maven, you will have to include the following dependencies:

<dependency>
<groupId>com.datastax.cassandra</groupId>
<artifactId>cassandra-driver-core</artifactId>
<version>3.0.3</version>
</dependency>

<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit</artifactId>
<version>3.0.0.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>com.datastax.cassandra</groupId>
<artifactId>cassandra-driver-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.9.6</version>
<scope>test</scope>
</dependency>

<dependency>
<!-- Not strictly needed, but I like it -->
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>

Domain Description

For the sake of the example, let’s say you have a table to store persons information with the following information:

  • Identifier, an alphanumeric unique identifier of that person
  • Name, the name of the person

Domain Java Entity

Based on the domain description above, we can define our Java entity as below:

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;

public class Person {
private final String id;
private final String name;

public Person(String id, String name) {
this.id = id;
this.name = name;
}

public String getId() {
return id;
}

public String getName() {
return name;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return Objects.equal(id, person.id);
}

@Override
public int hashCode() {
return Objects.hashCode(id);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("name", name)
.toString();
}
}

Data Access Object

The DAO implementation will perform the bridge between your application’s domain and the database domain. Let’s consider the following simple DAO interface.

public interface PersonDao {
List<Person> findAll();
void insert(Person person);
}

And its implementation for accessing Cassandra:

import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import java.util.List;
import static com.google.common.collect.Lists.newArrayList;

public class PersonDaoImpl implements PersonDao {
private static final String TABLE_PERSON = "person";
private static final String FIELD_ID = "id";
private static final String FIELD_NAME = "name";
private final Session session;

public PersonDaoImpl(Session session) {
this.session = session;
}

@Override
public List<Person> findAll() {
Statement select = QueryBuilder
.select().all()
.from(TABLE_PERSON);
ResultSet resultSet = session.execute(select);
List<Person> personList = newArrayList();
for (Row row : resultSet) {
Person foundPerson = new Person(
row.getString(FIELD_ID),
row.getString(FIELD_NAME)
);
personList.add(foundPerson);
}
return personList;
}

@Override
public void insert(Person person) {
Statement insert = QueryBuilder
.insertInto(TABLE_PERSON)
.value(FIELD_ID, person.getId())
.value(FIELD_NAME, person.getName());
session.execute(insert);
}
}

Note that we are here using QueryBuilder, which provides a fluent API to create Cassandra queries.

Testing

Now we need to create a unit test for PersonDaoImpl class. We are testing the following:

Fetch all records from a pre-populated person table in the database and verify it has the expected values. Insert a person into the database and query the database to check if the records were added successfully. If you read through the cassandra-unit documentation, the way of declaring the Cassandra cluster for use in your test is something like:

@Rule
private CassandraCQLUnit cassandraCQLUnit =
new CassandraCQLUnit(new ClassPathCQLDataSet("db_setup.cql", "PersonKS"));

TestNG does not have equivalent to the JUnit rules, but it’s easy to emulate if we think on what this is used for. This basically creates the Cassandra embedded cluster while ensuring that the before and after methods are executed before and after the test scenario. So we can emulate it by using an abstract class and providing @BeforeClass and @AfterClass behaviour.

However, before and load of CassandraCQLUnit are protected, so let’s decorate that class a bit first:

import org.cassandraunit.CassandraCQLUnit;
import org.cassandraunit.dataset.CQLDataSet;

public class CassandraEmbedded extends CassandraCQLUnit {
public CassandraEmbedded(CQLDataSet dataSet) {
super(dataSet);
}

public CassandraEmbedded(CQLDataSet dataSet, int readTimeoutMillis) {
super(dataSet, readTimeoutMillis);
}

public CassandraEmbedded(CQLDataSet dataSet, String configurationFileName) {
super(dataSet, configurationFileName);
}

public CassandraEmbedded(CQLDataSet dataSet, String configurationFileName, int readTimeoutMillis) {
super(dataSet, configurationFileName, readTimeoutMillis);
}

public CassandraEmbedded(CQLDataSet dataSet, String configurationFileName, long startUpTimeoutMillis) {
super(dataSet, configurationFileName, startUpTimeoutMillis);
}

public CassandraEmbedded(CQLDataSet dataSet, String configurationFileName, long startUpTimeoutMillis, int readTimeoutMillis) {
super(dataSet, configurationFileName, startUpTimeoutMillis, readTimeoutMillis);
}

public void start() throws Exception {
before();
}

public void stop() {
after();
}
}

Now our AbstractTest class that will hide the cluster setup and some utility methods for validation purposes.

import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.List;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

public class AbstractTest {
protected CassandraEmbedded cassandra
= new CassandraEmbedded(new ClassPathCQLDataSet("db_setup.cql", "TestingKeySpace"));

@BeforeClass
public void tearUp() throws Exception {
cassandra.start();
}

@AfterClass(alwaysRun = true)
public void tearDown() {
cassandra.stop();
}

protected Row fetchPersonRowById(String id) {
//then
Statement select = QueryBuilder
.select().all()
.from("person")
.where(eq("id", id));

ResultSet resultSet = cassandra.session.execute(select);

List<Row> rows = resultSet.all();
assertThat(rows.size(), equalTo(1));
return rows.get(0);
}
}

The db_setup.cql will be used for creating the necessary tables and pre-populating data into the embedded Cassandra database. The test that will fetch all persons from the table is counting on having some pre-populated data. Let’s create the db_setup.cql file:

CREATE TABLE person(
id text,
name text,
PRIMARY KEY(id)
);
INSERT INTO person (id, name) values ('XYZ123', 'John');
INSERT INTO person (id, name) values ('ZYX567', 'Anna');

After have this only-once setup, our test may be created, as well as any tests that need a Cassandra embedded cluster.

import com.datastax.driver.core.Row;
import com.github.picadoh.examples.cassandra.util.AbstractTest;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.List;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;

public class PersonDaoImplTest extends AbstractTest {
private PersonDao personDao;

@BeforeClass
public void setUp() throws Exception {
personDao = new PersonDaoImpl(cassandra.session);
}

@Test
public void shouldReturnStoredPersons() throws Exception {
//given
Person john = new Person("XYZ123", "John");
Person anna = new Person("ZYX567", "Anna");

//when
List<Person> personList = personDao.findAll();

//then
assertThat(personList, hasItem(john));
assertThat(personList, hasItem(anna));
}

@Test
public void shouldInsertNewPerson() throws Exception {
//given
String personId = "ABC988";
String personName = "Joanne";
Person newPerson = new Person(personId, personName);

//when
personDao.insert(newPerson);
// then
Row personRow = fetchPersonRowById(personId);
assertThat(personRow.getString("id"), equalTo(personId));
assertThat(personRow.getString("name"), equalTo(personName));
}
}

Conclusion

It’s important to get your application properly tested locally before deploying into any integration environment. Cassandra Unit project provides the ability of creating a local Cassandra cluster that may be injected into your DAOs in order to emulate the real cluster while testing.

Some tweaks were made in order to have a seamless integration with TestNG while removing the boilerplate cluster setup from the test code itself.

Hope this article helps. Have a good testing.