Como usar o JUnit para testar processos assíncronos

204

Como você testa métodos que acionam processos assíncronos com o JUnit?

Não sei como fazer meu teste aguardar o término do processo (não é exatamente um teste de unidade, é mais como um teste de integração, pois envolve várias classes e não apenas uma).

Sam
fonte
Você pode tentar o JAT (Java Asynchronous Test): bitbucket.org/csolar/jat
cs0lar
2
O JAT possui 1 observador e não é atualizado em 1,5 anos. Awaitility foi atualizada há apenas 1 mês e está na versão 1.6 no momento desta redação. Eu não sou afiliado a nenhum dos projetos, mas se eu fosse investir em uma adição ao meu projeto, daria mais credibilidade à Awaitility no momento.
Les Hazlewood
O JAT ainda não possui atualizações: "Última atualização em 2013-01-19". Apenas economize tempo para seguir o link.
deamon
@ LesHazlewood, um observador é ruim para o JAT, mas quase não há atualizações há anos ... Apenas um exemplo. Com que frequência você atualiza a pilha TCP de baixo nível do seu sistema operacional, se funcionar? A alternativa ao JAT é respondida abaixo stackoverflow.com/questions/631598/… .
user1742529

Respostas:

46

IMHO, é uma prática ruim ter testes de unidade para criar ou aguardar threads, etc. Você gostaria que esses testes fossem executados em segundos. É por isso que eu gostaria de propor uma abordagem em duas etapas para testar processos assíncronos.

  1. Teste se seu processo assíncrono foi enviado corretamente. Você pode simular o objeto que aceita suas solicitações assíncronas e garantir que o trabalho enviado tenha propriedades corretas etc.
  2. Teste se seus retornos de chamada assíncronos estão fazendo as coisas certas. Aqui você pode simular o trabalho enviado originalmente e presumir que ele foi inicializado corretamente e verificar se seus retornos de chamada estão corretos.
Cem Catikkas
fonte
148
Certo. Mas, às vezes, você precisa testar o código que deveria especificamente gerenciar os threads.
lacker
77
Para aqueles de nós que usam o Junit ou o TestNG para fazer testes de integração (e não apenas testes de unidade) ou testes de aceitação do usuário (por exemplo, com pepino), é absolutamente necessário aguardar uma conclusão assíncrona e verificar o resultado.
Les Hazlewood
38
Processos assíncronos são alguns dos códigos mais complicados para corrigir, e você diz que não deve usar o teste de unidade para eles e apenas testar com um único thread? Essa é uma péssima ideia.
Charles
18
Os testes simulados geralmente falham em provar que a funcionalidade funciona de ponta a ponta. A funcionalidade assíncrona precisa ser testada de maneira assíncrona para garantir que funcione. Chame de teste de integração, se preferir, mas ainda é necessário.
22815 Scott Scoring
4
Esta não deve ser a resposta aceita. O teste vai além do teste de unidade. O OP chama isso de mais um teste de integração do que um teste de unidade.
Jeremias Adams
190

Uma alternativa é usar a classe CountDownLatch .

public class DatabaseTest {

    /**
     * Data limit
     */
    private static final int DATA_LIMIT = 5;

    /**
     * Countdown latch
     */
    private CountDownLatch lock = new CountDownLatch(1);

    /**
     * Received data
     */
    private List<Data> receiveddata;

    @Test
    public void testDataRetrieval() throws Exception {
        Database db = new MockDatabaseImpl();
        db.getData(DATA_LIMIT, new DataCallback() {
            @Override
            public void onSuccess(List<Data> data) {
                receiveddata = data;
                lock.countDown();
            }
        });

        lock.await(2000, TimeUnit.MILLISECONDS);

        assertNotNull(receiveddata);
        assertEquals(DATA_LIMIT, receiveddata.size());
    }
}

NOTA: você não pode apenas usar sincronizado com um objeto comum como um bloqueio, pois retornos de chamada rápidos podem liberar o bloqueio antes que o método de espera do bloqueio seja chamado. Veja este post de Joe Walnes.

EDIT Removidos blocos sincronizados em torno do CountDownLatch, graças aos comentários de @jtahlborn e @Ring

Martin
fonte
8
por favor, não siga este exemplo, está incorreto. você não deve estar sincronizando em um CountDownLatch, pois ele lida com a segurança de threads internamente.
jtahlborn
1
Foi um bom conselho até a parte sincronizada, que consumiu provavelmente cerca de 3-4 horas de tempo de depuração. stackoverflow.com/questions/11007551/…
Toque em
2
Desculpas pelo erro. Eu editei a resposta adequadamente.
Martin
7
Se você estiver verificando se o onSuccess foi chamado, deve afirmar que lock.await retorna true.
Gilbert
1
@ Martin, isso estaria correto, mas isso significaria que você tem um problema diferente que precisa ser corrigido.
George Aristy
75

Você pode tentar usar a biblioteca Awaitility . Isso facilita o teste dos sistemas dos quais você está falando.

Johan
fonte
21
Um aviso amigável: Johan é o principal colaborador do projeto.
dbm
1
Sofre com o problema fundamental de ter que esperar (os testes de unidade precisam ser executados rapidamente ). Idealmente, você realmente não quer esperar um milissegundo mais do que o necessário, então acho que usar CountDownLatch(ver resposta de @Martin) é melhor nesse sentido.
George Aristy
Realmente fantástico.
R. Karlus
Essa é a biblioteca perfeita que atende aos meus requisitos de teste de integração de processos assíncronos. Realmente fantástico. A biblioteca parece estar bem mantida e possui recursos que vão do básico ao avançado que acredito serem suficientes para atender à maioria dos cenários. Obrigado pela referência impressionante!
Tanvir
Sugestão realmente incrível. Obrigado
RoyalTiger
68

Se você usar um CompletableFuture (introduzido no Java 8) ou um SettableFuture (do Google Guava ), poderá fazer o teste terminar assim que terminar, em vez de esperar um período de tempo predefinido. Seu teste seria mais ou menos assim:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());
user393274
fonte
4
... e se você está preso com java-less than-e oito tentativa goiabas SettableFuture que faz praticamente a mesma coisa
Markus T
23

Inicie o processo e aguarde o resultado usando a Future.

Tom Hawtin - linha de orientação
fonte
18

Um método que achei bastante útil para testar métodos assíncronos é injetar uma Executorinstância no construtor do objeto a testar. Na produção, a instância do executor está configurada para ser executada de forma assíncrona, enquanto no teste ela pode ser zombada para ser executada de forma síncrona.

Então, suponha que eu esteja tentando testar o método assíncrono Foo#doAsync(Callback c),

class Foo {
  private final Executor executor;
  public Foo(Executor executor) {
    this.executor = executor;
  }

  public void doAsync(Callback c) {
    executor.execute(new Runnable() {
      @Override public void run() {
        // Do stuff here
        c.onComplete(data);
      }
    });
  }
}

Na produção, eu construiria Foocom uma Executors.newSingleThreadExecutor()instância Executor, enquanto no teste provavelmente a construiria com um executor síncrono que faz o seguinte -

class SynchronousExecutor implements Executor {
  @Override public void execute(Runnable r) {
    r.run();
  }
}

Agora, meu teste JUnit do método assíncrono está bastante limpo -

@Test public void testDoAsync() {
  Executor executor = new SynchronousExecutor();
  Foo objectToTest = new Foo(executor);

  Callback callback = mock(Callback.class);
  objectToTest.doAsync(callback);

  // Verify that Callback#onComplete was called using Mockito.
  verify(callback).onComplete(any(Data.class));

  // Assert that we got back the data that we expected.
  assertEquals(expectedData, callback.getData());
}
Matt
fonte
Não funciona se eu quiser testar a integração algo que envolva uma chamada de biblioteca assíncrona como a da SpringWebClient
Stefan Haberl
8

Não há nada inerentemente errado com o teste de código encadeado / assíncrono, principalmente se o threading for o ponto do código que você está testando. A abordagem geral para testar essas coisas é:

  • Bloquear a linha de teste principal
  • Capturar afirmações com falha de outros threads
  • Desbloqueie o segmento de teste principal
  • Repetir todas as falhas

Mas isso é muita clichê para um teste. Uma abordagem melhor / mais simples é usar apenas o ConcurrentUnit :

  final Waiter waiter = new Waiter();

  new Thread(() -> {
    doSomeWork();
    waiter.assertTrue(true);
    waiter.resume();
  }).start();

  // Wait for resume() to be called
  waiter.await(1000);

O benefício disso sobre a CountdownLatchabordagem é que é menos detalhado, pois as falhas de asserção que ocorrem em qualquer encadeamento são relatadas adequadamente ao encadeamento principal, o que significa que o teste falha quando deveria. Um artigo que compara a CountdownLatchabordagem do ConcurrentUnit está aqui .

Também escrevi um post sobre o assunto para quem quer aprender um pouco mais detalhadamente.

Jonathan
fonte
uma solução similar eu usei no passado é github.com/MichaelTamm/junit-toolbox , também caracterizado como uma extensão de terceiros no junit.org/junit4
dschulten
4

Que tal chamar SomeObject.waite notifyAllcomo descrito aqui OU usando o método Robotiums Solo.waitForCondition(...) OU usar uma classe que eu escrevi para fazer isso (consulte os comentários e a classe de teste para saber como usar)

Dori
fonte
1
O problema com a abordagem de espera / notificação / interrupção é que o código que você está testando pode potencialmente interferir nos threads em espera (eu já vi isso acontecer). É por isso que o ConcurrentUnit usa um circuito privado no qual os threads podem esperar, que não podem ser inadvertidamente interferidos por interrupções no thread de teste principal.
24415 Jonathan
3

Eu encontro uma biblioteca socket.io para testar a lógica assíncrona. Parece uma maneira simples e breve usando o LinkedBlockingQueue . Aqui está um exemplo :

    @Test(timeout = TIMEOUT)
public void message() throws URISyntaxException, InterruptedException {
    final BlockingQueue<Object> values = new LinkedBlockingQueue<Object>();

    socket = client();
    socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
        @Override
        public void call(Object... objects) {
            socket.send("foo", "bar");
        }
    }).on(Socket.EVENT_MESSAGE, new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            values.offer(args);
        }
    });
    socket.connect();

    assertThat((Object[])values.take(), is(new Object[] {"hello client"}));
    assertThat((Object[])values.take(), is(new Object[] {"foo", "bar"}));
    socket.disconnect();
}

Usando o LinkedBlockingQueue, a API é bloqueada até obter o resultado da mesma forma síncrona. E defina o tempo limite para evitar assumir muito tempo para aguardar o resultado.

Fantasy Fang
fonte
1
Abordagem impressionante!
aminography
3

Vale ressaltar que há um capítulo muito útil Testing Concurrent Programsno Concurrency in Practice, que descreve algumas abordagens de teste de unidade e fornece soluções para problemas.

onze
fonte
1
Que abordagem é essa? Você poderia dar um exemplo?
Bruno Ferreira
2

É isso que estou usando hoje em dia se o resultado do teste for produzido de forma assíncrona.

public class TestUtil {

    public static <R> R await(Consumer<CompletableFuture<R>> completer) {
        return await(20, TimeUnit.SECONDS, completer);
    }

    public static <R> R await(int time, TimeUnit unit, Consumer<CompletableFuture<R>> completer) {
        CompletableFuture<R> f = new CompletableFuture<>();
        completer.accept(f);
        try {
            return f.get(time, unit);
        } catch (InterruptedException | TimeoutException e) {
            throw new RuntimeException("Future timed out", e);
        } catch (ExecutionException e) {
            throw new RuntimeException("Future failed", e.getCause());
        }
    }
}

Usando importações estáticas, o teste é agradável. (observe, neste exemplo, estou iniciando um tópico para ilustrar a ideia)

    @Test
    public void testAsync() {
        String result = await(f -> {
            new Thread(() -> f.complete("My Result")).start();
        });
        assertEquals("My Result", result);
    }

Se f.completenão for chamado, o teste falhará após um tempo limite. Você também pode f.completeExceptionallyfalhar cedo.

Jochen Bedersdorfer
fonte
2

Há muitas respostas aqui, mas uma simples é apenas criar um CompletableFuture completo e usá-lo:

CompletableFuture.completedFuture("donzo")

Então, no meu teste:

this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));

Estou apenas garantindo que todas essas coisas sejam chamadas assim mesmo. Essa técnica funciona se você estiver usando este código:

CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();

Ele passará através dele quando todos os CompletableFutures estiverem concluídos!

markthegrea
fonte
2

Evite testar com threads paralelos sempre que puder (o que é na maioria das vezes). Isso só tornará seus testes esquisitos (algumas vezes passam, outras vezes falham).

Somente quando você precisar chamar alguma outra biblioteca / sistema, poderá ser necessário aguardar outros threads; nesse caso, sempre use a biblioteca Awaitility em vez de Thread.sleep().

Nunca chame apenas get()ou join()em seus testes; caso contrário, seus testes poderão ser executados para sempre no servidor de IC, caso o futuro nunca seja concluído. Sempre afirme isDone()primeiro em seus testes antes de ligar get(). Para CompletionStage, é isso .toCompletableFuture().isDone().

Quando você testa um método sem bloqueio como este:

public static CompletionStage<String> createGreeting(CompletableFuture<String> future) {
    return future.thenApply(result -> "Hello " + result);
}

então você não deve apenas testar o resultado passando um futuro concluído no teste, mas também certificar-se de que seu método doSomething()não seja bloqueado chamando join()or get(). Isso é importante em particular se você usar uma estrutura sem bloqueio.

Para fazer isso, teste com um futuro não concluído que você definiu como concluído manualmente:

@Test
public void testDoSomething() throws Exception {
    CompletableFuture<String> innerFuture = new CompletableFuture<>();
    CompletableFuture<String> futureResult = createGreeting(innerFuture).toCompletableFuture();
    assertFalse(futureResult.isDone());

    // this triggers the future to complete
    innerFuture.complete("world");
    assertTrue(futureResult.isDone());

    // futher asserts about fooResult here
    assertEquals(futureResult.get(), "Hello world");
}

Dessa forma, se você adicionar future.join()ao doSomething (), o teste falhará.

Se o seu Serviço usar um ExecutorService como em thenApplyAsync(..., executorService), em seus testes injete um ExecutorService de thread único, como o da goiaba:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Se o seu código usar o forkJoinPool como thenApplyAsync(...), reescreva o código para usar um ExecutorService (existem muitas boas razões) ou use Awaitility.

Para encurtar o exemplo, fiz do BarService um argumento de método implementado como um lambda Java8 no teste, normalmente seria uma referência injetada que você zombaria.

tkruse
fonte
Hey @tkruse, talvez você tenha um repositório público de Git com um teste usando essa técnica?
Cristiano
@Christiano: isso seria contra a filosofia SO. Em vez disso, alterei os métodos para compilar sem nenhum código adicional (todas as importações são java8 + ou junit) quando você as cola em uma classe de teste de junit vazia. Sinta-se livre para votar.
tkruse
Eu entendi agora. obrigado. Meu problema agora é testar quando os métodos retornam CompletableFuture, mas aceitam outros objetos como parâmetros diferentes de um CompletableFuture.
Cristiano
No seu caso, quem cria o CompletableFuture que o método retorna? Se for outro serviço, isso pode ser ridicularizado e minha técnica ainda se aplica. Se o método em si cria um CompletableFuture, a situação muda muito para que você possa fazer uma nova pergunta sobre ele. Em seguida, depende de qual thread concluirá o futuro que seu método retornará.
tkruse
1

Eu prefiro usar esperar e notificar. É simples e claro.

@Test
public void test() throws Throwable {
    final boolean[] asyncExecuted = {false};
    final Throwable[] asyncThrowable= {null};

    // do anything async
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                // Put your test here.
                fail(); 
            }
            // lets inform the test thread that there is an error.
            catch (Throwable throwable){
                asyncThrowable[0] = throwable;
            }
            // ensure to release asyncExecuted in case of error.
            finally {
                synchronized (asyncExecuted){
                    asyncExecuted[0] = true;
                    asyncExecuted.notify();
                }
            }
        }
    }).start();

    // Waiting for the test is complete
    synchronized (asyncExecuted){
        while(!asyncExecuted[0]){
            asyncExecuted.wait();
        }
    }

    // get any async error, including exceptions and assertationErrors
    if(asyncThrowable[0] != null){
        throw asyncThrowable[0];
    }
}

Basicamente, precisamos criar uma referência Array final, a ser usada dentro da classe interna anônima. Prefiro criar um booleano [], porque posso colocar um valor para controlar se precisarmos esperar (). Quando tudo estiver pronto, apenas lançamos o asyncExecuted.

Paulo
fonte
1
Se sua afirmação falhar, o segmento de teste principal não saberá sobre isso.
Jonathan
Obrigado pela solução, me ajuda a depurar código com conexão websocket.
Nazar Sakharenko 02/02
@ Jonathan, atualizei o código para capturar qualquer afirmação e exceção e informá-lo para o segmento de teste principal.
Paulo
1

Para todos os usuários do Spring, é assim que eu costumo fazer meus testes de integração hoje em dia, onde o comportamento assíncrono está envolvido:

Dispare um evento de aplicativo no código de produção, quando uma tarefa assíncrona (como uma chamada de E / S) for concluída. Na maioria das vezes, esse evento é necessário para lidar com a resposta da operação assíncrona na produção.

Com esse evento, você pode usar a seguinte estratégia no seu caso de teste:

  1. Execute o sistema em teste
  2. Ouça o evento e verifique se o evento foi disparado
  3. Faça suas afirmações

Para quebrar isso, primeiro você precisará de algum tipo de evento de domínio para disparar. Estou usando um UUID aqui para identificar a tarefa que foi concluída, mas é claro que você pode usar outra coisa, desde que ela seja única.

(Observe que os seguintes trechos de código também usam anotações Lombok para se livrar do código da placa da caldeira)

@RequiredArgsConstructor
class TaskCompletedEvent() {
  private final UUID taskId;
  // add more fields containing the result of the task if required
}

O código de produção em si normalmente fica assim:

@Component
@RequiredArgsConstructor
class Production {

  private final ApplicationEventPublisher eventPublisher;

  void doSomeTask(UUID taskId) {
    // do something like calling a REST endpoint asynchronously
    eventPublisher.publishEvent(new TaskCompletedEvent(taskId));
  }

}

Posso usar um Spring @EventListenerpara capturar o evento publicado no código de teste. O ouvinte de evento é um pouco mais envolvido, porque precisa lidar com dois casos de uma maneira segura para threads:

  1. O código de produção é mais rápido que o caso de teste e o evento já foi acionado antes que o caso de teste verifique o evento ou
  2. O caso de teste é mais rápido que o código de produção e o caso de teste precisa aguardar o evento.

A CountDownLatché usado para o segundo caso, como mencionado em outras respostas aqui. Observe também que a @Orderanotação no método manipulador de eventos garante que esse método manipulador de eventos seja chamado após qualquer outro ouvinte de evento usado na produção.

@Component
class TaskCompletionEventListener {

  private Map<UUID, CountDownLatch> waitLatches = new ConcurrentHashMap<>();
  private List<UUID> eventsReceived = new ArrayList<>();

  void waitForCompletion(UUID taskId) {
    synchronized (this) {
      if (eventAlreadyReceived(taskId)) {
        return;
      }
      checkNobodyIsWaiting(taskId);
      createLatch(taskId);
    }
    waitForEvent(taskId);
  }

  private void checkNobodyIsWaiting(UUID taskId) {
    if (waitLatches.containsKey(taskId)) {
      throw new IllegalArgumentException("Only one waiting test per task ID supported, but another test is already waiting for " + taskId + " to complete.");
    }
  }

  private boolean eventAlreadyReceived(UUID taskId) {
    return eventsReceived.remove(taskId);
  }

  private void createLatch(UUID taskId) {
    waitLatches.put(taskId, new CountDownLatch(1));
  }

  @SneakyThrows
  private void waitForEvent(UUID taskId) {
    var latch = waitLatches.get(taskId);
    latch.await();
  }

  @EventListener
  @Order
  void eventReceived(TaskCompletedEvent event) {
    var taskId = event.getTaskId();
    synchronized (this) {
      if (isSomebodyWaiting(taskId)) {
        notifyWaitingTest(taskId);
      } else {
        eventsReceived.add(taskId);
      }
    }
  }

  private boolean isSomebodyWaiting(UUID taskId) {
    return waitLatches.containsKey(taskId);
  }

  private void notifyWaitingTest(UUID taskId) {
    var latch = waitLatches.remove(taskId);
    latch.countDown();
  }

}

O último passo é executar o sistema em teste em um caso de teste. Estou usando um teste SpringBoot com o JUnit 5 aqui, mas isso deve funcionar da mesma forma para todos os testes usando um contexto Spring.

@SpringBootTest
class ProductionIntegrationTest {

  @Autowired
  private Production sut;

  @Autowired
  private TaskCompletionEventListener listener;

  @Test
  void thatTaskCompletesSuccessfully() {
    var taskId = UUID.randomUUID();
    sut.doSomeTask(taskId);
    listener.waitForCompletion(taskId);
    // do some assertions like looking into the DB if value was stored successfully
  }

}

Observe que, ao contrário de outras respostas aqui, esta solução também funcionará se você executar seus testes em paralelo e vários segmentos exercitarem o código assíncrono ao mesmo tempo.

Stefan Haberl
fonte
0

Se você quiser testar a lógica, não a teste de forma assíncrona.

Por exemplo, para testar este código que funciona com resultados de um método assíncrono.

public class Example {
    private Dependency dependency;

    public Example(Dependency dependency) {
        this.dependency = dependency;            
    }

    public CompletableFuture<String> someAsyncMethod(){
        return dependency.asyncMethod()
                .handle((r,ex) -> {
                    if(ex != null) {
                        return "got exception";
                    } else {
                        return r.toString();
                    }
                });
    }
}

public class Dependency {
    public CompletableFuture<Integer> asyncMethod() {
        // do some async stuff       
    }
}

No teste, simule a dependência com a implementação síncrona. O teste de unidade é completamente síncrono e é executado em 150ms.

public class DependencyTest {
    private Example sut;
    private Dependency dependency;

    public void setup() {
        dependency = Mockito.mock(Dependency.class);;
        sut = new Example(dependency);
    }

    @Test public void success() throws InterruptedException, ExecutionException {
        when(dependency.asyncMethod()).thenReturn(CompletableFuture.completedFuture(5));

        // When
        CompletableFuture<String> result = sut.someAsyncMethod();

        // Then
        assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
        String value = result.get();
        assertThat(value, is(equalTo("5")));
    }

    @Test public void failed() throws InterruptedException, ExecutionException {
        // Given
        CompletableFuture<Integer> c = new CompletableFuture<Integer>();
        c.completeExceptionally(new RuntimeException("failed"));
        when(dependency.asyncMethod()).thenReturn(c);

        // When
        CompletableFuture<String> result = sut.someAsyncMethod();

        // Then
        assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
        String value = result.get();
        assertThat(value, is(equalTo("got exception")));
    }
}

Você não testa o comportamento assíncrono, mas pode testar se a lógica está correta.

Nils El-Himoud
fonte