5

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.

5
  • What specifically are you trying to test? Searching, or your queuing mechanism? Commented Feb 21, 2014 at 23:06
  • Searching, more specifically, I need my search query to deliver an expected result. I want to "inject" a fake backend into my search component so that I can test how it responds to callbacks from an API server. Commented Feb 21, 2014 at 23:11
  • If you use mockito, you can create an Answer which sleeps before it returns the result; not sure if that helps you however? Commented Feb 21, 2014 at 23:38
  • @fge That sounds like blocking the main thread until the asynchronous task finishes, which I would like to avoid Commented Feb 21, 2014 at 23:44
  • What's wrong with using threads? Commented Feb 22, 2014 at 1:34

3 Answers 3

1

Another approach:

Just dont start the thread. thats all. Asume you have a SearcherService which uses your Searcher class. Then don't start the async SearcherService, instead just call searcher.search(), which blocks until search is finished.

Searcher s = new Searcher(); s.search(); // blocks and returns when finished // now somehow check the result 
Sign up to request clarification or add additional context in comments.

2 Comments

I'm not sure I understand this one, you are encapsulating the asynchronous behaviour and mocking it with a synchronous one here?
No there is no Mock, The productiv implemetation has a class SearcherService (or -Manager), which you use asynchronusly (Register you class to receive the result). This service internally uses a search engine (class Searcher), which works synchrnouse. The service provides the async access in productive code. For unit test you just test the internal Search engine, and asume that the async Servcie works (or was tested by my other answer)
1

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

2 Comments

feel free to refactor such that the loop is inside a helper method, where you pass the expected result into, that helper returns true or false.
Updated to have the helper method.
1

Create a synchronous version of the class that listens for its own results and uses an internal latch that search() waits on and searchFinished() clears. Like this:

public static class SynchronousSearcher implements SearcherListener { private CountDownLatch latch = new CountDownLatch(1); private String[] results; private class WaitingSearcher extends Searcher { @Override public void search(String input) { super.search(input); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public String[] search(String input) { WaitingSearcher searcher = new WaitingSearcher(); searcher.listener = this; searcher.search(input); return results; } @Override public void searchFinished(String[] results) { this.results = results; latch.countDown(); } } 

Then to use it, simply:

String[] results = new SynchronousSearcher().search("foo"); 

There are no threads, no wait loops and the method returns in the minimal possible time. It also doesn't matter if the search returns instantly - before the call to await() - because await() will immediately return if the latch is already at zero.

1 Comment

btw, it took me a couple of days of "background processing" to realise how this could neatly be done, hence the delay in answering

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.