Vert.x Testing — Mock Services with Mockito

Testing specific logic in a microservice can be difficult due to dependencies on other services. You may start out thinking it will be simple to test some function, then realize that you need to add a dependency. Then that dependency leads to others.

Mocking services can reduce the number of dependencies required to complete you test. That helps you focus your test on the code that you wrote. It also makes it much easier to regression test in your CI/CD pipeline.

The Mockito Framework can help us easily mock objects and services in our Vert.x tests.

Create Maven POM

Let’s start out by modifying a Maven POM to support JUnit testing with Mockito. 

Here at the Lab, we like to mention the versions of imported libraries in a properties section and also use a POM import get all of the Vert.x dependencies like below.

<project>
    ...
    <properties>
        <vertx.version>3.6.3</vertx.version>
        <junit.version>4.12</junit.version>
        <mockito.version>2.24.5</mockito.version>
        <java.version>1.8</java.version>
    </properties>

    <!-- Import the vert.x dependencies BOM -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.vertx</groupId>
                <artifactId>vertx-dependencies</artifactId>
                <version>${vertx.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    ...
</project>

Dependencies

Now let’s add the dependencies will need for this example.

<project>
    ...
    <properties>
        <vertx.version>3.6.3</vertx.version>
        <junit.version>4.12</junit.version>
        <mockito.version>2.24.5</mockito.version>
        <java.version>1.8</java.version>
    </properties>
    ...
    <dependencies>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-core</artifactId>
        </dependency>

        <!-- The following two dependencies enable the generation of service proxies -->
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-service-proxy</artifactId>
        </dependency>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-codegen</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- The following dependencies enable vert.x junit and testing with mockito -->
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-unit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>${mockito.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    ...
</project>

We need vertx-core to include the basis Vert.x library support. There is no need to mention the version here because we imported the dependencies pom previously.

The vertx-service-proxy is needed as we want to generate service proxies to mock in this example.

To generate the code needed for the service proxy we need to mention vertx-codegen. Its scope is provided as we only need this dependency at build time. It is not needed at run time and isn’t included in the jar.

JUnit testing is supported by including vertx-unit. We’ll also need to include junit.

Finally, we will include the mockito-core dependency so we can mock our Vert.x services.

Create Simple Service to Mock

Let’s create a very simple service that we can use to test mocking a regular service as well as a proxy to that service.

package com.diabolicallabs.example;

import io.vertx.codegen.annotations.GenIgnore;
import io.vertx.codegen.annotations.ProxyGen;
import io.vertx.codegen.annotations.VertxGen;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;

@ProxyGen
@VertxGen
public interface Service {

  /**
   * This is the default address that the service proxy will 
   * listen to for events on the Event Bus
   */
  String DEFAULT_ADDRESS = "service.example";

  /**
   * Creates a service instance
   * @return An instance of the default service implementation
   */
  @GenIgnore
  static Service create() {
    return new ServiceImpl();
  }

  /**
   * Creates a proxy for the service that listens for events on the Event Bus
   * @param vertx An instance of Vertx
   * @param address This is the address the service will listen to on the Event Bus
   * @return An instance fo the service proxy
   */
  @GenIgnore
  static Service createProxy(Vertx vertx, String address) {
    return new ServiceVertxEBProxy(vertx, address);
  }

  /**
   * Repeats the text passed by the caller
   * @param text The text to repeat
   * @param handler This handler will be called with a copy of the text passed
   */
  void repeat(String text, Handler<AsyncResult<String>> handler);

}

The only function of the service is to repeat the passed text back to the caller. The result will be returned asynchronously using an AsyncResult.

The service definition also has a couple of helper methods to create a default instance of the service as well as a proxy to that service.

Service Implementation

package com.diabolicallabs.example;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;

public class ServiceImpl implements Service {

  @Override
  public void repeat(String text, Handler<AsyncResult<String>> handler) {
    handler.handle(Future.succeededFuture(text));
  }

}

The default service implementation merely returns the text passed to it. Later we will override this behavior with a mock service to prove that Mockito is working.

 

Create Verticle to call the Service

Next we will create a Verticle to call the service and the proxy to the service. The services defined here are the ones that we will mock in the test. In real life, mocked services will probably be ones that you added through dependencies that you may not have control over.

package com.diabolicallabs.example;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.serviceproxy.ServiceBinder;

public class Verticle extends AbstractVerticle {

  private Service exampleService;       //Default implementation of Service
  private Service exampleServiceProxy;  //A proxy to the default implementation of Service

  @Override
  public void init(Vertx vertx, Context context) {

    //Create an implementation of Service
    exampleService = Service.create();
    //Create a proxy to an implementation of Service
    exampleServiceProxy = Service.createProxy(vertx, Service.DEFAULT_ADDRESS);

    super.init(vertx, context);

  }

  @Override
  public void start(Future<Void> startFuture) throws Exception {

    //Bind the default implementation of Service to the Event Bus at the default address
    new ServiceBinder(vertx)
      .setAddress(Service.DEFAULT_ADDRESS)
      .register(Service.class, new ServiceImpl());

    //Create a consumer that will call the repeat method of the service and reply with the result
    //This allows us to demonstrate how a service can be mocked during testing
    vertx.eventBus().<String>consumer("example.service.repeat", handler -> {
      String text = handler.body();
      exampleService.repeat(text, stringAsyncResult -> {
        if (stringAsyncResult.succeeded()) {
          handler.reply(stringAsyncResult.result());
        } else {
          handler.fail(-1, stringAsyncResult.cause().getMessage());
        }
      });
    });

    //Create a consumer that will call the repeat method of the service proxy and reply with the result
    //This allows us to demonstrate how a proxy to a service can be mocked during testing
    vertx.eventBus().<String>consumer("example.service.proxy.repeat", handler -> {
      String text = handler.body();
      exampleServiceProxy.repeat(text, stringAsyncResult -> {
        if (stringAsyncResult.succeeded()) {
          handler.reply(stringAsyncResult.result());
        } else {
          handler.fail(-1, stringAsyncResult.cause().getMessage());
        }
      });
    });

    startFuture.complete();
  }

}

On lines 11 and 12 we define private variables to hold an instance of the service and a proxy to the service. Take note of the variable names here as they will have to match the names for our mocks in the test. These instances are created in the init method on lines 18 and 20. Vert.x will call the init method when the Verticle is deployed. 

It is important to note that Mockito will inject the mock instance of the service when the service is instantiated. Unfortunately, Vert.x will then call the init method which will then overwrite the variables with the actual service and proxy. So, the mock will be lost. We shall account for this in the test definition.

The proxy to the service will be bound to an address on the EventBus starting on line 30.

We will then create two EventBus consumers that will listen at example.service.repeat and  example.service.proxy.repeat. They will call call the services repeat method and then reply with the result.

Create Service Test

First let’s create a test for calling the service without using a proxy. The test class is the same as a regular Vert.x JUnit test with a couple of additions for Mockito.

@RunWith(io.vertx.ext.unit.junit.VertxUnitRunner.class)
public class ServiceTest {

  @Mock
  private Service exampleService;

  @InjectMocks
  Verticle exampleVerticle;

  @Rule
  public RunTestOnContext rule = new RunTestOnContext();

  @Rule
  public MockitoRule mockitoRule = MockitoJUnit.rule();
  
  ...
}

Although Mockito has it’s own @RunWith Runner class, it won’t work in combination with the Vert.x Runner. So, we’ll use the VertxUnitRunner and add a @Rule for Mockito which will provide the same functions as the Runner.

The @Mock annotation is for the exampleService that is mentioned in the Verticle. The variable name here in the test class needs to match the variable name in the class you are going to inject the mock into. In this case we are injecting the mock into the Verticle. It doesn’t have to be in a Verticle though. You can mock most any class in your solution.

The @InjectMocks will inject the mock mentioned above into the example Verticle.

The next two @Rule rules are needed for Vert.x and Mockito.

Now we need to create a before method to deploy our Verticle and set up how we want the mock to behave during testing.

@Before
public void before(TestContext context) {

  Async async = context.async();

  rule.vertx().deployVerticle(exampleVerticle, stringAsyncResult -> {

    /*
     * Mockito will inject the mocks into the Verticle when it is created. Unfortunately,
     * Vert.x will call the init and start methods after that when the Verticle is
     * deployed. So, we need to re-set the services back to the mocks after deployment
     * with reflection.
     */
    try {
      Field exampleServiceProxyField = Verticle.class.getDeclaredField("exampleService");
      exampleServiceProxyField.setAccessible(true);
      exampleServiceProxyField.set(exampleVerticle, exampleService);
    } catch (NoSuchFieldException | IllegalAccessException e) {
      throw new RuntimeException(e);
    }

    Mockito.doAnswer((Answer<Void>) invocation -> {
      String parameter = invocation.getArgument(0);
      Handler<AsyncResult<String>> handler = (Handler<AsyncResult<String>>) invocation.getArguments()[1];

      System.out.println("Mocked 'repeat' method received parameter: " + parameter);

      switch (parameter) {
        case "goat":
          handler.handle(Future.succeededFuture("kao"));
          break;
        case "eel":
          handler.handle(Future.succeededFuture("puhi"));
          break;
        default:
          handler.handle(Future.failedFuture(new RuntimeException(parameter)));
      }
      return null;
    }).when(exampleService).repeat(Mockito.any(String.class), Mockito.any(Handler.class));

    async.complete();
  });

}

Since we are going to deploy the Verticle asynchronously, we need to get an instance of Async.

Next we will deploy the example Verticle that was defined with the @InjectMocks annotation.

As mentioned before, Mockito will inject the mocks just as soon as the Verticle instance is created. After that, Vert.x will call the init() and start() methods which will probably cause the mocks to be overwritten with the real services.

We can set the mocks back by using reflection to set the variables in the Verticle instance. To do this we will allow private access to variables and then set the variable in the Verticle to the mocks defined in the test class.

Now we’re ready to set the behavior of the mocked service. We’ll follow the format mentioned below.

Mockito.doAnswer((Answer<Something>) invocation -> {
  return new Something();
}).when(mockedService).methodName(...parameter definitions...);

The .doAnswer() lambda will be called when the methodName() is called on the mocked object with the defined number of parameters. The lambda will have an invocation instance that will have the actual parameters passed in the method call. The .doAnswer() method will return an Answer of a generic type. 

Let’s have another look at our service and mocked behavior.

Here is the repeat() method we want to mock.

void repeat(String text, Handler<AsyncResult<String>> handler);

.. and here is our mocked behavior. 

Mockito.doAnswer((Answer<Void>) invocation -> {
  String parameter = invocation.getArgument(0);
  Handler<AsyncResult<String>> handler = (Handler<AsyncResult<String>>) invocation.getArguments()[1];

  System.out.println("Mocked 'repeat' method received parameter: " + parameter);

  switch (parameter) {
    case "goat":
      handler.handle(Future.succeededFuture("kao"));
      break;
    case "eel":
      handler.handle(Future.succeededFuture("puhi"));
      break;
    default:
      handler.handle(Future.failedFuture(new RuntimeException(parameter)));
  }
  return null;
}).when(exampleService).repeat(Mockito.any(String.class), Mockito.any(Handler.class));

The last line is defining that this block of code will execute when repeat() is called with a String and a Handler. That matches the definition of repeat() mentioned just above. The second parameter is a Handler which we call call in the invocation block to provide the response just like in the actual service implementation.

The block at the top, provides the Answer. In this case the repeat method returns void, so we defined the Answer<Void> type on the invocation.

Inside the block we will get the first two parameters and save them. The first one is the text being passed in and the second is the handler we call call with the answer.

Then there is a switch statement which will translate a couple of words to Hawaiian. The non-mocked service just returns the passed text.

At the end we will return null. In a synchronous service, this is where we would provide the answer rather than calling the handler.

Create a Test Case

@Test
public void testMockedServiceMethodWithGoat(TestContext context) {

  Async async = context.async();
  rule.vertx().eventBus().send("example.service.repeat", "goat", result -> {
    context.assertTrue(result.succeeded());
    //The normal service just repeats the parameter back as a reply. The mocked service
    //will return the parameter translated to Hawaiian. If we get back Hawaiian, then
    //the mocked service was called.
    context.assertEquals("kao", result.result().body());
    async.complete();
  });
}

Here we are sending the value “goat” over the EventBus to the example.service.repeat address. The consumer in our Verticle will receive that and call the repeat() method of our service. If the mocked service is working, we should get “kao” back rather than “goat”.

References

Example on GitHub

Vert.x Documentation

Mockito Documentation