The problem boils down to to the fact that the tested method calls a static method: CompletableFuture.runAsync(). Static methods in general give little control for mocking and asserting.
Even if you use a sleep() in your test, you cannot assert whether someObject.foo() is called asynchronously or not. If the call is made on the calling thread, the test will still pass. Moreover using sleep() slows down your tests, and a too short sleep() will cause the test to randomly fail.
If really this seems to be the only solution, you should use a library like Awaitability that polls until the assertion is satisfied, with a timeout.
There are several alternatives to make your code easier to test:
- Make
testingMethod() return a Future (as you thought in the comments): this does not allow asserting the asynchronous execution, but it avoids waiting too much;
- Wrap the
runAsync() method in another service that you can mock, and capture the argument;
- If you are using Spring, move the lambda expression to another service, in a method annotated with
@Async. This allows to easily mock and unit test that service, and removes the burden of calling runAsync() directly;
- Use a custom executor, and pass it to
runAsync();
If you are using Spring, I would recommend using the third solution as it really is the cleanest, and avoids cluttering your code with runAsync() calls everywhere.
Options 2 and 4 are very similar, it just changes what you have to mock.
If you go for the fourth solution, here is how you can do it:
Change tested class to use a custom Executor:
class TestedObject {
private SomeObject someObject;
private Executor executor;
public TestedObject(SomeObject someObject, Executor executor) {
this.someObject = someObject;
this.executor = executor;
}
void testingMethod() {
// some operations
someObject.foo();
CompletableFuture.runAsync(() -> {
// some other operations
someObject.foo();
}, executor);
}
}
Implement a custom Executor that simply captures the command instead of running it:
class CapturingExecutor implements Executor {
private Runnable command;
@Override
public void execute(Runnable command) {
this.command = command;
}
public Runnable getCommand() {
return command;
}
}
(you could also @Mock the Executor and use an ArgumentCaptor but I think this approach is cleaner)
Use the CapturingExecutor in your test:
@RunWith(MockitoJUnitRunner.class)
public class TestedObjectTest {
@Mock
private SomeObject someObject;
private CapturingExecutor executor;
private TestedObject testObject;
@Before
public void before() {
executor = new CapturingExecutor();
testObject = new TestedObject(someObject, executor);
}
@Test
public void testingMethodTest() {
testObject.testingMethod();
verify(someObject).foo();
// make sure that we actually captured some command
assertNotNull(executor.getCommand());
// now actually run the command and check that it does what it is expected to do
executor.getCommand().run();
// Mockito still counts the previous call, hence the times(2).
// Not relevant if the lambda actually calls a different method.
verify(someObject, times(2)).foo();
}
}