In our example, an application has unmanaged out-of-process dependency represented by a Service interface which accepts a functional interface as one of its arguments. Our goal is to unit test such application and verify communication to the out-of-process dependency including data passed to the functional interface or received from it.
In context of this article, the unit test is meant to be a test which tests one unit of behaviour as defined by classical (Detroit) school of unit testing. More about the topic is in the book Unit Testing Principles, Practices, and Patterns by Vladimir Khorikov.
-
Unmanaged out-of-process dependency is an application's dependency on external system. Unmanaged means that it is not part of the application, or we don't have full control over it. For example, it can be a database to which multiple systems has access, or a mail server, etc.
Because this type of dependency is observable by application's users (user can be an external application), it should be mocked in tests and interactions with these mocks should be verified. Tests, testing unmanaged out-of-process dependencies are called integration tests.
-
Managed out-of-process dependency is a dependency on external system, which is part of our application. This is usually a database to which an application stores its data. Such a database and its mode is part of the application and should be tested by unit tests together with the application itself. This kind of dependency should not be mocked, because data-model of the database is practically part of the application.
-
In-process dependency is the internal dependency, mostly dependency between various classes in the application. Dependency of this type should not be tested as well, because calls of the classes are part of the application's behaviour.
To create a mock of the Service, we will use the Mockito framework in version 3.5 together with JUnit 5. Similar approach will work with older versions of these libraries as well.
-
Stub is fake implementation of a class used in tests. Stub is not used to verify behaviour of the "stubbed" interface or class, or interaction between it and its users, rather to provide sensible inputs to those users, so tests can run.
-
Mock is also fake implementation of a class used to observe and verify interactions between the mock and its users, usually by verifying calls on methods of the mocked class.
-
Spy is a subtype of mock which is implemented in the test not generated by a mocking library (Mockito), sometimes called "hand-written mock". Spies often bring more flexibility and better readability into integration tests.
Example application
Service
In the example, Service is an interface representing some out-of-process dependency. To demonstrate different situations it has four methods.
-
consume()
to demonstrate work with theConsumer
interface accepting an input, which we will provide by the test method, and then we will check what actually happened to that input. -
supply()
to demonstrate work with theSupplier
interface, returning an output which we will check in the test. -
execute()
to demonstrate work with theFunction
interface, which actually combines previous two methods. -
process()
is more complicated demonstration of theFunction
interface. Theprocess()
method passes input to the function and then returns output from that function as its result.
public interface Service {
void consume(Consumer<Input> consumer);
void supply(Supplier<Output> supplier);
void execute(Function<Input, Output> function);
Output process(Input input, Function<Input, Output> function);
}
Client
Client is the user of the Service interface. Each method of the client's API represents one unit of behaviour, so each should have at least one test method. Each client's method calls a method of the Service interface with the same name.
public class Client {
private final Service service;
public Client(Service service) {
this.service = service;
}
public void consume() {
service.consume(input -> {
input.doSomething();
});
}
public void supply() {
service.supply(() -> {
return new Output();
});
}
public void execute() {
service.execute(input -> {
input.doSomething();
return new Output();
});
}
public Output process(Input input) {
return service.process(input, input2 -> {
input2.doSomething();
return new Output();
});
}
}
Testing
Consumer in the consume()
method
Consumer is an interface which accepts a value and eventually does something with it. In our example we will pretend Service represents an external system which provides an Input value into a consumer and expects its clients to invoke the Input.doSomething()
method.
Because we want to verify such communication with an external system we will mock the Input class, so we can verify a client's interaction with it. Then we will manually invoke the consumer, since Service is mocked as well, and the part of code invoking the consumer is missing. Of course if Input would be a POJO class we wouldn't have to mock it, but we could just verify its state instead.
Also, we need to extract the consumer's implementation from the client's implementation. We will do that with Mockito "argument captor" mechanism which will capture consumer's lambda function and then return it, by calling the getValue()
method.
- Call the client's
consume()
method. - Capture the consumer.
- Provide an input for the consumer and call it "manually" in the test.
- Check what happened to the input.
@Captor
private ArgumentCaptor<Consumer<Input>> consumerArgumentCaptor;
@Test
void consumeCallsServiceAndDoSomethingWithInput() {
// given
Service service = Mockito.mock(Service.class);
Input input = Mockito.mock(Input.class);
Client client = new Client(service);
// when
client.consume();
// then
Mockito.verify(service).consume(consumerArgumentCaptor.capture());
Consumer<Input> consumer = consumerArgumentCaptor.getValue();
consumer.accept(input);
Mockito.verify(input).doSomething();
}
Problem with this solution is that we are calling the consumer in the "then" part of the test. If there is a bug in the consumer's lambda function, then eventual exception stacktrace will point to this part of the test. This is problematic because in the "then" part we should be checking results of the tested method not its execution. Execution exceptions are not expected in the "then" part of the test. These should happen only in the "when" part of the test.
To avoid problems from the previous example we can mock the Service.consume()
method and call the consumer in our mock. Thanks to this approach client's workflow will be executed in the "when" part of the test by just calling the client.consume()
method.
- Provide an input for a consumer.
- Create fake implementation of the
Service.consume()
method, which will invoke consumer at the right time. - Call the client's
consume()
method. - Do some verification.
@Test
void consumeCallsServiceAndDoSomethingWithInput() {
// given
Service service = Mockito.mock(Service.class);
Input input = Mockito.mock(Input.class);
// By providing fake implementation of the consume method, consumer will be
// invoked in the "when" part of the test.
Mockito.doAnswer(invocation -> {
Consumer<Input> consumer = invocation.getArgument(0);
consumer.accept(input);
return null;
}).when(service).consume(Mockito.any());
Client client = new Client(service);
// when
client.consume();
// then
Mockito.verify(service).consume(Mockito.any());
Mockito.verify(input).doSomething();
}
Supplier in the supply()
method
Supplier is an interface which returns a value created by its implementation, usually a lambda function. In our example, the returned value is an Output object. We will verify that a Output instance has been returned. In real-world we can test its value as well.
Similarly, as in the previous example, we will create a fake implementation of Service's supply method in which the Supplier will be invoked. To be able to check the Output, we have to capture Supplier's result and store it somewhere, so it can be verified in the "then" part of the test. To do that I've "misused" the AtomicReference interface. Of course, you can implement your own, lighter version, of similar value holder, without all that unnecessary "thread-safe stuff".
- Invoke the supplier manually, by mocking implementation of the
Service.supply()
method. - Check the supplier's output.
@Test
void supplyCallsServiceAndProvidesItOutput() {
// given
Service service = Mockito.mock(Service.class);
AtomicReference<Output> outputReference = new AtomicReference<>();
Mockito.doAnswer(invocation -> {
Supplier<Output> supplier = invocation.getArgument(0);
Output output = supplier.get();
outputReference.set(output);
return null;
}).when(service).supply(Mockito.any());
Client client = new Client(service);
// when
client.supply();
// then
Mockito.verify(service).supply(Mockito.any());
assertNotNull(outputReference.get());
}
Function in the execute()
method
Function is an interface which combines previous two. It accepts a value passed to it and returns an output. So in this case we have to combine previous approaches. We will pass an Input mock to Function and capture its result, the Output object will be stored to the value holder. Both, Input and Output are verified in the "then" part of the test.
- Provide an input for a function.
- Invoke the Function, in the fake implementation of the
Service.execute()
method and capture it's output. - Check what happened to the Input object and check the Output object.
@Test
void executeCallsServiceDoesSomethingWithInputAndProvidesOutput() {
// given
Service service = Mockito.mock(Service.class);
Input input = Mockito.mock(Input.class);
AtomicReference<Output> outputReference = new AtomicReference<>();
Mockito.doAnswer(invocation -> {
Function<Input, Output> function = invocation.getArgument(0);
Output output = function.apply(input);
outputReference.set(output);
return null;
}).when(service).execute(Mockito.any());
Client client = new Client(service);
// when
client.execute();
// then
Mockito.verify(service).execute(Mockito.any());
Mockito.verify(input).doSomething();
assertNotNull(outputReference.get());
}
Function with complications in the process()
method
This is the test of the Function interface with some added complexity. In this case an input is passed to the Client.process()
method, which is then forwarded to the Function interface. Similarly, the output of the Function interface is then forwarded as a result of the Client.process()
method which is verified.
Please take a note that such a test is potentially dangerous. This test makes an assumption about implementation details of tested method which may not be true. For example the Service.process()
method may modify the input before passing it to the Function interface. In the test we are assuming the input is always the same and changes, even correct, to the application ma cause test not to pass. Then we would have to modify the fake function to represent Service's implementation which will lead to overcomplicated test, being more costly or bringing less value to the project.
- Pass input parameter to a function.
- Invoke the Function, in the fake implementation of the
Service.execute()
method and forward it's output to theClient.process()
method's result. - Check what happened to the Input object and check the Output object.
@Test
void processPassesInputToServiceDoesSomethingWithItAndReturnsOutput() {
// given
Service service = Mockito.mock(Service.class);
Input input = Mockito.mock(Input.class);
Mockito.doAnswer(invocation -> {
Input internalInput = invocation.getArgument(0);
Function<Input, Output> function = invocation.getArgument(1);
Output internalOutput = function.apply(internalInput);
return internalOutput;
}).when(service).process(Mockito.eq(input), Mockito.any());
Client client = new Client(service);
// when
Output output = client.process(input);
// then
Mockito.verify(service).process(Mockito.eq(input), Mockito.any());
Mockito.verify(input).doSomething();
assertNotNull(output);
}