使用 EasyMock 测试多线程 (CompletableFuture)

test multithreading (CompletableFuture) with EasyMock

我想为包含 CompletableFuture 的方法添加测试:

 public void report(List<String> srcList) {
        if (srcList != null) {
            ...
            CompletableFuture.runAsync(() ->
               ....
               srcList.forEach(src-> downloader.send(url)));
        }
 }

我想测试一下,那个方法 send 被调用了。我的测试看起来像:

 @Test
 public void _test() {
        List<String> events = new ArrayList();
        events.add("http://xxxx//");
        events.add("http://xxxx//");

        expect(downloader.send(events.get(0))).andReturn("xxx").times(2);
        replay(downloader);
        eventReporter.report(events);

        verify(downloader);
 }

我收到这样的错误 Downloader.send("http://xxxx//"): expected: 2, actual: 0

避免此错误的一种方法是设置 Thread.sleep(100); 超时。然后线程将等待并验证该方法是否已调用。但这会增加测试时间。

还有其他方法可以使用 EasyMock 测试多线程吗?

使用 Thread.sleep() 方法对异步代码进行单元测试是一种不好的做法 因为即使它能正常工作,测试也会不稳定和闪烁(运行 3 次,2 次通过,1 次失败) 如果你设置了大量的睡眠时间并且像这样编写很少的测试你会遇到大量的执行时间 那可能会超过几十秒。要完成此任务,您需要解耦异步部分 你们中的代码来自同步。如何操作的示例:

class Service {

    private Downloader downloader;
    private ExecutorService service;

    public Service (Downloader downloader, ExecutorService service) {
        //set variables
    }

    public void doWork(List<String> list) {
        for (String item : list) {
            service.submit(() -> {
                downloader.download(item);
            });
        }
    }
}

ExecutorService 是接口,我们需要使我们的服务同步

class SycnronousService impliments ExecutorService {

    //methods empty implementations

    public void submit(Runnable runnable) {
        runnable.run(); //run immediately
    }

    //methods empty implementations
}

public class ServiceTest {

    public void shouldPassAllItemsToDownloader() {
        Downloader mockDownloader = AnyMockFramework.mockIt();
        Service service = new Service(mockDownloader, new SycnronousService());
        List<String> tasks = Arrays.asList("A", "B");
        service.doWork(tasks);
        verify(mockDownloader).download("A"); //verify in your way with EasyMock
        verify(mockDownloader).download("B"); //verify in your way with EasyMock
        // no more Timer.sleep() , test runs immeadetely  
    }

}

您需要将 CompletableFuture 替换为我示例中的内容,因为 无法以这种方式对该代码进行单元测试。 稍后在您的应用程序中,您将能够将 SycnronousService 替换为异步实现,并且一切都将按预期工作。

我同意@joy-dir 的回答。你可能应该按照她所说的来简化你的测试。

为了完整起见,您的问题是 verify 在您的任务实际完成之前被调用。您可以做很多事情。

一个是在 verify.

上循环
@Test
public void test() throws Exception {
    List<String> events = new ArrayList();
    events.add("http://xxxx//");
    events.add("http://xxxx//");

    expect(downloader.send(events.get(0))).andReturn("xxx").times(2);
    replay(downloader);
    report(events);

    for (int i = 0; i < 10; i++) {
        try {
            verify(downloader);
            return;
        } catch(AssertionError e) {
            // wait until it works
        }
        Thread.sleep(10);
    }
    verify(downloader);
}

成功后不会白睡很久。但是,您确实需要确保等待足够长的时间以防止测试不稳定。

另一个解决方案实际上是使用runAsync返回的CompletableFuture。我更喜欢这个解决方案。

public CompletableFuture<Void> report(List<String> srcList) {
    if (srcList != null) {
        return CompletableFuture.runAsync(() -> srcList.forEach(src-> downloader.send(src)));
    }
    return CompletableFuture.completedFuture(null);
}

@Test
public void test2() throws Exception {
    List<String> events = new ArrayList();
    events.add("http://xxxx//");
    events.add("http://xxxx//");

    expect(downloader.send(events.get(0))).andReturn("xxx").times(2);
    replay(downloader);
    CompletableFuture<Void> future = report(events);

    future.get(100, TimeUnit.MILLISECONDS);

    verify(downloader);
}

最后,还有一个hackish的方法。你问公共池是否完成了。这是 hackish,因为其他东西可能会使用它。所以它很可爱,但我真的不推荐它。

@Test
public void test3() throws Exception {
    List<String> events = new ArrayList();
    events.add("http://xxxx//");
    events.add("http://xxxx//");

    expect(downloader.send(events.get(0))).andReturn("xxx").times(2);
    replay(downloader);
    report(events);

    while(!ForkJoinPool.commonPool().isQuiescent()) {
        Thread.sleep(10);
    }

    verify(downloader);
}