Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I unit test asynchronous methods nicely?

I'm currently unit testing my asynchronous methods using thread locking, usually I inject a CountDownLatch into my asynchronous component and let the main thread wait for it to reach 0. However, this approach just looks plain ugly, and it doesn't scale well, consider what happens when I write 100+ tests for a component and they all sequentially have to wait for a worker thread to do some fake asynchronous job.

So is there another approach? Consider the following example for a simple search mechanism:

Searcher.java

public class Searcher {

    private SearcherListener listener;

    public void search(String input) {
        // Dispatch request to queue and notify listener when finished
    }

}

SearcherListener.java

public interface SearcherListener {

    public void searchFinished(String[] results);

}

How would you unit test the search method without using multiple threads and blocking one to wait for another? I've drawn inspiration from How to use Junit to test asynchronous processes but the top answer provides no concrete solution to how this would work.

like image 231
soren.qvist Avatar asked Oct 20 '22 13:10

soren.qvist


1 Answers

Writing unit test for async never looks nice.

It's necessary that the testMyAsyncMethod() (main thread) blocks until you are ready to check the correct behaviour. This is necessary because the test case terminates at the end of the method. So there is no way around, the question is only how you block.

A straightforward approach that does not influence much the productive code is to use a while loop: asume AsyncManager is the class under test:

ArrayList resultTarget = new ArrayList();
AsyncManager fixture = new AsyncManager(resultTarget);
fixture.startWork();
// now wait for result,  and avoid endless waiting
int numIter = 10;
// correct testcase expects two events in resultTarget
int expected = 2;
while (numIter > 0 && resulTarget.size() < expected) {
   Thread.sleep(100);
   numIter--;
}
assertEquals(expected, resulTarget.size());

productive code would use apropriate target in the constructor of AsyncManager or uses another constructor. For test purpose we can pass our test target.

You will write this only for inherent async tasks like your own message queue. for other code, only unitest the core part of the class that performs the calculation task, (a special algorithm, etc) you dont need to let it run in a thread.

However for your search listener the shown principle with loop and wait is appropriate.

public class SearchTest extends UnitTest implements SearchListener {
  public void searchFinished() {
     this.isSearchFinished = true;
  }

  public void testSearch1() {
    // Todo setup your search listener, and register this class to receive 
    Searcher searcher = new Searcher();
    searcher.setListener(this);
    // Todo setup thread
    searcherThread.search();
    asserTrue(checkSearchResult("myExpectedResult1"));

  }

  private boolean checkSearchResult(String expected) {
    boolean isOk = false;
    int numIter = 10;
    while (numIter > 0 && !this.isSearchFinished) {
       Thread.sleep(100);
       numIter--;
    }
    // todo somehow check that search was correct
    isOk = .....

    return isOk;
  }
}
like image 187
AlexWien Avatar answered Oct 23 '22 06:10

AlexWien