Как я могу проверить вызов метода внутри асинхронной операции в модульном тестировании - PullRequest
1 голос
/ 23 октября 2019

У меня есть метод, который сначала выполняет серию действий, после чего он запускает асинхронную задачу. Я хочу протестировать этот метод, но я не понимаю, как проверить, что асинхронная операция завершена.

Используя Moсkito, я хочу убедиться, что метод foo был выполнен 2раз, один раз до начала асинхронной задачи и один раз внутри нее. Проблема в том, что во время проверки Mockito асинхронная задача еще не вызывала метод внутри асинхронной операции. Поэтому тест иногда выполняется, а иногда нет.

Это пример моего метода:

void testingMethod() {
    // some operations
    someObject.foo();
    CompletableFuture.runAsync(() -> {
        // some other operations
        someObject.foo();
    });
}

И пример моего теста, в котором насмехается над someObject:

@Test
public void testingMethodTest() {
    testObject.testingMethod();

    Mockito.verify(someObject, Mockito.times(2)).foo();
}

Есть ли способ дождаться завершения асинхронной операции перед методом проверки. Или это плохой способ проверки и что вы можете посоветовать в этом случае?

Ответы [ 2 ]

1 голос
/ 23 октября 2019

Проблема сводится к тому, что тестируемый метод вызывает статический метод: CompletableFuture.runAsync(). Статические методы в целом дают мало контроля над насмешками и утверждениями.

Даже если вы используете sleep() в своем тесте, вы не можете утверждать, вызывается ли someObject.foo() асинхронно или нет. Если вызов сделан в вызывающем потоке, тест все равно пройдет. Более того, использование sleep() замедляет ваши тесты, а слишком короткое sleep() приведет к случайному сбою теста.

Если на самом деле это единственное решение, вам следует использовать такую ​​библиотеку, как Ожидание , которое опрашивает до тех пор, пока утверждение не будет удовлетворено, с тайм-аутом.

Существует несколько альтернативных вариантов, облегчающих тестирование вашего кода:

  1. Сделать testingMethod() вернуть Future (как вы и думали в комментариях): это не позволяет утверждать асинхронное выполнение, но избегает слишком длительного ожидания;
  2. ОбернутьrunAsync() метод в другом сервисе , который можно смоделировать и захватить аргумент;
  3. Если вы используете Spring , переместите лямбда-выражение в другой сервис в аннотированном методес @Async. Это позволяет легко макетировать и модульно тестировать эту услугу, а также снимает бремя непосредственного вызова runAsync();
  4. Использовать пользовательского исполнителя и передавать его 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();
    }
}
0 голосов
/ 23 октября 2019

Вы можете иметь TimeUnit.SECONDS.sleep(1); после testObject.testingMethod(); в своем тесте.

С другой стороны, я даже не думаю, что вам следует проверять, что то, что происходит асинхронно, завершено или вызвано или что-то еще, это не ответственностьэтой функции.

...