Проблема сводится к тому, что тестируемый метод вызывает статический метод: CompletableFuture.runAsync()
. Статические методы в целом дают мало контроля над насмешками и утверждениями.
Даже если вы используете sleep()
в своем тесте, вы не можете утверждать, вызывается ли someObject.foo()
асинхронно или нет. Если вызов сделан в вызывающем потоке, тест все равно пройдет. Более того, использование sleep()
замедляет ваши тесты, а слишком короткое sleep()
приведет к случайному сбою теста.
Если на самом деле это единственное решение, вам следует использовать такую библиотеку, как Ожидание , которое опрашивает до тех пор, пока утверждение не будет удовлетворено, с тайм-аутом.
Существует несколько альтернативных вариантов, облегчающих тестирование вашего кода:
- Сделать
testingMethod()
вернуть Future
(как вы и думали в комментариях): это не позволяет утверждать асинхронное выполнение, но избегает слишком длительного ожидания; - Обернуть
runAsync()
метод в другом сервисе , который можно смоделировать и захватить аргумент; - Если вы используете Spring , переместите лямбда-выражение в другой сервис в аннотированном методес
@Async
. Это позволяет легко макетировать и модульно тестировать эту услугу, а также снимает бремя непосредственного вызова runAsync()
; - Использовать пользовательского исполнителя и передавать его
runAsync()
;
Если вы используете Spring, Я бы порекомендовал использовать третье решение , так как оно действительно является самым чистым и позволяет избежать загромождения кода с помощью вызовов runAsync()
везде.
Варианты 2 и 4 очень похожи, они просто изменяют то, что вы должны высмеивать.
Если вы выберете четвертое решение, вот как вы можете это сделать:
Измените проверенный класс, чтобы использовать пользовательский 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);
}
}
Реализуйте пользовательский Executor
, который просто захватывает команду вместо ее запуска:
class CapturingExecutor implements Executor {
private Runnable command;
@Override
public void execute(Runnable command) {
this.command = command;
}
public Runnable getCommand() {
return command;
}
}
(вы также можете@Mock
Executor
и используйте ArgumentCaptor
, но я думаю, что этот подход более чист)
Используйте CapturingExecutor
в своем тесте:
@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();
}
}