JUnit Testing

Introduction

This tutorial shows how to test a simple Baratine service with JUnit.

  • Testing an ephemeral service (echo) with no dependencies.
  • Testing a persistent resource (counter)
  • Testing dependencies (mocking and database loading)

Echo @Service

Since Baratine services are designed to be isolated from each other, they naturally fit into independent unit tests. When possible, Baratine services should be designed as leaf services with few dependencies, which makes them easier to test.

The JUnit tests can focus on the sequential logic for the service because Baratine’s ensures in-order message queues. If your application has a buggy or unusual timing sequence, the JUnit tests can replicate it precisely.

To start you will need your Baratine implementation class (annotated with @Service) and a matching interface for the service proxy. Let’s start with an echo service.

The service below echos its method argument:

package example;

import io.baratine.core.Service;

/**
 * Implementation of the echo service.
 */
@Service("/echo")
public class EchoServiceBean
{
  public String echo(String message)
  {
    return message;
  }
}

This example is an internal service bound to the “/echo” address. The JUnit test will lookup the service at “/echo”, the same as when the service is used. The service is internal because the address is “/echo”, not “public:///echo”.

The echo interface will become a client proxy to the service. For test convenience, the interface includes blocking versions of the method calls. The blocking methods are also useful for calling the service from a blocking environment. The non-blocking version is used by other Baratine services:

package example;

/**
 * Proxy interface to the echo service.
 */
public interface EchoService
{
  // non-blocking operational methods to be called from other services
  void echo(String message, Result<String> result);

  // blocking and QA methods
  String echo(String message);
}

The JUnit test for EchoServiceBean creates a Baratine environment as if the echo service was deployed as in production. The test class select services to be tested; any others are not deployed. The proxy is looked up with injection using its deployed URL for convenience. (It’s possible to programmatically lookup the proxy from the ServiceManager.)

The JUnit test for EchoService might look like the following:

package example;

import echo.EchoService;
import echo.EchoServiceBean;
import com.caucho.baratine.junit.BaratineRunner;
import com.caucho.baratine.junit.ConfigurationBaratine;
import io.baratine.core.Lookup;
import org.junit.Assert;
import org.junit.Test;

@RunWith(BaratineRunner.class)
@ConfigurationBaratine(services=EchoServiceBean.class)
public class EchoTestLocal
{
  @Lookup("/echo")
  EchoService _echoService;

  @Test
  public void test()
  {
    String message = "Hello World!";

    //make a call to the service
    String result = _echoService.echo(message);

    Assert.assertEquals(message, result);
  }
}

Testing Async Calls

While the echo service does not need to test its proxy’s async methods because the service implementation is synchronous, some services do need to test their async calls. For example, a routing services might hold a result waiting for an endpoint to complete, but continue to process new requests.

@Test
public void test()
{
  String message = "Hello World!";

  Future<String> refA = new CompletableFuture<>();
  Future<String> refB = new CompletableFuture<>();

  _echoService.echo("Hello-A", x->refA.set(x));
  _echoService.echo("Hello-B", x->refB.set(x));

  Assert.assertEquals("Hello-A", refA.get(1, TimeUnit.SECONDS));
  Assert.assertEquals("Hello-B", refB.get(1, TimeUnit.SECONDS));
}

Or you can use Baratine’s ServiceFuture, which is a future that integrates tightly with Baratine services:

@Test
public void test()
{
  String message = "Hello World!";

  ServiceFuture<String> refA = new ServiceFuture<>();
  ServiceFuture<String> refB = new ServiceFuture<>();

  _echoService.echo("Hello-A", refA);
  _echoService.echo("Hello-B", refA);

  Assert.assertEquals("Hello-A", refA.get(1, TimeUnit.SECONDS));
  Assert.assertEquals("Hello-B", refB.get(1, TimeUnit.SECONDS));
}

It’s only necessary to test the async proxy methods if the service implementation can return results out of order. For example, a routing service or managing service will often start requests to an end-service without blocking on the results. On the other hand, services that always return results in order, particularly leaf services, often do not need redundant testing of the async proxies.

Multiple Services and Mocking

When a service calls other services, it can be convenient to test against a simpler mock dependency. Your test can register the mock service at the same URL as the real service by using Baratine’s ServiceManager in its JUnit runner.

For example, if we have a PingService that is a management service which pings child devices for their status, the child device services can be replaced with mock services that simulate live, dead or stuck devices.

Since services lookup other services with URLs, a mock service is configured by binding it to the expected URL, in the example “/deviceA” and “/deviceB”. The JUnit test uses the JUnit @Before annotation to configure the URLs before running the test.

PingService is the service we’re testing. Its dependent device services are DeviceService. PingService will need to test both the blocking and async proxy methods because it can return result out-of-order, not blocking when a device is slow. The single method pings a named device and returns the device’s response. If the device throws and exception, it will be rethrown:

public interface PingService {
  void ping(String name, Result<Boolean> result);

  boolean ping(String name);
}

The child device interface is a single async call to return its status. “ok” for a valid device and “fail” for a failed device. It’s also possible for an exception to occur:

public interface DeviceService {
  // async operational method
  void getStatus(Result<String> result);

  // QA method
  String getStatus();
}

The ping implementation queries the named device and returns a boolean response. Because the device returns a string status, PingService needs to do post-processing to determine if the result is valid. Device exceptions are returned to the caller. Both the post-processing and the exception chaining take advantage of the``Result.wrap`` convenience method:

@Service("/ping")
public class PingServiceBean {
  @Lookup("/deviceA")
  DeviceService _deviceA;

  @Lookup("/deviceB")
  DeviceService _deviceB;

  @Lookup("/deviceSlow")
  DeviceService _deviceSlow;

  public void ping(String name, Result<Boolean> result)
  {
    getDevice(name).getStatus(result.wrap((x,r)->validate(x, r)));
  }

  private void validate(String value, Result<Boolean> result)
  {
    result.completed("ok".equals(value));
  }

  private DeviceService getDevice(String name)
  {
    if ("A".equals(name)) {
      return _deviceA;
    }
    else if ("B".equals(name)) {
      return _deviceB;
    }
    else if ("S".equals(name)) {
      return _deviceSlow;
    }
    else {
      throw new IllegalArgumentException();
    }
  }
}

The ping JUnit test needs an extra step from the echo test, because it needs to register the mock services. The mock registration occurs in the JUnit @Before method, and uses the injected Baratine ServiceManager to register the mocks.

The test checks that device-A passes the ping, device-B fails, and the unknown device-C throws an exception:

package foo;

import com.caucho.baratine.Baratine;
import echo.EchoService;
import echo.EchoServiceBean;
import com.caucho.baratine.junit.BaratineRunner;
import com.caucho.baratine.junit.ConfigurationBaratine;
import io.baratine.core.Lookup;
import io.baratine.core.ServiceManager;
import org.junit.Assert;

@RunWith(BaratineRunner.class)
@ConfigurationBaratine(services=PingServiceBean.class)
public class PingTestLocal
{
  @Lookup("/ping")
  PingService _ping;

  @Inject
  ServiceManager _manager;

  @org.junit.Before
  public void before()
  {
    // deviceA gets an always-"ok" mock
    _manager.service(new MockDeviceBean("ok"))
            .bind("/deviceA");

    // serviceB gets an always-"fail" mock
    _manager.service(new MockDeviceBean("fail"))
            .bind("/deviceB");
  }

  @org.junit.Test
  public void test()
  {
    Assert.assertTrue(_ping.ping("A"));
    Assert.assertFalse(_ping.ping("B"));

    try {
      _ping.ping("C");
      Assert.assertFalse(true);
    } catch (Exception e) {
      Assert.assertException(IllegalArgumentException.class, e);
    }
  }
}

Out-of-Order Testing Revisited

PingService can return its results out of order if a device is slow. Fast devices return results quickly without getting stuck for the slow devices. For example, a monitored device is down or slow, PingService should return fast measurements on the devices that are active, and eventually return results for the slow device. Since out-of-order processing is a feature of PingService, it needs testing.

To test PingService‘s out-of-order handling, the JUnit test includes a mock device that delays its results for a second. The test uses the async proxy call and a ServiceFuture to verify the expected out-of-order response.:

@org.junit.Before
public void before()
{
  // fast deviceA returns quickly
  _manager.service(new MockDeviceBean("ok"))
          .bind("/deviceA");

  // slow serviceS sleeps for 1 second before responding
  _manager.service(new MockDeviceBean("ok", 1, TimeUnit.SECONDS))
          .bind("/deviceSlow");
}

/**
 * out-of-order results: when serviceS is delayed, serviceA's ping returns
 * first, even if the serviceS ping is called before it.
 */
@org.junit.Test
public void testOutOfOrder()
{
  ServiceFuture<Boolean> futureA = new ServiceFuture<>();
  ServiceFuture<Boolean> futureS = new ServiceFuture<>();

  _ping.ping("S", futureS);
  _ping.ping("A", futureA);

  // wait for A's response, which will be quick
  Assert.assertTrue(futureA.get(10, TimeUnit.SECONDS));

  // verify that S's response is still pending
  Assert.assertFalse(futureS.peek())

  // verify that S's response is eventually returned
  Assert.assertTrue(futureS.get(10, TimeUnit.SECONDS));
}

Timer Testing

If a service uses Baratine’s “timer:” service for long-lived timeouts, the test runner can control an artificial time to validate timeouts.

The time control is in the RunnerBaratine object itself and can be injected into the test.

package example;

import com.caucho.baratine.Baratine;
import echo.EchoService;
import echo.EchoServiceBean;
import com.caucho.baratine.junit.BaratineRunner;
import com.caucho.baratine.junit.ConfigurationBaratine;
import io.baratine.core.Lookup;
import io.baratine.core.ServiceManager;
import org.junit.Assert;

@RunWith(BaratineRunner.class)
@ConfigurationBaratine(services=TimeServiceBean.class,time=0)
public class PingTestLocal
{
  @Lookup("/my-time")
  TimeService _time;

  @Inject
  RunnerBaratine _testContext;

  @org.junit.Test
  public void test()
  {
    _time.registerTimeout(10, TimeUnit.MINUTES);

    Assert.assertFalse(_time.isTimeout());

    _testContext.addTime(9, TimeUnit.MINUTES);

    Assert.assertFalse(_time.isTimeout());

    _testContext.addTime(2, TimeUnit.MINUTES);

    Assert.assertTrue(_time.isTimeout());
  }
}

@BeforeBatch and @AfterBatch

Baratine’s performance is partly based on executing batches of service calls in one Thread. Before the batch is processed Service might want to prepare to do the work, e.g. set variables of refresh caches. Annotating method with @BeforeBatch nominates method for invocation before Baratine starts work on batch. Batch may consist of one or more invocation of the service’s method:

package qa;

import io.baratine.core.Service;
import io.baratine.core.AfterBatch;
import io.baratine.core.BeforeBatch;

@Service("public:///echo")
public class EchoServiceBean
{
  private static ThreadLocal _xLocal = new ThreadLocal();

  @BeforeBatch
  public void beforeBatch()
  {
    _xLocal.set(" World!");
  }

  public String echo(String message)
  {
    return message + _x.get();
  }

  @AfterBatch
  public void afterBatch()
  {
    _xLocal.set(null);
  }
}

A method annotated with @AfterBatch is called after batch completion. It can be used to clear the variables or reset the caches.

The JUnit tests must assume that methods @BeforeBatch and @AfterBatch will be invoked automatically by Baratine container.

Example Code on GitHub

Examples on testing various Baratine services with JUnit can be found at a link below: “https://github.com/baratine/baratine-examples”:https://github.com/baratine/baratine-examples