Jak używać Junit do testowania procesów asynchronicznych

Jak testować metody odpalające procesy asynchroniczne za pomocą Junit?

Nie wiem, jak sprawić, by mój test czekał na zakończenie procesu (nie jest to do końca test jednostkowy, jest bardziej jak test integracyjny, ponieważ obejmuje kilka klas, a nie tylko jedną)

Author: Sam, 2009-03-10

15 answers

IMHO to zła praktyka, żeby testy jednostkowe tworzyły lub czekały na wątki itp. Chcesz, żeby te testy zostały przeprowadzone w ułamku sekundy. Dlatego chciałbym zaproponować dwuetapowe podejście do testowania procesów asynchronicznych.

  1. Sprawdź, czy proces asynchroniczny jest przesyłany prawidłowo. Możesz wyśledzić obiekt, który akceptuje twoje żądania asynchroniczne i upewnić się, że przesłane zadanie ma poprawne właściwości itp.
  2. Sprawdź, czy Twoje wywołania asynchroniczne robią właściwe rzeczy. Tutaj możesz wyśmiać pierwotnie przesłane zadanie i załóżmy, że zostało poprawnie zainicjowane i sprawdź, czy wywołania zwrotne są poprawne.
 40
Author: Cem Catikkas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-10 18:53:30

Alternatywą jest użycie klasy 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());
    }
}

Uwaga nie można po prostu użyć zsynchronizowanego ze zwykłym obiektem jako blokady, ponieważ szybkie wywołania zwrotne mogą zwolnić blokadę przed wywołaniem metody wait. Zobacz this blog post by Joe Walnes.

EDIT usunięto synchronizowane bloki wokół CountDownLatch dzięki komentarzom @jtahlborn i @Ring

 152
Author: Martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-05 20:35:08

Możesz spróbować skorzystać z biblioteki . Ułatwia to testowanie systemów, o których mówisz.

 56
Author: Johan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-26 20:05:27

Jeśli używasz CompletableFuture (wprowadzonego w Java 8) lub SettableFuture (z Google Guava), możesz zakończyć test natychmiast po jego zakończeniu, zamiast czekać z góry określoną ilość czasu. Twój test wyglądałby mniej więcej tak:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());
 40
Author: user393274,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-01 16:54:02

Rozpocznij proces i poczekaj na wynik używając Future.

 21
Author: Tom Hawtin - tackline,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-10 18:12:49

Jedną z metod, które uznałem za przydatne do testowania metod asynchronicznych, jest wstrzyknięcie instancji Executor do konstruktora obiektu do testu. W produkcji instancja executora jest skonfigurowana do działania asynchronicznie, podczas gdy w testach może być wyśmiewana do działania synchronicznie.

Więc załóżmy, że próbuję przetestować metodę asynchroniczną 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);
      }
    });
  }
}

W produkcji skonstruowałbym Foo z Executors.newSingleThreadExecutor() instancją executora, podczas gdy w teście prawdopodobnie skonstruowałbym ją z synchronicznym executor wykonujący następujące czynności --

class SynchronousExecutor implements Executor {
  @Override public void execute(Runnable r) {
    r.run();
  }
}
Teraz mój test JUnit metody asynchronicznej jest całkiem czysty --]}
@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());
}
 15
Author: Matthew,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-13 19:31:57

A może wywołanie SomeObject.wait i notifyAllzgodnie z opisem tutaj lub użycie robotów Solo.waitForCondition(...) Metoda lub użyj klasy, którą napisałem , aby to zrobić (zobacz komentarze i klasę testową, aby dowiedzieć się, jak używać)

 4
Author: Dori,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-14 12:14:55

Nie ma nic złego w testowaniu kodu threaded/async, szczególnie jeśli threading jest punktem kodu, który testujesz. Ogólne podejście do testowania tego jest następujące:

  • Zablokuj główny wątek testowy
  • Przechwytywanie nieudanych twierdzeń z innych wątków
  • Odblokuj główny wątek testowy
  • Rethrow wszelkie porażki
Ale to dużo jak na jeden test. Lepszym/prostszym podejściem jest po prostu użycie :
  final Waiter waiter = new Waiter();

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

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

Zaletą tego podejścia w stosunku do CountdownLatch jest to, że jest to mniej gadatliwe, ponieważ błędy twierdzeń, które występują w dowolnym wątku, są odpowiednio zgłaszane do głównego wątku, co oznacza, że test nie powiedzie się, kiedy powinien. Writeup porównujący podejście CountdownLatch do współbieżności jest tutaj.

Napisałem również post na blogu na ten temat dla tych, którzy chcą dowiedzieć się trochę więcej szczegółów.

 4
Author: Jonathan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-17 05:03:20

Wolę używać wait and notify. Jest to proste i jasne.

@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];
    }
}

Zasadniczo, musimy utworzyć ostateczne odniesienie do tablicy, które będzie użyte wewnątrz anonimowej klasy wewnętrznej. Wolałbym raczej utworzyć boolean [], ponieważ mogę umieścić wartość do kontroli, jeśli mamy czekać(). Kiedy wszystko jest zrobione, po prostu uwalniamy asyncExecuted.

 2
Author: Paulo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-09 14:59:26

Warto wspomnieć, że w współbieżność w praktyce znajduje się bardzo przydatny rozdziałTesting Concurrent Programs, który opisuje niektóre podejścia do testów jednostkowych i daje rozwiązania problemów.

 2
Author: eleven,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-23 13:39:24

I find an library socket.io do testowania logiki asynchronicznej. Wygląda to w prosty i zwięzły sposób używając LinkedBlockingQueue. Oto przykład :

    @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();
}

Using LinkedBlockingQueue take API to block until to get result just like synchronous way. I ustaw timeout, aby uniknąć zakładania zbyt dużo czasu, aby czekać na wynik.

 1
Author: Fantasy Fang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-17 02:55:48

Jest wiele odpowiedzi tutaj, ale proste jest po prostu stworzyć completablefuture i używać go:

CompletableFuture.completedFuture("donzo")

Więc w moim teście:

this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));
Upewniam się, że i tak wszystkie te rzeczy zostaną nazwane. Ta technika działa, jeśli używasz tego kodu:
CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();

To będzie zip prosto przez to, jak wszystkie Completablefuture są skończone!

 1
Author: markthegrea,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-26 20:14:37

Unikaj testowania z równoległymi wątkami, kiedy tylko możesz (co jest przez większość czasu). To tylko sprawi, że twoje testy będą łuszczące się (czasami zdają, czasami nie).

Tylko wtedy, gdy musisz wywołać inną bibliotekę / system, możesz czekać na inne wątki, w takim przypadku zawsze używaj Waitility zamiast Thread.sleep().

Nigdy po prostu nie wywołaj get() lub join() w swoich testach, w przeciwnym razie twoje testy mogą działać w nieskończoność na twoim serwerze CI w przypadku, gdy przyszłość nigdy się nie skończy. Zawsze sprawdzaj isDone() najpierw w swoich testach przed wywołaniem get(). Na zakończenie, czyli .toCompletableFuture().isDone().

Podczas testowania metody nieblokującej takiej jak ta:

public static CompletionStage<Foo> doSomething(BarService service) {
    CompletionStage<Bar> future = service.getBar();
    return future.thenApply(bar -> fooToBar());
}

Następnie nie należy tylko testować wynik zdając ukończoną przyszłość w teście, należy również upewnić się, że twoja metoda doSomething() nie blokuje przez wywołanie join() lub get(). Jest to ważne w szczególności, jeśli używasz RAM nieblokujących.

Aby to zrobić, przetestuj z nie ukończoną przyszłością, którą ustawisz na uzupełnione ręcznie:

@Test
public void testDoSomething() {
    CompletableFuture<Bar> innerFuture = new CompletableFuture<>();
    fooResult = doSomething(() -> innerFuture).toCompletableFuture();
    assertFalse(fooResult.isDone());

    // this triggers the future to complete
    innerFuture.complete(new Bar());
    assertTrue(fooResult.isDone());

    // futher asserts about fooResult here
}

W ten sposób, jeśli dodasz future.join() do doSomething (), test się nie powiedzie.

Jeśli Twoja usługa korzysta z ExecutorService, takiego jak w thenApplyAsync(..., executorService), następnie w testach wprowadź jednowątkowy ExecutorService, taki jak ten z guava:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Jeśli Twój kod używa forkJoinPool, np. thenApplyAsync(...), przepisz kod tak, aby używał ExecutorService( jest wiele dobrych powodów), lub użyj Awaitility.

Aby skrócić przykład, zrobiłem BarService metodą argument zaimplementowany jako Java8 lambda w teście, Zazwyczaj będzie to iniekcyjne odniesienie, które można wyśmiewać.

 1
Author: tkruse,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-06 07:27:16

Jeśli chcesz przetestować logikę po prostu nie testuj jej asynchronicznie.

Na przykład, aby przetestować ten kod, który działa na wynikach metody asynchronicznej.

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       
    }
}

W teście mock zależności z implementacją synchroniczną. Test jednostkowy jest całkowicie synchroniczny i działa w 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")));
    }
}

Nie testujesz zachowania asynchronicznego, ale możesz sprawdzić, czy logika jest poprawna.

 0
Author: Nils El-Himoud,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-29 08:18:29

To jest to, czego używam obecnie, jeśli wynik testu jest wytwarzany asynchronicznie.

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());
        }
    }
}

Używając importu statycznego, test czyta się całkiem nieźle. (uwaga, w tym przykładzie rozpoczynam wątek, aby zilustrować pomysł)

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

Jeśli f.complete nie zostanie wywołana, test zakończy się niepowodzeniem po pewnym czasie. Możesz również użyć f.completeExceptionally, aby zawieść wcześniej.

 0
Author: Jochen Bedersdorfer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-29 01:35:38