Start
Quick start
The rhythm of every test written with this library is the same: seed, act, await, assert. Here it is end to end.
What you are testing against is an embedded MongoDB emulation. It is a driver-level shim over Nitrite, not a mongod. Your Spring Data code runs unmodified; anything the emulation does not cover fails fast by name. See Supported operations for the surface.
Spring test properties
After adding the Spring Boot dependency, create a test profile under src/test/resources/. The examples below activate it with @ActiveProfiles("test"); import that annotation from org.springframework.test.context.ActiveProfiles.
spring:
mongodb:
database: orders
bonsai:
collections: orders,outbox,billing.invoices
cleanup: drop-databasesSpring Boot 4 uses spring.mongodb.database. Here, unqualified collection names such as orders and outbox use database orders, while billing.invoices targets the separate billing database. The older spring.data.mongodb.* keys do not configure this harness.
These settings are optional: with no configured database or URI database, the default database is test, collections are created on demand, and cleanup defaults to drop-databases. Declaring collections pre-creates them and recreates them after cleanup. The annotation's defaultDatabase takes priority over Spring's database property, which takes priority over a database in the application's MongoDB URI. See Configuration for all settings and precedence.
Embedded mode needs no MongoDB URI, credentials, host, port, or running server. Bonsai replaces the synchronous client and database factory and neutralises the standard Spring connection properties before the context starts. Keep these settings in test resources and activate the harness on the test class as shown next.
1. Annotate the test class
@EnableMongoEmulator is the entire setup. It starts the emulation, replaces the application's MongoClient and MongoDatabaseFactory beans, neutralises the connection properties, and drops every touched database between test methods.
@SpringBootTest
@ActiveProfiles("test")
@EnableMongoEmulator
class OrderRepositoryIT {
// ...
}Databases and collections materialise lazily, so declaring them is optional. Declare them when you want them pre-created, for example when a test asserts on an empty collection, or when a unique index must exist before the first write.
2. Static imports
Assertions come from one entry point, so a single static import covers most tests. Matchers live beside them.
import static io.typecheck.bonsai.assertions.MongoAssertions.assertThatCollection;
import static io.typecheck.bonsai.assertions.MongoAssertions.assertThatChoreography;
import static io.typecheck.bonsai.assertions.MongoFieldMatchers.matchesRegex;3. Seed the data
Drop fixtures under src/test/resources/mongo/seed/ and the loader infers collection and database names from the layout: a file is a collection, a subdirectory is a database.
src/test/resources/mongo/seed/
├── customers.json → collection "customers" in the default database
├── outbox.ndjson → collection "outbox" in the default database
└── billing/ → subdirectory name is the database name
└── invoices.json → collection "invoices" in database "billing"Files are Extended JSON, a JSON array, or NDJSON. The format is detected, not declared.
[
{ "_id": "cust-42", "name": "Ada", "tier": "GOLD" },
{ "_id": "cust-43", "name": "Grace", "tier": "SILVER" }
]Point the class at that directory with @MongoSeed, or seed a single collection from a test method with MongoTestData.seed(...). Both route through the same loader, so behaviour is identical either way. See Seeding & placeholders for formats, precedence, and the ${...} resolver.
4. Act and assert
Call the application code under test: a real service, repository, and transaction boundaries. Then assert on what landed in the store. Assertions read the collection lazily, so they always see the current state.
assertThatCollection("orders")
.hasDocumentCount(1)
.containsDocumentSatisfying(d -> d
.hasField("customerId", "cust-42")
.hasJsonPath("$.lines[0].qty", 2)
.hasFieldMatching("orderNumber", matchesRegex("ORD-\\d{8}")));When a write happens asynchronously, wait for it first. MongoTestAwait.untilCount("outbox", 1) polls every 50 ms up to 10 seconds and, on timeout, fails with a rendered table of what the collection actually held. That is usually the whole diagnosis.
All together
@SpringBootTest
@ActiveProfiles("test")
@EnableMongoEmulator(databases = "billing", collections = {"orders", "outbox"})
@MongoSeed("classpath:mongo/seed/")
class OrderFlowIT {
@Autowired OrderService orderService;
@Test
void placedOrderIsPersistedInvoicedAndOutboxed() {
// 1. seed: method-level, on top of the class-level directory seed
MongoTestData.seed("customers", "classpath:fixtures/customers.json");
// 2. act: real app code, real repositories, real converters, real @Transactional
orderService.placeOrder("cust-42", "WIDGET-1", 2);
// 3. await, then assert
MongoTestAwait.untilCount("outbox", 1);
assertThatCollection("orders")
.hasDocumentCount(1)
.containsDocumentSatisfying(d -> d
.hasField("customerId", "cust-42")
.hasJsonPath("$.lines[0].qty", 2)
.hasFieldMatching("orderNumber", matchesRegex("ORD-\\d{8}")));
assertThatChoreography()
.forId("cust-42")
.collection("orders").contains()
.collection("outbox").contains()
.collection("billing.invoices").contains()
.verify();
}
}Without Spring
The generic harness gives you the same lifecycle with no application context. MongoTestSupport hands you the shim client, a database, or a collection directly.
@EnableMongoEmulator(collections = "orders")
class OrderStoreTest {
@Test
void writesAndReadsBack() {
MongoCollection<Document> orders = MongoTestSupport.collection("orders");
orders.insertOne(new Document("_id", "ord-1").append("status", "CONFIRMED"));
assertThatCollection("orders")
.hasDocumentCount(1)
.containsDocumentMatching("{status: 'CONFIRMED'}");
}
}Next steps
- Configuration: the
bonsai.*property set and the cleanup model. - Seeding & placeholders: fixture formats and the placeholder resolver.
- Assertions: the full fluent surface.
- Transactions: what is emulated and where it stops.
- Supported operations: the operators, stages and driver methods the emulation covers, and the boundaries that fail fast.