How to use JUnit to test asynchronous processes

前端 未结 18 1591
小蘑菇
小蘑菇 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:41

    Start the process off and wait for the result using a Future.

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

    For all Spring users out there, this is how I usually do my integration tests nowadays, where async behaviour is involved:

    Fire an application event in production code, when an async task (such as an I/O call) has finished. Most of the time this event is necessary anyway to handle the response of the async operation in production.

    With this event in place, you can then use the following strategy in your test case:

    1. Execute the system under test
    2. Listen for the event and make sure that the event has fired
    3. Do your assertions

    To break this down, you'll first need some kind of domain event to fire. I'm using a UUID here to identify the task that has completed, but you're of course free to use something else as long as it's unique.

    (Note, that the following code snippets also use Lombok annotations to get rid of boiler plate code)

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

    The production code itself then typically looks like this:

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

    I can then use a Spring @EventListener to catch the published event in test code. The event listener is a little bit more involved, because it has to handle two cases in a thread safe manner:

    1. Production code is faster than the test case and the event has already fired before the test case checks for the event, or
    2. Test case is faster than production code and the test case has to wait for the event.

    A CountDownLatch is used for the second case as mentioned in other answers here. Also note, that the @Order annotation on the event handler method makes sure, that this event handler method gets called after any other event listeners used in production.

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

    Last step is to execute the system under test in a test case. I'm using a SpringBoot test with JUnit 5 here, but this should work the same for all tests using a Spring context.

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

    Note, that in contrast to other answers here, this solution will also work if you execute your tests in parallel and multiple threads exercise the async code at the same time.

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

    You can try using the Awaitility library. It makes it easy to test the systems you're talking about.

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

    Let's say you have this code:

    public void method() {
            CompletableFuture.runAsync(() -> {
                //logic
                //logic
                //logic
                //logic
            });
        }
    

    Try to refactor it to something like this:

    public void refactoredMethod() {
        CompletableFuture.runAsync(this::subMethod);
    }
    
    private void subMethod() {
        //logic
        //logic
        //logic
        //logic
    }
    

    After that, test the subMethod this way:

    org.powermock.reflect.Whitebox.invokeMethod(classInstance, "subMethod"); 
    

    This isn't a perfect solution, but it tests all the logic inside your async execution.

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

    An alternative is to use the CountDownLatch class.

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

    NOTE you can't just used syncronized with a regular object as a lock, as fast callbacks can release the lock before the lock's wait method is called. See this blog post by Joe Walnes.

    EDIT Removed syncronized blocks around CountDownLatch thanks to comments from @jtahlborn and @Ring

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

    How about calling SomeObject.wait and notifyAll as described here OR using Robotiums Solo.waitForCondition(...) method OR use a class i wrote to do this (see comments and test class for how to use)

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