Testing code that talks to Cassandra usually looks like one of these:
- a
@BeforeEachfull ofINSERTstatements, with uuids, timestamps and blobs quoted by hand — and quoted wrong the first three times; - Testcontainers'
withInitScript: one CQL file, and nothing at all for asserting what the code then wrote; - a mocked
CqlSession, which tests the mock.
There is a fourth option, and this repo is 95 runnable tests of it — 88 of them need nothing but a JDK, the other 7 a Docker daemon, and they skip without one.
@RegisterExtension
static CassandraUnitExtension cassandra = new CassandraUnitExtension(
CQLDataSetFactory.fromClassPathAll(KEYSPACE,
"cql/assertionSchema.cql", "rows/assertion-data.yaml"));
@Test
@ExpectedCassandraDataSet(value = "rows/expected-widget.yaml", keyspace = KEYSPACE,
ignoreColumns = "created")
void shipping_a_widget_changes_its_label(CqlSession session) {
session.execute("update ... set label = 'shipped' where id = ...");
}A YAML file sets the table up, your code runs, a YAML file says what should be true afterwards.
Values convert through the real column types read from the live schema, so nothing is quoted by
hand and nothing silently becomes a number — a text column holding "1" stays the string "1".
When the expectation does not match, the failure names the row and the column that differ and
echoes the SELECT it ran, so it pastes into cqlsh.
That snippet is
ExpectedCassandraDataSetAnnotationTest,
with the update shortened. Everything else about it is real, and it runs:
git clone https://github.com/jsevellec/cassandra-unit-examples.git
cd cassandra-unit-examples && mvn test # 78 tests, a minute or two, no DockerA real Apache Cassandra 5.0 starts inside the test JVM — four startups for the whole suite, one
shared and one per example that brings its own cassandra.yaml. Everything resolves from Maven
Central; nothing has to be built first. Use JDK 17, which the build enforces; the reason is
below.
If the node is not yours to start — Testcontainers, a shared CI node, Astra, ScyllaDB — the same
fixtures load through a session you supply, and none of the setup below applies. See
CqlDataSetExtensionTest and
TestcontainersFixtureTest.
| I want to… | file |
|---|---|
| load rows without hand-writing CQL | YamlRowDataSetTest |
| the same fixture in JSON, XML and CSV | RowDataSetFormatsTest |
| write the fixture in Java, with no file at all | BuiltDataSetTest |
| assert what the database holds afterwards | ExpectedCassandraDataSetAnnotationTest |
| …the same, without the annotation | ExpectedDataSetFluentTest |
| assert one value or one row count, fluently | CqlAssertionsTest |
| load into a Cassandra I already run | CqlDataSetExtensionTest |
| load into a Cassandra in Testcontainers | TestcontainersFixtureTest |
| reset between tests without rebuilding the schema | IsolationTest, CleanDataBetweenTestsTest |
| start from the plain embedded-server case | CassandraUnitExtensionTest |
| stay on JUnit 4 | everything in src/test/java/org/cassandraunit/test/cql/ — row datasets, assertions, isolation and Testcontainers included |
| use Spring Test | SpringExpectedCassandraDataSetTest |
| use Spring Boot, with nothing wired up | SpringBootEmbeddedCassandraTest |
load fixtures through my Spring CqlSession bean |
SpringSessionsFixtureTest |
bring my own cassandra.yaml |
StartWithCustomCassandraYamlTest |
That table is the shortlist. Every file under src/test is a working example — including the
narrower ones it leaves out, such as loading a script from disk, driving the server by hand, or
running on a random port — and all of them are in the suite, so whatever is in this repo passes.
src/test/java/org/cassandraunit/test/dataset/
A dataset is either a CQL script (.cql, any statements, creates the schema) or a row
dataset (.yaml, .yml, .json, .xml, .csv — rows and nothing else, needs the schema to
already exist). The format comes from the file extension; there is no type attribute to keep
in sync with the filename.
| Example | Shows |
|---|---|
YamlRowDataSetTest |
The whole idea: uuid, set, map, timestamp, blob written in their natural form, plus null vs. absent |
RowDataSetFormatsTest |
The same rows in JSON, XML and CSV, and where CSV differs |
Because the schema is already in the database when rows load, the loader reads each column's real type from there, and the driver's own codecs do the converting.
Two rules are worth knowing before you write a fixture:
| In the file | Meaning | Effect |
|---|---|---|
| the column is absent from that row | unset | not in the generated INSERT at all — an existing value is left alone |
the column is present with null |
explicit null | a tombstone, erasing any existing value |
and CSV cannot express null: an empty field means unset. A NULL sentinel was deliberately not
invented, because a text column can legitimately contain the string NULL.
A row dataset needs its schema first, and rule and extension both take exactly one dataset, so chain them:
CQLDataSetFactory.fromClassPathAll("mykeyspace", "cql/schema.cql", "rows/widget.yaml")The keyspace is dropped and created once, for the chain. Doing it by hand with two load calls
is where people drop the keyspace they have just populated.
Quote anything whose YAML meaning differs from its CQL meaning — "0x0a0b" for a blob, "1" for
a number-shaped value in a text column. Counters, USING TTL, USING TIMESTAMP and DELETE are
not expressible as rows; use a CQL script for those.
New in 5.2.0, and the sixth format: the same rows, with no file.
RowsCQLDataSet fixtures = CQLDataSetFactory.builder("mykeyspace")
.named("the widget fixture, built in code")
.table("widget").columns("id", "label", "quantity", "created")
.row(id, "ordered", 1, Instant.parse("2026-09-19T10:00:00Z"))
.build();For three rows a file is a file's worth of ceremony, and it puts the fixture somewhere other than
the test that needs it. This is not a second loader: build() returns the same RowsCQLDataSet a
.yaml parses to, so the column types still come from the live schema and the rule, the extensions
and CQLDataLoader take it as they take any dataset.
What it can do that a file cannot is take the object you already have — a UUID, an Instant, a
Set<String> — rather than its string form. The rest of the rules are unchanged: a positional
row(...) fills the columns declared for the table, row(Map) is for a row of a different shape,
an explicit null tombstones and an absent column stays unset. Keyspace creation and deletion
default to off, because a builder describes rows and never schema.
Being a RowsCQLDataSet, the same object can also state the expectation —
ExpectedDataSetFactory.of(fixtures, "mykeyspace").verify(session) — which is why the example
declares it as that concrete type rather than as CQLDataSet.
A malformed dataset raises ParseException at the call that malformed it, not at build(), so
the stack trace points at the row that is wrong.
CQLDataLoader.Isolation |
|
|---|---|
DATASET |
Honour the dataset's own keyspace flags — normally drop the keyspace and rebuild it. The default, and what every release before 5.1.0 did |
TRUNCATE |
Keep the keyspace and its schema; empty every table instead |
NONE |
Clear nothing; the keyspace is still selected if it exists |
TRUNCATE is far cheaper — the library measures a median of 940ms against 1.6ms for two tables,
and 1640ms against 10.7ms for fifty, because a schema rebuild is not free. It is not the default
because it is not a drop-in: it ignores the dataset's creation and deletion flags, so a
per-test dataset that builds its own schema breaks under it. Pair it with a schema loaded once —
CQLDataLoader.loadIfKeyspaceAbsent, or schemaOnce on CqlDataSetExtension — and keep the
per-test dataset to rows.
new CQLDataLoader(session).load(rows, Isolation.TRUNCATE);
new CassandraUnitExtension(dataSet).withIsolation(Isolation.TRUNCATE);CqlOperations.truncateKeyspace(session, keyspace, excludedTables...) is the same primitive
without a load, and CqlOperations.quote(identifier) quotes an identifier that needs it — both
public API as of 5.1.0. See
CleanDataBetweenTestsTest.
src/test/java/org/cassandraunit/test/assertion/
@Test
@ExpectedCassandraDataSet(value = "rows/expected-widget.yaml", keyspace = "mykeyspace")
void shipping_a_widget_marks_it_dispatched() {
service.ship(widgetId);
}Verified after the test method, and only if it passed. The load rules and the assert rules are identical, so one file can state the setup and the expectation.
| Example | Shows |
|---|---|
ExpectedCassandraDataSetAnnotationTest |
The annotation, ignoreColumns, MatchMode.CONTAINS, Scope.MENTIONED_PARTITIONS, checkClusteringOrder |
ExpectedDataSetFluentTest |
ExpectedDataSetFactory without the annotation, and reading DataSetMismatchError.getDifferences() |
CQLScriptLoadWithExpectedDataSetRuleTest |
JUnit 4, as a chained rule |
SpringExpectedCassandraDataSetTest |
Spring, where the listeners check it before dropping the keyspace |
Strict by default: a table the file names must hold exactly the rows it lists; a table it does
not name is not asserted at all. That is stricter than DBUnit's usual default, deliberately —
Cassandra is upsert-only and has no unique constraints, so the bug worth catching is a write
landing in the wrong partition, which produces an extra row that a contains-style assertion never
sees. containing() relaxes it.
Rows are matched on the primary key, only the mentioned columns are selected, and the failure
message renders values as CQL literals with the SELECT that was run, so it pastes into cqlsh. A
mismatch is a DataSetMismatchError (an AssertionError) — the code under test is wrong. An
unusable expectation is a ParseException — the test is wrong. Engines report the first as a
failure and the second as an error, which is the right way round.
Wiring, by integration: CassandraUnitExtension and the Spring listeners pick the annotation up
with nothing added; against a session you supply, register ExpectedCassandraDataSetExtension; on
JUnit 4, chain ExpectedCassandraDataSetRule.
Also new in 5.2.0, and the companion to the annotation rather than a replacement for it. A dataset file is the right tool for these are all the rows this table should hold; this is the right tool for one value or one row count, where a file would be out of proportion.
import static org.cassandraunit.assertion.CqlAssertions.assertThat;
assertThat(session).keyspace("mykeyspace")
.table("widget")
.hasRowCount(3)
.row("id", widgetId)
.hasValue("label", "ordered")
.hasNull("created");Both halves agree on what equal means — they share one value comparison — so an empty set reading
back as null, or 1.50 against 1.5, behaves identically whichever you use, and expected values
take the same forms a row dataset takes (hasValue("quantity", 1) against a bigint, a uuid as
its string form). A row is addressed by its whole primary key, so a table with a clustering
column needs the map form.
assertThat(row) and assertThat(resultSet) cover what you fetched yourself, and
keyspace(...).matches(expectedDataSet) hands a whole file-shaped expectation back to the chain.
Needs assertj-core, which is optional in cassandra-unit; every assert type extends AssertJ's
AbstractAssert, so as(), satisfies() and SoftAssertions work as usual.
CqlDataSetExtension starts nothing. You give it a session — a Testcontainers container, a node CI
already runs, a managed service — and it loads the same datasets through that:
@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("rows/widget.yaml", false, false, "mykeyspace"))
.build();Build the session inside the lambda: Jupiter runs declaratively registered extensions —
@Testcontainers among them — before @RegisterExtension ones. And the extension never closes a
session it did not create; closingSession() opts in.
The fixture layer is its own artifact, with no cassandra-all, no jamm agent and no JDK ceiling:
<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit-dataset</artifactId>
<version>5.3.0</version>
<scope>test</scope>
</dependency>None of the surefire argLine above is needed for it — that block is the price of the embedded
server, not of the fixtures.
Two examples, one shape:
| Example | Session comes from |
|---|---|
CqlDataSetExtensionTest |
the embedded server, standing in for "a node you own" — no Docker |
TestcontainersFixtureTest |
a real cassandra:5.0 container |
The dataset code in the two files is identical; only the supplier differs. The container one needs
two more test-scope dependencies — note the 2.x rename, testcontainers-cassandra, not cassandra:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-cassandra</artifactId>
<version>2.0.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<version>2.0.5</version>
<scope>test</scope>
</dependency>@Testcontainers(disabledWithoutDocker = true) makes it skip where there is no Docker; the
attribute defaults to false, which errors instead, so set it. Cassandra runs on the JDK the image
ships, so the JDK 17 ceiling below does not apply to that path.
src/test/java/org/cassandraunit/test/junit6/
| Example | Shows |
|---|---|
CassandraUnitExtensionTest |
The normal case: @RegisterExtension + a CqlSession injected straight into the test method |
CqlDataSetWithoutKeyspaceCreationTest |
When the script owns its CREATE KEYSPACE |
EmbeddedCassandraManualStartTest |
No extension: drive EmbeddedCassandraServerHelper and CQLDataLoader yourself |
MultipleDataSetsTest |
Schema and data in separate scripts, and what the keyspace flags mean |
CleanDataBetweenTestsTest |
Resetting state without restarting Cassandra: CqlOperations, truncateKeyspace, cleanDataEmbeddedCassandra |
FileCqlDataSetTest |
Loading a script from disk rather than the classpath |
CassandraUnitExtension has no no-arg constructor — the dataset comes in through it — so it is used
with @RegisterExtension on a static field, never @ExtendWith. From 5.3.0 these run on
Jupiter 6 — 6.1.3 here — and the API they use is unchanged from JUnit 5.
src/test/java/org/cassandraunit/test/cql/ — still fully supported, kept as the reference for
projects that have not migrated, and unaffected by the move to Jupiter 6. Needs
junit-vintage-engine to run alongside Jupiter; it is versioned with Jupiter, so 6.1.3 here.
| Example | Shows |
|---|---|
CQLScriptLoadWithJunitRuleTest |
The @Rule and its public session field |
CQLScriptLoadWithAbstractTestCaseTest |
AbstractCassandraUnit4CQLTestCase, which also cleans up after each method |
CQLScriptLoadWithoutKeyspaceCreationTest |
keyspaceCreation = false |
CQLScriptLoadWithNativeApproachTest |
Manual start + load |
CQLScriptLoadWithExpectedDataSetRuleTest |
@ExpectedCassandraDataSet through a RuleChain |
RowDataSetRuleTest |
A YAML row dataset through the @Rule — the same CQLDataSet the extension takes |
CqlAssertionsRuleTest |
CqlAssertions on JUnit 4: a static import, no framework coupling |
IsolationRuleTest |
The three Isolation modes, and CqlOperations.truncateKeyspace on its own |
TestcontainersRuleTest |
Fixtures into a container, with both lifecycles hand-rolled |
Two things have no JUnit 4 equivalent, and TestcontainersRuleTest shows what to do instead:
- No rule takes a session you supply.
CassandraCQLUnitandAbstractCassandraUnit4CQLTestCasealways start the embedded server;CqlDataSetExtensionis Jupiter-only. Against your own Cassandra, callnew CQLDataLoader(session).load(dataSet, isolation)— it has always taken a session.ExpectedCassandraDataSetRuleis the exception: it takes aSupplier<CqlSession>, but it only verifies, it does not load. - Testcontainers 2.x dropped
@Rulesupport.GenericContainerno longer extendsFailureDetectingExternalResource, so@ClassRuledoes not compile; start and stop the container in@BeforeClass/@AfterClass. There is nodisabledWithoutDockereither —Assume.assumeTrue(...)skips the class instead.
src/test/java/org/cassandraunit/test/spring/cql/
| Example | Shows |
|---|---|
SpringCQLScriptLoadTest |
@ExtendWith(SpringExtension.class) + CassandraUnitTestExecutionListener, reloading per test method |
SpringCassandraUnitAnnotationTest |
The composed @CassandraUnit annotation and dataset-by-convention |
SpringExpectedCassandraDataSetTest |
A row dataset as the fixture, and @ExpectedCassandraDataSet as the assertion |
There is no cassandra-unit-specific Jupiter extension for Spring: use Spring's own
SpringExtension and add cassandra-unit as a TestExecutionListener. @EmbeddedCassandra is
mandatory — the listener does a requireNonNull on it, so @CassandraDataSet alone fails with
an NPE. @CassandraDataSet takes several locations and any supported extension, so schema and rows
can be listed together; the first one drops and creates the keyspace.
src/test/java/org/cassandraunit/test/spring/boot/
New in 5.3.0, and the thing issue #217 asked for in 2017. Annotate a @SpringBootTest with
@EmbeddedCassandra and Boot's own auto-configured CqlSession connects to the embedded node:
@SpringBootTest(classes = WidgetBootTest.BootApplication.class)
@TestExecutionListeners(value = CassandraUnitTestExecutionListener.class,
mergeMode = MergeMode.MERGE_WITH_DEFAULTS)
@EmbeddedCassandra
@CassandraDataSet(value = {"cql/widgetSchema.cql", "rows/widget.yaml"}, keyspace = "mykeyspace")
class WidgetBootTest {
@Autowired
CqlSession session; // Boot's bean, pointed at the embedded node by nothing you wrote
@Test
void readsTheFixture() {
long rows = session.execute("select count(*) from mykeyspace.widget").one().getLong(0);
assertThat(rows).isEqualTo(4);
}
}There is no contact point in that file, and no application.yml anywhere in this repo. Before the
context refreshes, cassandra-unit publishes the node's real address into the test's Environment:
| Property | Value |
|---|---|
spring.cassandra.contact-points |
the host the node bound to |
spring.cassandra.port |
the port it really got |
spring.cassandra.local-datacenter |
datacenter1 |
This exists because the defaults do not line up. Boot and the driver both default to 9042, while the embedded node listens on 9142 — deliberately, so it cannot collide with a Cassandra you are already running locally.
| Example | Shows |
|---|---|
SpringBootEmbeddedCassandraTest |
The whole idea, plus the precedence rule below |
SpringBootRandomPortTest |
cu-cassandra-rndport.yaml, the case a properties file cannot express |
ExposedPropertiesOptOutTest |
exposeProperties = false, when you want your own values |
The mechanism is a ContextCustomizerFactory in META-INF/spring.factories. It returns null for
any class without the annotation, contributing nothing — not even a context cache key entry — so a
project that uses this for one test class does not load the embedded server for the rest.
Four things to know before you copy that snippet:
- The published values win. The property source is added first, so it outranks
@TestPropertySource, inlined properties andapplication-test.yml. If you previously bridged this gap by hand, your literal is now overridden by the address the node is really listening on.@EmbeddedCassandra(exposeProperties = false)keeps your own values instead — and note that opting out silences the publishing, not the server: the node still starts. MERGE_WITH_DEFAULTSis not optional. A bare@TestExecutionListenersreplaces the default listeners, and dependency injection is one of them, so@Autowiredsilently stops happening and your fields stay null.- Do not set
spring.cassandra.keyspace-name. Boot would build the session withwithKeyspace(...)during the refresh — before any dataset had a chance to create that keyspace — and the context would fail to start. Name the keyspace in@CassandraDataSetand qualify your queries, as above. Qualifying is needed anyway: the dataset'sUSEapplied to cassandra-unit's own session, not to Boot's bean. - Boot 4 split auto-configuration into one module per technology.
CassandraAutoConfigurationnow lives inspring-boot-cassandra; on Boot 3 it was inspring-boot-autoconfigure.
SpringSessions loads the same fixtures through a CqlSession bean rather than starting
anything, which is the path for Testcontainers, a shared cluster or Astra:
@RegisterExtension
static final CqlDataSetExtension fixtures = CqlDataSetExtension
.using(SpringSessions.fromApplicationContext())
.schemaOnce(CQLDataSetFactory.fromClassPath("cql/schema.cql", "mykeyspace"))
.rowsPerTest(CQLDataSetFactory.fromClassPath("rows/widget.yaml", false, false, "mykeyspace"))
.build();| Example | Shows |
|---|---|
SpringSessionsFixtureTest |
The bean-by-type lookup, and the Testcontainers shape in a comment |
SpringSessionsByBeanNameTest |
fromApplicationContext("name"), for a context with two sessions |
It lives in cassandra-unit-dataset, so it needs none of the surefire argLine below and has
no JDK ceiling. spring-test and spring-context are optional dependencies there — they reach you
only because you already have Spring.
Three things bite. Never add closingSession(): the bean belongs to the context, and Jupiter runs
afterAll in reverse registration order, so a closed bean would stay in the shared context cache.
Leave the CqlSession test-method parameter bare — annotating it makes SpringExtension claim
it too and Jupiter fails with Discovered multiple competing ParameterResolvers. And
@DirtiesContext is fine at class level but not AFTER_EACH_TEST_METHOD, because the session is
resolved once per class.
Pick one path per class: the annotations load through the embedded server, CqlDataSetExtension
loads through a session you own, and combining them means two ideas of where the data went.
src/test/java/org/cassandraunit/test/
| Example | Shows |
|---|---|
StartWithCustomCassandraYamlTest |
Your own cassandra.yaml — see another-cassandra.yaml |
StartWithRandomPortTest |
CASSANDRA_RNDPORT_YML_FILE, so parallel builds on one machine cannot collide |
Both run in their own JVM, as does
SpringBootRandomPortTest.
One Cassandra per JVM is a hard constraint: DatabaseDescriptor,
Schema and StorageService hold static state that cannot be reset in-process, so a JVM is pinned
to the first configuration it starts. See the isolated-config-tests surefire execution in
pom.xml for how the suite is split. For the same reason every example class here owns
its own keyspace: they share one embedded node, and a DATASET load drops the keyspace it names.
Write your own yaml by starting from the bundled one — Cassandra 5 rejects unknown properties,
so any pre-4.x file (start_rpc, rpc_port, thrift_*, the *_in_ms spellings) will not load at
all:
unzip -p ~/.m2/repository/org/cassandraunit/cassandra-unit/5.3.0/cassandra-unit-5.3.0.jar cu-cassandra.yamlTwo dependencies — note there is no explicit driver dependency. Since 5.0.0 the driver is a
required dependency of cassandra-unit, so it arrives transitively. Its coordinates also moved:
com.datastax.oss:java-driver-core is frozen at 4.17.0 and the driver now releases as
org.apache.cassandra:java-driver-core. The Java packages are unchanged
(com.datastax.oss.driver.*), so declaring the old coordinates puts two jars with the same
packages on your classpath for no benefit.
<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit</artifactId>
<version>5.3.0</version>
<scope>test</scope>
</dependency>
<!-- only if you use the Spring integration -->
<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit-spring</artifactId>
<version>5.3.0</version>
<scope>test</scope>
</dependency>5.3.0 requires JUnit 6. Spring 7's SpringExtension calls ExtensionContext.Store.computeIfAbsent,
which JUnit 5 spells getOrComputeIfAbsent, so Spring 7 cannot run on JUnit 5 at all — and every
Jupiter extension cassandra-unit publishes is compiled against it. If you are on JUnit 5, stay on
cassandra-unit 5.2.0 with Spring 6.2 and Boot 3. The JUnit 4 @Rule integration is unaffected and
still runs through the vintage engine, now 6.1.3.
JUnit is optional in cassandra-unit — declare whichever platform you use (junit-jupiter, or
junit 4 plus junit-vintage-engine for the @Rule). Spring is provided in
cassandra-unit-spring, and provided scope is not transitive, so declare spring-test and
spring-context yourself. Spring Boot is not a dependency of cassandra-unit at all — the module
only publishes a ContextCustomizerFactory that Spring Test discovers — so a Boot test needs
spring-boot, spring-boot-autoconfigure, spring-boot-cassandra, spring-boot-test and
spring-boot-test-autoconfigure declared at test scope. Prefer those five explicit artifacts over
spring-boot-starter-test: the starter brings its own JUnit, AssertJ and spring-test versions, and
spring-boot-dependencies manages jackson and snakeyaml, which would quietly override the
jackson pin below. CSV datasets need
com.fasterxml.jackson.dataformat:jackson-dataformat-csv, which is optional in cassandra-unit
and therefore not transitive; YAML, JSON and XML need nothing. The fluent CqlAssertions are
optional in the same way and need assertj-core — without it, touching that class raises
NoClassDefFoundError: org/assertj/core/api/AbstractAssert. Nothing else in the library requires
either.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.22.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>cassandra-unit 5.3.0 otherwise leaves a consumer with a mixed family: jackson-databind 2.22.1
arrives through cassandra-unit-dataset, while jackson-core and jackson-annotations come from
cassandra-all at 2.19.2 and win on declaration order. The embedded daemon then dies during commit
log initialisation with NoClassDefFoundError: com/fasterxml/jackson/annotation/JsonSerializeAs,
which surfaces as "Cassandra daemon did not start within timeout" — a message pointing nowhere
near the cause. The BOM settles it, and the CSV dependency then needs no version of its own.
The surefire argLine — mandatory for the embedded server, or the fork dies with "The forked VM terminated without properly saying goodbye"
Cassandra 5.0 reaches deep into the JDK, so the embedded daemon needs the same JVM flags a real
node gets. Without them surefire dies with The forked VM terminated without properly saying goodbye, which tells you nothing about the cause. The jamm agent path is resolved by
maven-dependency-plugin, so no version is baked into a path — jamm arrives transitively with
cassandra-all. If you only use cassandra-unit-dataset against your own Cassandra, you need none
of it.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<phase>initialize</phase>
<goals><goal>properties</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<argLine>
-javaagent:${com.github.jbellis:jamm:jar}
-Djdk.attach.allowAttachSelf=true
-Dio.netty.tryReflectionSetAccessible=true
--add-exports java.base/jdk.internal.misc=ALL-UNNAMED
--add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED
--add-exports java.management/com.sun.jmx.remote.security=ALL-UNNAMED
--add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED
--add-exports java.rmi/sun.rmi.server=ALL-UNNAMED
--add-exports java.sql/java.sql=ALL-UNNAMED
--add-exports java.base/java.lang.ref=ALL-UNNAMED
--add-exports jdk.unsupported/sun.misc=ALL-UNNAMED
--add-opens java.base/java.lang.module=ALL-UNNAMED
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED
--add-opens java.base/jdk.internal.ref=ALL-UNNAMED
--add-opens java.base/jdk.internal.reflect=ALL-UNNAMED
--add-opens java.base/jdk.internal.math=ALL-UNNAMED
--add-opens java.base/jdk.internal.module=ALL-UNNAMED
--add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED
--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED
--add-opens java.base/sun.nio.ch=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED
</argLine>
</configuration>
</plugin>Why JDK 17, exactly
The embedded daemon runs inside the build JVM, Cassandra 5.0 supports only JDK 11 and 17, and
spring-test 7.0 needs 17 — and on JDK 24+ Cassandra's ThreadAwareSecurityManager calls the
now-removed System::setSecurityManager and throws. This build enforces [17,18) so you get a
sentence instead of a stack trace.
Check with mvn -v, not java -version — a jenv shim overrides JAVA_HOME for Maven only.
There is a .java-version file here for that reason.
The bound belongs to the embedded server. cassandra-unit-dataset, the fixture layer on its
own, runs on 17+ with no ceiling — see
Fixtures without the embedded server.
From 5.0.0 the version number leads with the embedded Apache Cassandra major; the minor and patch are cassandra-unit's own. The driver version never appears in it.
| artifact | Embedded Cassandra | CQL driver | JDK |
|---|---|---|---|
cassandra-unit-dataset |
none — you supply the session | org.apache.cassandra:java-driver-core 4.19.3 |
17+ |
cassandra-unit |
5.0.8 | same | 17 only |
cassandra-unit-spring |
via cassandra-unit |
same | 17 only |
The test platform is a separate axis, and 5.3.0 moved it:
| cassandra-unit | JUnit | Spring | Spring Boot |
|---|---|---|---|
| 5.3.0 | Jupiter 6 | 7.x | 4.x |
| 5.0.0 – 5.2.0 | Jupiter 5 | 6.x | 3.x |
These move together rather than independently: Spring 7 requires JUnit 6, and Boot 4 requires Spring 7.
4.3.1.0 is the trap in the history: it tracked the driver, and embeds Cassandra 3.11.5, not
4. The driver is not managed in cassandra-unit's dependencyManagement, so you can pin your own
4.x in yours.
| Then | Now |
|---|---|
com.datastax.oss:java-driver-core |
org.apache.cassandra:java-driver-core, required rather than optional. Java packages unchanged |
XML / JSON / YAML datasets, DataSetFileExtensionEnum |
Row datasets in YAML, JSON, XML and CSV are back in 5.1.0 — but they describe CQL tables, share nothing with the 4.x Thrift formats, and an old dataset will not load. No type attribute: the extension decides |
Thrift, Hector, DataLoader |
Gone |
@CassandraDataSet(type = ...) |
Gone |
getRpcPort() |
getNativeTransportPort() |
DEFAULT_TMP_DIR = target/embeddedCassandra |
${java.io.tmpdir}/cassandra-unit, and it now really relocates data, commitlog, hints and caches |
cu-loader / cu-starter CLI, cassandra-unit-shaded |
Gone |
| JUnit 4 and Hamcrest on your classpath whether you wanted them or not | Both optional — declare what you use |
| JUnit 5 | CassandraUnitExtension (5.0.0), and CqlDataSetExtension for a session you own (5.1.0). Jupiter 6 as of 5.3.0 |
readTimeoutMillis was stored and ignored |
Now actually applied, so queries can time out. Also setRequestTimeout(Duration) |
| Nothing asserted the end state | @ExpectedCassandraDataSet (5.1.0), and fluent CqlAssertions (5.2.0) |
| A fixture had to be a file | It can be a Java builder instead — CQLDataSetFactory.builder(...) (5.2.0) |
| Spring Boot had to be told where Cassandra was | @EmbeddedCassandra publishes spring.cassandra.* itself, so Boot's auto-configured CqlSession needs no wiring (5.3.0) |
Fixtures could not use a CqlSession your context already had |
SpringSessions.fromApplicationContext() (5.3.0) |
[ERROR] 'dependencies.dependency.systemPath' ... ${jmc5.path}during the build is noise from Apache Cassandra's own parent pom, which declares system-scoped JMC and VisualVM artifacts with unresolved properties. Harmless, and only upstream Cassandra can fix it.- The driver refreshes schema metadata asynchronously. Right after startup,
session.getMetadata().getKeyspaces()is legitimately empty — query the server instead of asserting on it. - Cassandra logs a lot.
logback-test.xmlholds it atWARN; raiseorg.apache.cassandratoDEBUGwhen a startup goes wrong. - "Cassandra daemon did not start within timeout" is a symptom, not a cause. Two things produce it: something else already listening on 9042 — another build of your own, most often — or the jackson mismatch described under Pin jackson. The daemon's real error is in the log above the timeout.
CassandraUnit is MIT as of 5.0.0.