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.

src/test/resources/application-test.yml
spring:
  mongodb:
    database: orders

bonsai:
  collections: orders,outbox,billing.invoices
  cleanup: drop-databases

Spring 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.

Java
@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.

Java
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.

text
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.

src/test/resources/mongo/seed/customers.json
[
  { "_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.

Java
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

OrderFlowIT.java
@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.

OrderStoreTest.java
@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