Skip to content
 
 

Repository files navigation

CassandraUnit

CassandraUnit

Test fixtures and assertions for Apache Cassandra, with an embedded server if you want one.

Maven Central build docs License: MIT JDK 17+

CassandraUnit does three things. Take all of them, or just the one you need.

  1. Load test data. A YAML, JSON, XML, CSV or CQL file — or a builder, in Java — becomes rows in a real keyspace, with every value converted using the column's actual type, read from the live schema.
  2. Assert what the database holds. Fluent and AssertJ-native for a single value or row count, or a dataset file for every row a table should hold.
  3. Start a Cassandra. A real Apache Cassandra node inside your test JVM, for when you want one and would rather not run Docker.

The first two work against any CqlSession — a container, a local node, ScyllaDB, Astra. Only the third starts a server, and it is optional.

Load test data

# widget.yaml — data only; the schema stays in a .cql script
widget:
  - id: 1690e8da-5bf8-49e8-9583-4dff8a570701
    label: "1"                       # stays the string "1", never the number
    tags: [alpha, beta]
    created: "2026-09-19T10:00:00Z"
    status: pending
new CQLDataLoader(session).load(
        CQLDataSetFactory.fromClassPathAll("mykeyspace", "cql/schema.cql", "widget.yaml"));

Every value is converted using the column's actual type, read from the live schema — so uuid, timestamp, blob, collections and UDTs need no hand-formatted CQL literals, and a text column holding "1" stays text rather than becoming the number 1.

For a handful of rows, skip the file — the same dataset, next to the test that needs it:

new CQLDataLoader(session).load(CQLDataSetFactory.builder("mykeyspace")
        .table("widget").columns("id", "label", "status")
            .row(widgetId, "one", "pending")
            .row(otherId, "two", "shipped")
        .build());

Hand over the objects you already have — a UUID, an Instant, a Set<String> — rather than spelling them as text.

YAML, JSON, XML, CSV, a plain .cql script, or the builder. See Datasets.

Assert what the database holds

import static org.cassandraunit.assertion.CqlAssertions.assertThat;

@Test
void shipping_marks_it_dispatched(CqlSession session) {
    new ShippingService(session).ship(widgetId);

    assertThat(session).keyspace("mykeyspace")
            .table("widget").row("id", widgetId)
                .hasValue("status", "dispatched")
                .hasNull("held_until");
}

Fluent and AssertJ-native, with nothing to register — static methods over a session, so they work under any framework or none. See Asserting in code.

For the stricter version, point a dataset file at the result with @ExpectedCassandraDataSet and it checks every row, catching the write that landed in the wrong partition — the failure a hand-written SELECT never looks for. See Asserting with a dataset file.

Start a Cassandra

class WidgetTest {

    @RegisterExtension
    static final CassandraUnitExtension cassandra = new CassandraUnitExtension(
            CQLDataSetFactory.fromClassPathAll("mykeyspace", "cql/schema.cql", "widget.yaml"));

    @Test
    void readsTheFixture(CqlSession session) {   // injected by the extension
        ...
    }
}

A real Apache Cassandra node, in your test JVM, with the schema and rows already loaded. Roughly three seconds of startup, once per JVM, and no Docker.

It is one node per JVM, your test JVM needs the JVM flags a Cassandra server needs, and your JDK is the server's JDK — read the surefire section before your first run, because skipping it fails confusingly.

Against a Cassandra you already have

Loading and asserting need no embedded server. The driver-only cassandra-unit-dataset artifact loads and asserts through any CqlSession you hand it:

I already have one — a container, a local node, ScyllaDB, Astra Your first test — your own Cassandra — no embedded server, no JVM flags, no JDK ceiling.
Start one for me, in-process Your first test — embedded server — cassandra-unit, the embedded server above.

Against a container, the whole setup is one extension:

@Testcontainers
class WidgetIT {

    @Container
    static final CassandraContainer cassandra =
            new CassandraContainer("cassandra:5.0").withReuse(true);

    @RegisterExtension
    static final CqlDataSetExtension fixtures = CqlDataSetExtension
            .using(() -> CqlSession.builder()
                    .addContactPoint(cassandra.getContactPoint())
                    .withLocalDatacenter(cassandra.getLocalDatacenter())
                    .build())
            .closingSession()
            .schemaOnce(CQLDataSetFactory.fromClassPath("cql/schema.cql", "mykeyspace"))
            .rowsPerTest(CQLDataSetFactory.fromClassPath(
                    "data/widget.yaml", false, false, "mykeyspace"))
            .build();

    @Test
    void readsTheFixture(CqlSession session) {
        ...
    }
}

The container supplies the node; the extension supplies the schema and the rows.

Runnable examples

cassandra-unit-examples is a standalone Maven project you can clone and mvn test. It covers the Jupiter extension and the JUnit 4 rule, CqlDataSetExtension against a session you supply, all four row formats, both ways of asserting, isolation between tests, the Spring integration, and starting on a random port or a custom cassandra.yaml.

They are executable JUnit tests rather than snippets, so they either pass or tell you they do not.

Also in the box

  • Create the schema from a CQL script.
  • Integrations for JUnit 4 (@Rule), JUnit Jupiter (Extension) and Spring Test, including Spring Boot.
  • truncateKeyspace to empty tables between tests without dropping the schema.

Documentation

Full documentation is published at jsevellec.github.io/cassandra-unit, built from docs/ and so versioned alongside the code:

Working code lives in a separate repository: cassandra-unit-examples.

The old project wiki is retired. It had drifted to the point of documenting classes and annotation attributes that never existed; its pages now point here.

Requirements

The two artifacts have different requirements, and the difference is the main reason to prefer one.

cassandra-unit-dataset cassandra-unit
Apache Cassandra none — you supply the session embedded, pulled in transitively
JDK 17 or later, no upper bound 17 — nothing else
Surefire argLine not needed mandatory when you start the node, see Setup
Maven 3.9+ 3.9+

cassandra-unit's JDK row is not a recommendation, it is the whole supported set: no released Cassandra line supports JDK 18–23, and 24+ never will — see Your first test — embedded server for why. The build enforces it, so a wrong JDK fails with a clear message rather than a confusing crash; if that message surprises you, check mvn -v rather than java -version, because tools like jenv shim mvn only.

cassandra-unit-dataset carries none of that. It starts no daemon, so it needs no JPMS flags, and the 24+ ceiling does not apply — it compiles to 17 and runs on anything later.

Version compatibility

From 5.0.0, the version number leads with the embedded Apache Cassandra major. The minor and patch are cassandra-unit's own, by ordinary semver. The driver version never appears in the number — it is a compatibility fact, listed below. The full policy is in CONTRIBUTING.md.

The artifacts, as of 5.3.0:

artifact Embedded Cassandra CQL driver JUnit Spring JDK
cassandra-unit-dataset none — you supply the session org.apache.cassandra:java-driver-core 4.19.3 Jupiter 6 (optional) 7 (optional) 17+
cassandra-unit 5.0.8 same Jupiter 6 and/or JUnit 4 (optional) — 17 only
cassandra-unit-spring via cassandra-unit same Jupiter 6 7, provided 17 only

JUnit Jupiter 6 is required from 5.3.0, and that is a breaking change: the extensions are Jupiter extensions, so a project still on Jupiter 5 must stay on 5.2.0. It is not a free choice — Spring 7 calls a JUnit 6 API, so Spring 7 and Jupiter 6 move together. The JUnit 4 @Rule integration is unaffected and still runs through the vintage engine.

cassandra-unit-dataset also has a few optional dependencies: assertj-core 3.x for the fluent CqlAssertions API (see Asserting in code), and spring-test + spring-context 7.x for SpringSessions, which loads fixtures through a Spring-managed CqlSession (see Spring integration). Optional dependencies are not transitive, so they reach you only if you declare them yourself.

And the history, which is all cassandra-unit:

cassandra-unit Embedded Cassandra CQL driver JDK
5.1.x 5.0.8 org.apache.cassandra:java-driver-core 4.19.3 17
5.0.x 5.0.8 org.apache.cassandra:java-driver-core 4.19.3 17
4.3.1.0 3.11.5 com.datastax.oss:java-driver-core 4.3.1 (optional) 8
3.7.1.0 3.11.4 com.datastax.cassandra:cassandra-driver-core 3.7.1 (optional) 8
3.11.2.0 3.11.4 same — identical code to 3.7.1.0, see below 8
3.5.0.1 3.11.2 com.datastax.cassandra:cassandra-driver-core 3.5.0 (optional) 8
3.3.0.2 3.11.0 com.datastax.cassandra:cassandra-driver-core 3.3.0 (optional) 8
3.1.3.2 3.9 com.datastax.cassandra:cassandra-driver-core 3.1.3 (optional) 7
2.2.2.1 2.2.2 com.datastax.cassandra:cassandra-driver-core 2.1.9 (optional) 7

Every release before 5.0.0 also put a second CQL driver on your classpath: cassandra-all carries a shaded cassandra-driver-core exposing the older com.datastax.driver.core.* API, and nothing excluded it. 5.0.0 does, so you now get exactly one driver.

Two rows need explaining, because they are the reason this section exists:

  • 4.3.1.0 tracked the driver, not Cassandra. It embeds Cassandra 3.11.5. Everyone who read it as "Cassandra 4" read it the obvious way and was wrong — between 3.0.0.1 and 4.3.1.0 the number followed the DataStax driver, while cassandra-all moved independently.
  • 3.11.2.0 and 3.7.1.0 are the same code, released seventeen minutes apart on 2019-05-09 while the scheme was being reverted to driver-tracking. 3.7.1.0 is the intended one, even though it sorts lower than the release it replaced.

Using a different driver version

The driver is deliberately not managed in this project's dependencyManagement, so you can pick your own 4.x:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.cassandra</groupId>
            <artifactId>java-driver-core</artifactId>
            <version>4.18.1</version>
        </dependency>
    </dependencies>
</dependencyManagement>

Setting a cu.cassandra.driver.version property in your own pom does not work, and this trips people up. The published pom carries the literal ${cu.cassandra.driver.version}, which Maven resolves against cassandra-unit's parent, never yours. Use dependencyManagement above, or declare java-driver-core directly.

Setup

If you already have a Cassandra, this is the whole setup — no surefire block, nothing else:

<dependency>
    <groupId>org.cassandraunit</groupId>
    <artifactId>cassandra-unit-dataset</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
</dependency>

See Your first test — your own Cassandra and stop here.

If you want the embedded server, take cassandra-unit instead — it includes everything above:

<dependency>
    <groupId>org.cassandraunit</groupId>
    <artifactId>cassandra-unit</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
</dependency>

You must also configure surefire

This is not optional, and it is the single biggest difference from older versions. CassandraUnit starts a real Cassandra node inside your test JVM, so that JVM needs the same flags a Cassandra server gets: the JPMS --add-exports / --add-opens set from Cassandra's own conf/jvm17-server.options. Skip them and the test fails on the call that starts the node, with an IllegalAccessException on sun.nio.ch.DirectBuffer.cleaner.

The block to copy is in Your first test — embedded server; it is kept there so there is one copy of it. What needs it is any test JVM that starts the node, not merely having the jar on the classpath — Mixed modules covers a module that runs both kinds of test.

Usage

A dataset is a .cql script, or rows loaded against a schema a .cql script created — from a .yaml / .yml / .json / .xml / .csv file, or from a builder. For a file the format comes from the extension:

new ClassPathCQLDataSet("cql/simple.cql", "mykeyspace")                  // one CQL script

CQLDataSetFactory.fromClassPathAll("mykeyspace",                         // schema, then rows
        "cql/schema.cql", "data/widget.yaml")

CQLDataSetFactory.builder("mykeyspace")                                  // rows, no file
        .table("widget").columns("id", "label").row(1, "hello")
        .build()

Either can go anywhere a dataset is accepted below. See Datasets.

JUnit Jupiter

class MyTest {

    @RegisterExtension
    static CassandraUnitExtension cassandra =
            new CassandraUnitExtension(new ClassPathCQLDataSet("cql/simple.cql", "mykeyspace"));

    @Test
    void queries(CqlSession session) {   // injected by the extension
        ResultSet rs = session.execute("select * from my_table");
        ...
    }
}

JUnit 4

public class MyTest {

    @Rule
    public CassandraCQLUnit cassandra =
            new CassandraCQLUnit(new ClassPathCQLDataSet("cql/simple.cql", "mykeyspace"));

    @Test
    public void queries() {
        ResultSet rs = cassandra.session.execute("select * from my_table");
        ...
    }
}

Spring Test

@ExtendWith(SpringExtension.class)
@ContextConfiguration(...)
@TestExecutionListeners(CassandraUnitTestExecutionListener.class)
@CassandraDataSet(value = {"cql/schema.cql", "data/widget.yaml"}, keyspace = "mykeyspace")
@EmbeddedCassandra
class MySpringTest { ... }

Spring is a provided dependency: your application decides the Spring version. The module is compiled against Spring 7, with its Boot coverage on Spring Boot 4. Spring 7 requires JUnit Jupiter 6.

One embedded Cassandra per JVM

A permanent design constraint, not a bug. Cassandra's DatabaseDescriptor, Schema and StorageService hold static state that cannot be reset in-process, so the first startEmbeddedCassandra call in a JVM wins and a second configuration in that JVM is refused. Test classes that need different configurations need different JVMs, at roughly three seconds each.

Embedded server has the surefire recipe and what cleanup between tests actually depends on. If the constraint is the wrong trade for you, Testcontainers' Cassandra module runs a real node in Docker with no JVM or JDK coupling to your test process.

Migrating from 4.3.1.0

5.0.0 deliberately breaks compatibility. docs/migrating-from-4.md is the full guide and CHANGELOG.md the release-by-release detail. The highlights:

  • Removed: the cassandra-unit-shaded artifact. It existed to hide old, vulnerable copies of guava/netty/jackson. Those versions are gone with the Cassandra 5.0 upgrade, so it has no purpose. Declare your own exclusions if you still need them.
  • Removed: the cu-loader / cu-starter command line tools. cu-starter never worked.
  • Removed: EmbeddedCassandraServerHelper.getRpcPort() — Thrift is gone from Cassandra 4.0+.
  • Removed: @CassandraDataSet(type = ...), and the XML/JSON/YAML dataset enum behind it. Only CQL datasets were loadable in 5.0.0. 5.1.0 adds YAML/JSON/XML/CSV row datasets, chosen by file extension rather than by an attribute — a new design, not the 4.x one.
  • JUnit 4 and Hamcrest are no longer compile-scope dependencies, so they no longer land on your classpath through this library. Declare whichever test framework you actually use.
  • The driver is now a required dependency (org.apache.cassandra:java-driver-core), not an optional one — that was the cause of the recurring NoClassDefFoundError reports.
  • tmpDir now genuinely relocates Cassandra's data, commitlog, hints, saved caches and cdc directories. It previously relocated nothing but a copy of the yaml.
  • New: a JUnit 5 extension, CassandraUnitExtension. See the Jupiter section.

License

This project is licensed under the MIT License - see LICENSE.txt for details.

About

Test fixtures and assertions for Apache Cassandra, with an embedded server if you want one.

Resources

Contributing

Security policy

Stars

419 stars

Watchers

29 watching

Forks

Releases

Packages

Used by

Contributors

Languages