How to use JUnit to test asynchronous processes

前端 未结 18 1590
小蘑菇
小蘑菇 2020-11-29 15:06

How do you test methods that fire asynchronous processes with JUnit?

I don\'t know how to make my test wait for the process to end (it is not exactly a unit test, it

相关标签:
18条回答
  • 2020-11-29 15:32

    I find an library socket.io to test asynchronous logic. It looks simple and brief way using LinkedBlockingQueue. Here is example:

        @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. And set timeout to avoid assuming too much time to wait the result.

    0 讨论(0)
  • 2020-11-29 15:33

    I prefer use wait and notify. It is simple and clear.

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

    Basically, we need to create a final Array reference, to be used inside of anonymous inner class. I would rather create a boolean[], because I can put a value to control if we need to wait(). When everything is done, we just release the asyncExecuted.

    0 讨论(0)
  • 2020-11-29 15:35

    If you use a CompletableFuture (introduced in Java 8) or a SettableFuture (from Google Guava), you can make your test finish as soon as it's done, rather than waiting a pre-set amount of time. Your test would look something like this:

    CompletableFuture<String> future = new CompletableFuture<>();
    executorService.submit(new Runnable() {         
        @Override
        public void run() {
            future.complete("Hello World!");                
        }
    });
    assertEquals("Hello World!", future.get());
    
    0 讨论(0)
  • 2020-11-29 15:37

    One method I've found pretty useful for testing asynchronous methods is injecting an Executor instance in the object-to-test's constructor. In production, the executor instance is configured to run asynchronously while in test it can be mocked to run synchronously.

    So suppose I'm trying to test the asynchronous method 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);
          }
        });
      }
    }
    

    In production, I would construct Foo with an Executors.newSingleThreadExecutor() Executor instance while in test I would probably construct it with a synchronous executor that does the following --

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

    Now my JUnit test of the asynchronous method is pretty clean --

    @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());
    }
    
    0 讨论(0)
  • 2020-11-29 15:38

    Avoid testing with parallel threads whenever you can (which is most of the time). This will only make your tests flaky (sometimes pass, sometimes fail).

    Only when you need to call some other library / system, you might have to wait on other threads, in that case always use the Awaitility library instead of Thread.sleep().

    Never just call get() or join() in your tests, else your tests might run forever on your CI server in case the future never completes. Always assert isDone() first in your tests before calling get(). For CompletionStage, that is .toCompletableFuture().isDone().

    When you test a non-blocking method like this:

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

    then you should not just test the result by passing a completed Future in the test, you should also make sure that your method doSomething() does not block by calling join() or get(). This is important in particular if you use a non-blocking framework.

    To do that, test with a non-completed future that you set to completed manually:

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

    That way, if you add future.join() to doSomething(), the test will fail.

    If your Service uses an ExecutorService such as in thenApplyAsync(..., executorService), then in your tests inject a single-threaded ExecutorService, such as the one from guava:

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    

    If your code uses the forkJoinPool such as thenApplyAsync(...), rewrite the code to use an ExecutorService (there are many good reasons), or use Awaitility.

    To shorten the example, I made BarService a method argument implemented as a Java8 lambda in the test, typically it would be an injected reference that you would mock.

    0 讨论(0)
  • 2020-11-29 15:40

    There's nothing inherently wrong with testing threaded/async code, particularly if threading is the point of the code you're testing. The general approach to testing this stuff is to:

    • Block the main test thread
    • Capture failed assertions from other threads
    • Unblock the main test thread
    • Rethrow any failures

    But that's a lot of boilerplate for one test. A better/simpler approach is to just use ConcurrentUnit:

      final Waiter waiter = new Waiter();
    
      new Thread(() -> {
        doSomeWork();
        waiter.assertTrue(true);
        waiter.resume();
      }).start();
    
      // Wait for resume() to be called
      waiter.await(1000);
    

    The benefit of this over the CountdownLatch approach is that it's less verbose since assertion failures that occur in any thread are properly reported to the main thread, meaning the test fails when it should. A writeup that compares the CountdownLatch approach to ConcurrentUnit is here.

    I also wrote a blog post on the topic for those who want to learn a bit more detail.

    0 讨论(0)
提交回复
热议问题