Reference
Harness API
The framework-agnostic module: bonsai-junit. Everything here works with or without Spring on the classpath.
Annotations
@EnableMongoEmulator runs the test class against the embedded store. @MongoSeed is repeatable and valid on a class or a method; @MongoSeeds is its container and is rarely written by hand.
package io.typecheck.bonsai.junit;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ExtendWith(MongoEmulatorExtension.class)
public @interface EnableMongoEmulator {
String[] databases() default {};
String[] collections() default {};
String defaultDatabase() default "";
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(MongoSeeds.class)
public @interface MongoSeed {
String[] value() default {};
String[] locations() default {};
String database() default "";
String collection() default "";
Class<? extends SeedPlaceholders>[] placeholders() default {};
Class<? extends SeedSource>[] sources() default {};
Class<? extends MultiSeedSource>[] multiSources() default {};
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MongoSeeds {
MongoSeed[] value();
}On @MongoSeed: value() and locations() are aliases, and declaring both is an error unless they are equal. An empty database() means the fixture's subdirectory name, or else the default database; an empty collection() means the filename without its extension. A dotted collection() names its database (first-dot split) when database() is empty; setting both fails fast. sources() (3.4.0+) names code fixtures instead of files: mutually exclusive with value()/locations(), applied in array order, and legal alongside placeholders(). multiSources() (3.4.0+) names multi-collection code fixtures; its map keys decide the targets, so it is mutually exclusive with collection(), database() and locations() as well.
MongoTestSupport
Static access to the embedded MongoDB emulation the extension started. This is the seam every other helper in this library reads through.
package io.typecheck.bonsai;
public final class MongoTestSupport {
public static final String DEFAULT_DATABASE = "test";
public static MongoClient client();
public static MongoDatabase database(String name);
public static MongoCollection<Document> collection(String qualifiedOrPlain);
public static boolean isActive();
public static List<DatabaseSnapshot> snapshot();
public static String qualify(String qualifiedOrPlain);
public static String databaseOf(String qualifiedOrPlain);
public static String collectionOf(String qualifiedOrPlain);
}database(...) materialises on first use; no declaration is required. collection(...) accepts either "orders" or "billing.invoices". The three name helpers expose the same resolution rule the rest of the library uses, which is occasionally handy in test utilities of your own. A blank name raises IllegalArgumentException.
snapshot() returns an inventory of every database the harness knows about: those declared on @EnableMongoEmulator, seeded, or touched through this library's APIs. Each collection carries its documents (ordered by _id, so you can assert on their contents, not just their number), a count, and the indexes in place. You can also confirm the index a query relies on exists. It is built from the harness's own record of touched databases, never a server-wide listDatabaseNames(), so in passthrough mode it reports your test's footprint and never a bystander application's data. The order is deterministic: default database first, then alphabetical; collections by name; documents by _id; indexes with _id_ first. Each record has a toDocument() for logging. A declared-but-unwritten database appears with an empty collection list. The whole inventory, documents included, is proven byte-identical to real MongoDB by a passthrough A/B test.
MongoTestData
Programmatic seeding: the arrange step. clear deletes documents and keeps the collection and its indexes; clearDatabase drops the database outright.
package io.typecheck.bonsai;
public final class MongoTestData {
public static void insert(String collection, Document... documents);
public static void insertAll(String collection, List<Document> documents);
public static void seed(String collection, String location);
public static void seed(String collection, String location, Map<String, Object> placeholders);
public static void seed(String database, String collection, String location);
public static void seed(String database, String collection, String location,
Map<String, Object> placeholders);
public static void seedFrom(String collection, SeedSource source);
public static void seedFrom(String database, String collection, SeedSource source);
public static void seedFrom(MultiSeedSource source);
public static void seedAll(String directoryLocation);
public static void seedAll(String directoryLocation, Map<String, Object> placeholders);
public static void clear(String collection);
public static void clearDatabase(String database);
}Locations take the forms classpath:mongo/seed/orders.json, file:/tmp/orders.json, or a plain path like src/test/resources/orders.json. The verb for a code fixture is seedFrom rather than another seed overload, because seed(...) already means “from a location” and seed("orders", null) would otherwise be ambiguous.
SeedSource and SeedContext
The code-fixture SPI (3.4.0+), in io.typecheck.bonsai.core.seed beside SeedPlaceholders. Referenced from @MongoSeed(sources = …) or passed to seedFrom; the narrative version is in Seeding & placeholders.
package io.typecheck.bonsai.core.seed;
@FunctionalInterface
public interface SeedSource {
List<?> documents(SeedContext context);
}
public record SeedContext(String database,
String collection,
SeedPhase phase,
boolean reapplied,
long randomSeed,
PlaceholderLookup placeholders) { }
public enum SeedPhase { SUITE, CLASS, METHOD, MANUAL }
public interface PlaceholderLookup {
Optional<Object> resolve(String spec);
}
public class SeedSourceException extends FixtureException { }SeedSource: a functional interface returning the documents to insert, in insertion order. Elements may beDocument,MaporBsonDocumentin either harness; entities only under Spring.SeedContext: an immutable record of the resolved target, the phase, and what the fixture needs to be reproducible.SeedPhase:SUITE,CLASS,METHOD,MANUAL; the last is what the static helpers report.reapplied()distinguishes a post-cleanup re-application.randomSeed(): deterministic per test class, method and collection, so a generated fixture reproduces. Override with thebonsai.seed.random-seedproperty to replay a run.PlaceholderLookup:resolve("customerId"),resolve("#now"),resolve("tenant:default")The same resolution chain a JSON fixture would use. A lookup rather than a map, because the chain is lazy and${#objectId}yields a fresh value per occurrence.SeedSourceException: aFixtureException, deliberately not aMongoException, so acatch (MongoException)around setup cannot swallow it. It names both the source class and the offending element index.
MultiSeedSource
MultiSeedSource (3.4.0+) is the multi-collection code fixture. One source that fills several collections with correlated ids. It returns a map keyed by collection name rather than a list, and its MultiSeedContext drops SeedContext's collection field (the keys name the targets) while keeping a single randomSeed() for the whole source. Referenced from @MongoSeed(multiSources = …) or passed to the no-target seedFrom(source); the narrative version is in Seeding & placeholders.
package io.typecheck.bonsai.core.seed;
@FunctionalInterface
public interface MultiSeedSource {
/** Keys are collection names: "orders", "billing.invoices". */
Map<String, ? extends List<?>> documents(MultiSeedContext context);
}
public record MultiSeedContext(String database, // default for plain (undotted) keys
SeedPhase phase,
boolean reapplied,
long randomSeed, // ONE seed for the whole source
PlaceholderLookup placeholders) { }- Keys follow the first-dot namespace rule:
ordersin the context's default database,billing.invoicesinbilling. Leading- and trailing-dot keys and the system databases (admin,config,local) are rejected by name. - Every collection is encoded before anything is written, but multi-collection seeding is not atomic across collections: ordered per collection, like
insertMany. - A
nullmap is an error (return an empty map for a deliberate no-op); anullkey or list names itself.
BonsaiRecordings
verify (3.4.0+) byte-compares the live embedded store against a record-and-replay artifact through the same canonical writer the recorder used, and reports the first differing namespace, _id and byte offset. It checks the manifest's completeness and versions, and refuses refused or unknown artifacts by name.
package io.typecheck.bonsai;
public final class BonsaiRecordings {
/** Byte-compares the live embedded store against a recording artifact. */
public static void verify(Path recordingDirectory);
}MongoCollectionReader
For getting data out of the store without going through the application.
package io.typecheck.bonsai;
public final class MongoCollectionReader {
public static List<Document> readDocuments(String qualifiedOrPlain);
public static List<Document> readDocuments(String qualifiedOrPlain, String filterJson);
public static Document readSingleDocument(String qualifiedOrPlain, String filterJson);
public static long count(String qualifiedOrPlain);
public static long count(String qualifiedOrPlain, String filterJson);
}readSingleDocument returns null when nothing matches and raises IllegalStateException when several do. The ambiguity is a test bug worth surfacing rather than a first-match to paper over.
MongoTestAwait
The third beat of seed, act, await, assert. A timeout raises AssertionError with a rendered table of the collection's actual contents; a negative Duration raises IllegalArgumentException.
package io.typecheck.bonsai;
public final class MongoTestAwait {
public static final int DEFAULT_TIMEOUT_SECONDS = 10;
public static final int DEFAULT_POLL_MILLIS = 50;
public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(DEFAULT_TIMEOUT_SECONDS);
public static final Duration DEFAULT_POLL_INTERVAL = Duration.ofMillis(DEFAULT_POLL_MILLIS);
public static void untilCount(String collection, long count);
public static void untilCount(String collection, long count, Duration timeout);
public static void untilExists(String collection, String filterJson);
public static void untilExists(String collection, String filterJson, Duration timeout);
public static void untilAbsent(String collection, String filterJson);
public static void untilAbsent(String collection, String filterJson, Duration timeout);
public static void until(BooleanSupplier condition, String description);
public static void until(BooleanSupplier condition, String description, Duration timeout);
}MongoTestDiagnostics
The failure-message renderer shared by the await helpers and the assertions. It is public because it is occasionally useful directly, dropping System.out.println(MongoTestDiagnostics.collectionState("orders")) into a test under investigation beats a debugger.
package io.typecheck.bonsai;
public final class MongoTestDiagnostics {
public static final int AWAIT_DOCUMENT_LIMIT = 20;
public static final int ASSERTION_DOCUMENT_LIMIT = 50;
public static final int VALUE_WIDTH = 96;
public static String collectionState(String qualifiedOrPlain);
public static String collectionState(String qualifiedOrPlain, String caption, int limit);
public static String documentTable(List<Document> documents, int limit);
}Infrastructure types
These are public for cross-package reasons rather than because you should call them. Listed for completeness.
package io.typecheck.bonsai;
/** Infrastructure. Test code never touches this class. */
public final class MongoTestContext implements AutoCloseable {
public static MongoTestContext open(String name);
public static MongoTestContext open(String name, String defaultDatabase);
public static MongoTestContext current();
public static boolean isActive();
public MongoClient client();
public String contextId();
public String defaultDatabase();
public void setDefaultDatabase(String defaultDatabase);
public void bindToCurrentThread();
public void dropDatabases();
@Override public void close();
}
/** Registered by the annotation, so tests never name this class. */
public final class MongoEmulatorExtension
implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback {
public void beforeAll(ExtensionContext extensionContext);
public void beforeEach(ExtensionContext extensionContext);
public void afterEach(ExtensionContext extensionContext);
public static void rejectConcurrentMethods(ExtensionContext extensionContext);
}rejectConcurrentMethods is the shared detector both harnesses call, so a rejected parallel configuration produces the same error message whichever annotation you used. See Parallel execution.