Asserting with a dataset file¶
A dataset can state what a table should contain after a test, not only what it contained before. This is the half of the DBUnit comparison CassandraUnit never had.
Dependency: cassandra-unit-dataset, or cassandra-unit, which includes it. See What to declare.
Checking one value, or one row count? Use the fluent API instead — see Asserting in code. It is the same comparison, without a file. This page is for the case a file is good at: stating every row a table should hold.
@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 the test passed.
The same file, both directions¶
The load rules and the assert rules are deliberately identical, so one file can state the setup and the expectation:
| In the file | Loading | Asserting |
|---|---|---|
| column absent from a row | not written | not asserted |
column present with null |
writes a tombstone | asserts it reads back as null |
That symmetry is the point. It also fixes the limit: on read-back a tombstone and a never-written cell are indistinguishable, so an expected dataset can assert "this reads as null" but never "this was tombstoned".
CSV cannot express an explicit null — an empty field means unset — so a CSV expectation can never assert a null column.
What gets compared¶
Rows are matched on the primary key. Every expected row must give the full key, and no part of
it may be null — Cassandra allows neither, so such a row could never match anything. Both are
errors, not failures. Matching by key is what makes a wrong value report as one column on the right
row, rather than as a missing row plus an unexpected one.
Only the columns the file mentions, plus the primary key. Nothing else is selected, so an unrelated column of a type the driver cannot decode cannot break your assertion.
Never SELECT *, never ALLOW FILTERING. The statement is echoed in the failure message so you
can see exactly what was compared.
Ordering¶
Across partitions, order is never compared, and this is not configurable. An unrestricted
SELECT returns partition-token order — stable, but meaningless to whoever wrote the fixture.
Within a partition, order is meaningful and can be asserted with checkingClusteringOrder(), off by
default. Database Rider's orderBy = {...} has no equivalent here: Cassandra can only order by
clustering columns inside one partition, so the option is a yes/no, not a column list.
Values¶
Both sides go through the same codec, so they arrive as the same Java type and compare with
equals — which gives collections the right semantics for free: list ordered, set and map not.
Five exceptions, each for a real Cassandra behaviour:
collection null ≡ empty |
the driver's codecs decode an absent collection to an empty one, never to null, so these must compare equal |
BigDecimal by compareTo |
1.5 and 1.50 are the same decimal; equals is scale-sensitive |
float / double exact |
a fixture value round-trips exactly. withNumericTolerance(eps) is for values the code under test computed |
ByteBuffer duplicated |
reading one moves its position; a consumed buffer must not look different |
| UDT and tuple | compared by type plus fields, which works because both sides carry the same type instance |
Counters are assertable even though a row dataset cannot write one — they read back as a
bigint. Static columns need nothing special, but their value repeats on every row of a
partition, so every expected row in that partition must agree on it.
Strict by default¶
A table the dataset names must hold exactly the rows listed. A table it does not name is not asserted at all.
This is stricter than DBUnit's usual default, deliberately. Cassandra is upsert-only and has no unique constraints, so the bug an integration test most needs to catch is a write landing in the wrong partition or under the wrong clustering key. That produces an extra row — invisible to a contains-style assertion, and with no constraint violation to catch it for you.
containing() relaxes it, for suites that pre-seed reference data the fixture does not describe.
widget: [] therefore means "this table is empty", which falls out for free.
Options¶
ignoringColumns("created") |
not compared, not even selected. The usual case is a now() timestamp. A primary-key column cannot be ignored — it is how rows are matched |
containing() |
allow rows the dataset does not list |
withinMentionedPartitions() |
assert only the partitions the dataset names, leaving the rest of the table alone. Still strict inside each named partition |
checkingClusteringOrder() |
also assert row order within each partition |
withNumericTolerance(0.001) |
absolute tolerance for float and double |
The failure message¶
Values are rendered as CQL literals, so they paste into cqlsh:
Expected dataset does not match keyspace mykeyspace
expected : classpath:rows/assertion-data.yaml
mode : strict - every row in the asserted scope must be listed
mykeyspace.widget - 3 expected, 3 actual: 1 missing, 1 unexpected, 1 different
SELECT id, label, tags, created, quantity, ratio, props FROM mykeyspace.widget
missing (expected, not found in the database)
id=1690e8da-5bf8-49e8-9583-4dff8a570702
label = 'two'
tags = {}
quantity = 7
unexpected (in the database, not in the expected dataset)
id=1690e8da-5bf8-49e8-9583-4dff8a5707ff
label = 'stray'
tags = {}
created = NULL
different
id=1690e8da-5bf8-49e8-9583-4dff8a570701
label expected '1' but was 'one'
quantity expected 42 but was 41
The three sections are separate because they have different causes: missing means a write did not happen, unexpected means one happened that should not have, different means one wrote the wrong value.
Failures and errors are different¶
DataSetMismatchError (an AssertionError) |
the data does not match — the code under test is wrong |
ParseException |
the expectation is unusable: an unknown column, a row missing part of its key or setting part of it to null, a .cql file used as an expectation — the test is wrong |
Test engines report the first as a failure and the second as an error. Getting that backwards sends someone hunting through production code for a typo in a fixture.
DataSetMismatchError.getDifferences() gives the differences as data if you want to react to them
rather than read them.
Wiring it up¶
Jupiter, embedded server — nothing to add. CassandraUnitExtension picks the annotation up:
@RegisterExtension
static CassandraUnitExtension cassandra = new CassandraUnitExtension(...);
@Test
@ExpectedCassandraDataSet(value = "rows/expected.yaml", keyspace = "mykeyspace")
void myTest() { ... }
Jupiter, your own Cassandra — register the extension with your session:
@RegisterExtension
final ExpectedCassandraDataSetExtension expectations =
new ExpectedCassandraDataSetExtension(fixtures::getSession);
JUnit 4 — a sibling rule, chained. It cannot be folded into CassandraCQLUnit, because
ExternalResource is handed no Description and so cannot see a method annotation:
@Rule
public RuleChain rules = RuleChain.outerRule(cassandra)
.around(new ExpectedCassandraDataSetRule(cassandra::getSession));
Spring — nothing to add; the existing listeners check it, before they clean the keyspace.
Without any framework:
ExpectedDataSetFactory.fromClassPath("rows/expected-widget.yaml", "mykeyspace")
.ignoringColumns("created")
.verify(session);
From a builder, with no file at all — an expectation is a row dataset read the other way round, so one built in Java states it just as well. Everything on this page applies unchanged:
RowsCQLDataSet fixtures = CQLDataSetFactory.builder("mykeyspace")
.table("widget").columns("id", "label")
.row(widgetId, "dispatched")
.build();
ExpectedDataSetFactory.of(fixtures, "mykeyspace").verify(session);
Not supported¶
- TTL and writetime. Not expressible in the row format, and adding them would mean inventing
per-column metadata syntax in four file formats. Use a
.cqlscript andSELECT WRITETIME(...). - Arbitrary
WHERE. The two scopes cover the real cases without ever emittingALLOW FILTERING. - Placeholder matchers (
[any],regex:). They would make the file a third dialect on top of YAML and CQL;ignoringColumnscovers most of what they are reached for. - Schema assertions. A different feature for a different audience.
- Tables are capped at 10,000 rows per comparison. An expectation pointed at a real table should fail fast and say so.