Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/noenv/vertx-wiremongo

Lightweight mongo mocking for Vert.x
https://github.com/noenv/vertx-wiremongo

integration-testing mock mongodb mongomock test vertx vertx-mongo-client

Last synced: about 1 month ago
JSON representation

Lightweight mongo mocking for Vert.x

Awesome Lists containing this project

README

        

image:https://github.com/NoEnv/vertx-wiremongo/actions/workflows/ci.yml/badge.svg["Build Status",link="https://github.com/NoEnv/vertx-wiremongo/actions/workflows/ci.yml"]
image:https://codecov.io/gh/NoEnv/vertx-wiremongo/branch/main/graph/badge.svg["Code Coverage",link="https://codecov.io/gh/NoEnv/vertx-wiremongo"]
image:https://badgen.net/maven/v/maven-central/com.noenv/vertx-wiremongo["Maven Central",link="https://search.maven.org/artifact/com.noenv/vertx-wiremongo"]

= Vert.x-Wiremongo
:toc: left

Wiremongo is an alternative implementation of the Vert.x `MongoClient` interface that can be used to mock mongo calls. It is most useful for unit testing mongo database code and is more lightweight than running a mongodb instance.

== Quickstart

To use Vert.x Wiremongo, add the following dependency:

* Maven (in your `pom.xml`):

[source,xml,subs="+attributes"]
----

com.noenv
vertx-wiremongo
4.5.11

----

* Gradle (in your `build.gradle` file):

[source,groovy,subs="+attributes"]
----
compile 'com.noenv:vertx-wiremongo:4.5.11'
----

Imagine you have a mongodb to keep track of your fruit. In your application you have a class that uses `io.vertx.ext.mongo.MongoClient` for the database operations:

[source,java]
----
public class FruitDatabase {

private final MongoClient mongo;

public FruitDatabase(MongoClient mongo) {
this.mongo = mongo;
}

public Future addApple(int mass, Instant expiration) {
return insertFruit("apple", mass, expiration).mapEmpty();
}

public Future addBanana(int mass, Instant expiration) {
return insertFruit("banana", mass, expiration).mapEmpty();
}

private Future insertFruit(String type, int mass, Instant expiration) {
var p = Promise.promise();
mongo.insert("fruits", new JsonObject()
.put("type", type)
.put("mass", mass)
.put("expiration", new JsonObject().put("$date", expiration)), p);
return p.future();
}

public Future countFruitByType(String type) { }
public Future removeExpiredFruit() { }
// etc.
}
----

To test this class, simply create an instance of `WireMongo`, set up some mocking using its fluent methods, and use it as the `MongoClient` implementation in your tests:

[source,java]
----
@RunWith(VertxUnitRunner.class)
public class FruitDatabaseTest {

private WireMongo wiremongo;
private FruitDatabase db;

@Before
public void setUp() {
wiremongo = new WireMongo();
db = new FruitDatabase(wiremongo.getClient());
}

@Test
public void testAddApple(TestContext ctx) {
var expiration = Instant.now().plus(5, ChronoUnit.DAYS);

wiremongo.insert()
.inCollection("fruits")
.withDocument(new JsonObject()
.put("type", "apple")
.put("mass", 161)
.put("expiration", new JsonObject().put("$date", expiration)))
.returnsObjectId();

db.addApple(161, expiration)
.onComplete(ctx.asyncAssertSuccess());
}
}
----

You can also test error cases:

[source,java]
----
@Test
public void testInsertError(TestContext ctx) {
wiremongo.insert()
.returnsDuplicateKeyError();

db.addApple(123, Instant.now().plus(3, ChronoUnit.DAYS))
.onComplete(ctx.asyncAssertFailure());
}
----

_You can find the complete source code of these examples in `src/test/java/com/noenv/wiremongo/examples`._

== Matching

When setting up a mock, all the matching criteria can also be defined by *custom matchers*. The following mock would match all `update` commands that try to update the expiration of bananas:

[source,java]
----
wiremongo.updateCollection()
.inCollection("fruits")
.withQuery(q -> q.getString("type").equals("banana"))
.withUpdate(u -> u.getJsonObject("$set").containsKey("expiration"))
.returnsTimeoutException();
----

If you don't specify a custom matcher, `Objects.equals` is used by default. Since a lot of interaction with mongo happens using `JsonObject` and `JsonArray`, there is also a `JsonMatcher` that can be used like this:

[source,java]
----
import static com.noenv.wiremongo.matching.JsonMatcher.equalToJson;

wiremongo.insert()
.inCollection("fruits")
.withDocument(equalToJson(new JsonObject().put("type", "banana"), /*ignoreExtraElements*/ true))
.returns("2ad7533f");
----

The above example matches all commands that try to insert *bananas* into *fruits*. Note that the json matcher supports a flag `ignoreExtraElements` that allows these insert documents to be matched even if they contain additional fields (e.g. mass & expiration).

=== Priority

If several mocks are set up for the same command and matching criteria, WireMongo will use the mock with the highest priority. The default behaviour is to give mocks an increasing priority as they are added so the most recently added always has the highest priority:

[source,java]
----
wiremongo.count().inCollection("fruits").returns(21L);
wiremongo.count().inCollection("fruits").returns(42L);

// a call to mongo.count("fruits") will return 42
----

However, priorities can be user-defined:

[source,java]
----
wiremongo.count().inCollection("fruits").priority(13).returns(21L);
wiremongo.count().inCollection("fruits").priority(11).returns(42L);

// a call to mongo.count("fruits") will return 21
----

== Stubs

Stubs are the *response* part of the mock, i.e. they define how the mock *responds* to commands that match. The most low-level stubs are *custom stubs*:

[source,java]
----
wiremongo.findOne()
.inCollection("fruits")
.stub(c -> new JsonObject()
.put("type", "apple")
.put("mass", 123)
.put("expiration", new JsonObject().put("$date", Instant.now())));
----

Sometimes it may be useful to assert that the application actually invokes the expected mongo command:

[source,java]
----
@Test
public void testInsert(TestContext ctx) {
Async async = ctx.async();
wiremongo.insert()
.stub(c -> {
async.countDown();
return "37bd238fa";
});

application.addApple(); // adding an apple should trigger an insert command
}
----

The `returns("1234")` method is just a more convenient way for `stub(c -> "1234")`.

Stubs can also throw exceptions:

[source,java]
----
wiremongo.count()
.stub(c -> { throw new MongoTimeoutException("intentional"); });
----

For the most common errors, wiremongo contains helper methods that match the types and messages of an actual mongo instance (`returnsDuplicateKeyError`, `returnsTimeoutException`, `returnsConnectionException`).

Multiple stubs can be configured for a mock. The stubs are used once each in the order they are added, the last one is used forever. Consider the following mock:

[source,java]
----
wiremongo.insert()
.returns("37bd238fa")
.returns("73ab6cf21")
.returnsDuplicateKeyError();
----

The above code will return ids for the first two and a duplicate key error for every subsequent insert command.

== Match All

If you want to add a mapping that matches *all* mongo commands, you can use `matchAll`:

[source,java]
----
wiremongo.matchAll()
.stub(c -> {
ctx.assertTrue(c.method().equals("replaceDocuments") || c.method().equals("insert"));
log("mongo received command: " + c);
return 42;
});
----

Match All is not supported for file mappings however.

== Files

Mocks can also be defined in json files. You can ask wiremongo to read files from a directory like this:

[source,java]
----
@Before
public void setUp(TestContext ctx) {
wiremongo = new WireMongo(vertx);
wiremongo.readFileMappings("test/resources/wiremongo-files")
.onComplete(ctx.asyncAssertSuccess());
}
----

The wiremongo json files look like this:

[source,json]
----
{
"method": "insert",
"collection": {
"equalTo": "fruits"
},
"document": {
"equalToJson": {
"type": "banana",
"mass": 7533
},
"ignoreExtraElements": true
},
"response": "388adf7ab"
}
----

The details depend on the command that is mocked. To get started, it is easiest to just look at the json file for the command you want to mock in the `src/test/resources/wiremongo-mocks` folder of this project.

== Verifications

Very often it is not only important to have mocks for a database ready, but also to make sure those are used or even used properly. Verifications let you check if a call to database is made at all, or made for specific times or even never made.

Basic setup for verification is to have a `Verifier` and make sure it is reset before each test and all its verifications are asserted after each test. For example using JUnit:

[source,java]
----
public class SomeTestClass {

private Verifier verifier;

@Before
public void setUpTest() {
verifier = new Verifier();
}

@After
public void tearDownTest() {
verifier.assertAllSucceeded();
}

// your tests go here
}
----

Then each mock can define a verification when it is set up. For example:

[source,java]
----
public class SomeTestClass {
// ...
@Test
public void verify_RunExactlyOnce_shall_fail_ifRunTwice(TestContext ctx) {
// ...

mock
.findOneAndUpdate()
.inCollection("some-collection")
.verify(
verifier
.checkIf("find one and update in some-collection")
.isRunExactlyOnce()
)
.returns(null);

// ...
}
}
----

The requirements defined will be checked for in the `@After` annotated method.