При шпионаже в CompletableFuture с Mockito, spyObj.get иногда дает сбой - PullRequest
0 голосов
/ 30 июня 2018

Я сталкиваюсь с проблемой, из-за которой мой пример кода, приведенный ниже, иногда не работает при запуске набора тестов, но тесты по отдельности, кажется, всегда проходят. Если я использую только .get () для шпиона CompletableFuture без указания таймаута, он зависает бесконечно.

Эта проблема возникает как в Windows, так и в OS X, и я пробовал несколько разных версий Java 8 JDK.

У меня есть эта проблема с Mockito 2.18.3 и Mockito 1.10.19.

Я могу успешно выполнить приведенный ниже пример кода набора тестов, иногда 7-10 раз, но почти всегда при попытке более 10 раз я вижу случайные сбои теста.

Любая помощь будет принята с благодарностью. Я также разместил в списке рассылки Mockito, но там все выглядит довольно спокойно.

package example;


import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Test;
import static org.mockito.Mockito.spy;


public class MockitoCompletableFuture1Test {

    @Test
    public void test1() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

    @Test
    public void test2() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

    @Test
    public void test3() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

    @Test
    public void test4() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

    @Test
    public void test5() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

}

Ответы [ 2 ]

0 голосов
/ 02 июля 2018

Согласно Важное замечание о шпионаже реальных объектов! :

Mockito * не * делегирует вызовы переданному реальному экземпляру, вместо этого он фактически создает его копию. Поэтому, если вы сохраняете реальный экземпляр и взаимодействуете с ним, не ожидайте, что шпион узнает об этом взаимодействии и его влиянии на состояние реального экземпляра. [...]

Так что, в основном, он примет состояние вашего будущего в тот момент, когда вы позвоните spy(). Если он уже завершен, то и получающийся шпион тоже будет. В противном случае ваш шпион останется незавершенным, кроме случаев, когда вы выполните его самостоятельно.

Поскольку асинхронное завершение будет выполняться в исходном будущем, а не в вашем шпионе, оно, таким образом, не будет отражено в вашем шпионе.

Единственный случай , где это будет работать должным образом, это когда у вас есть полный контроль над ним. Это означает, что вы создали бы CompletableFuture с помощью new, завернули его в шпиона и использовали бы только этого шпиона.

В целом, однако, я бы советовал избегать насмешливых фьючерсов , так как вы часто не контролируете, как они обрабатываются. И, как указано в Раздел Помните Мокито :

Не издевайтесь над типами, которых у вас нет

CompletableFuture это не ваш тип.

В любом случае, не нужно издеваться над CompletableFuture методами, так как вы можете контролировать то, что они делают, основываясь на complete() или completeExecptionally(). С другой стороны, нет необходимости проверять, вызваны ли его методы, так как:

  • методы с побочными эффектами (например, complete()) могут быть легко утверждены впоследствии;
  • другие методы возвращают значения, которые должны сделать ваш тест неудачным, если они не используются.

По сути, CompletableFuture ведет себя подобно объекту значения, и в документации говорится:

Не имитировать значения объектов

Если вы чувствуете, что ваш тест не может быть написан без использования шпиона, попробуйте сократить его до MCVE и опубликовать отдельный вопрос о том, как это сделать.

0 голосов
/ 30 июня 2018

Когда создается future (вызывается CompletableFuture.supplyAsync), он также создает поток (ForkJoinPool.commonPool-worker-N) для выполнения лямбда-выражения. Этот поток имеет ссылку на вновь созданный объект (в нашем случае future). Когда асинхронное задание завершено, поток (ForkJoinPool.commonPool-worker-N) уведомит (разбудит) другой поток (main), ожидая его завершения.

Откуда он знает, какой поток его ждет? Когда вы вызываете метод get(), текущий поток будет сохранен как поле в классе, и поток будет парковаться (спать) и будет ждать, пока не будет отменен каким-либо другим потоком.

Проблема в том, что futureSpy сохранит в своем поле текущий поток (main), но асинхронный поток попытается прочитать информацию из объекта future (null).

Проблема не всегда появляется в вашем тестовом примере, потому что если асинхронная функция уже завершена, get не переведет основной поток в спящий режим.


Сокращенный пример

В целях тестирования я сократил количество тестов до более короткого, который надежно воспроизводит ошибку (кроме первого запуска):

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static org.mockito.Mockito.spy;

public class App {
   public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
      for (int i = 0; i < 100; i++) {
         CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
               Thread.sleep(500);
            } catch (InterruptedException e) {
               throw new RuntimeException(e);
            }
            return "ABC";
         });
         CompletableFuture<String> futureSpy = spy(future);
         try {
            futureSpy.get(2, TimeUnit.SECONDS);
            System.out.println("i = " + i);
         } catch (TimeoutException ex) {
            System.out.println("i = " + i + " FAIL");
         }
      }
   }
}

В моих тестах вывод:

i = 0
i = 1 FAIL
i = 2 FAIL
i = 3 FAIL
...