Skip to content

NoEnv/vertx-wiremongo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status Code Coverage Maven Central

Vert.x-Wiremongo

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):

<dependency>
  <groupId>com.noenv</groupId>
  <artifactId>vertx-wiremongo</artifactId>
  <version>4.5.8</version>
</dependency>
  • Gradle (in your build.gradle file):

compile 'com.noenv:vertx-wiremongo:4.5.8'

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:

public class FruitDatabase {

  private final MongoClient mongo;

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

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

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

  private Future<String> insertFruit(String type, int mass, Instant expiration) {
    var p = Promise.<String>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<Long> countFruitByType(String type) { }
  public Future<Long> 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:

@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:

@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:

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:

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:

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:

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:

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:

@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:

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:

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:

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:

@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:

{
  "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:

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:

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.