首页 > 解决方案 > 对线程应用程序进行单元测试

问题描述

我正在考虑如何使用 mockito 编写测试用例。

例如,我在主线程中的部分逻辑是创建一个执行 3 件事的线程。请在下面查看我的注释代码。

现在 RequestThread 可以根据来自主程序的输入数量多次生成。

public class MainThreads {
    public static void main(String[] args) {
        RequestThread rt = new RequestThread("sample");
        rt.start();

        //RequestThread another = new RequestThread("sample-2");
        //another.start();

        //RequestThread newThread = new RequestThread("sample-3");
        //newThread.start();
    }

    public static class RequestThread implements Runnable{
        private final String request;

        public RequestThread(String request) {
            this.request = request;
        }

        @Override
        public void run() {
            //1. Instantiate a service passing the required request parameter
            MyDataWebService service = new MyDataWebService(request);

            //2. Get the returned data
            List<String> dataList = service.requestData();

            //3. Write to file
            Path file = Paths.get("/someDir/" + request);
            Files.write(file, dataList, Charset.forName("UTF-8"));
        }

    }
}

我的问题是,我无法弄清楚如何为线程类正确编写 JUnit/Mockito 测试。一般来说,我对 Mockito 和 JUnit 不太了解,所以我正在寻找一种对线程应用程序进行单元测试的方法。

有人可以指导我如何对这样的东西进行单元测试吗?

标签: javajunitmockito

解决方案


您需要对代码进行一些更改,以使其更易于测试。尤其是:

  • 你想模拟的对象应该实现一个接口
  • 不要在要测试的函数中实例化要模拟的对象

这是对类的重写,以便您可以模拟MyDataWebService和测试RequestThread。基于此示例,您将能够更轻松地为MainThreads该类编写完整的测试。

public class MainThreads {
    public static void main(String[] args) {
        RequestThread rt = new RequestThread("sample");
        rt.start();

        //RequestThread another = new RequestThread("sample-2");
        //another.start();

        //RequestThread newThread = new RequestThread("sample-3");
        //newThread.start();
    }

    public static class RequestThread extends Thread {
        private final String request;
        // One important thing to note here, "service" has to be non-final. Else mockito won't be able to inject the mock.
        private MyDataWebServiceInterface service;

        public RequestThread(String request) {
            this.request = request;
            //1. Instantiate a service passing the required request parameter
            // => do it in constructor, or passed as parameter, but NOT in the function to test
            service = new MyDataWebService(request);
        }

        @Override
        public void run() {
            //2. Get the returned data
            List<String> dataList = service.requestData();

            //3. Write to file
            Path file = Paths.get("someDir/" + request);
            try {
                Files.write(file, dataList, Charset.forName("UTF-8"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

接口和实现MyDataWebService

interface MyDataWebServiceInterface {
    List<String> requestData();
}

class MyDataWebService implements MyDataWebServiceInterface {
    public MyDataWebService(String request) {
    }

    @Override
    public List<String> requestData() {
        return Arrays.asList("foo", "bar");
    }
}

并使用mockito. 请注意,检查现有文件和线程休眠可能不是这里最优雅的事情。如果您可以负担得起添加一些标记RequestThread以表明数据已被写入,那肯定会使测试更好、更安全(文件系统 i/o 有时很难测试)。

@RunWith(MockitoJUnitRunner.class)
public class RequestThreadTest {

    private static final Path FILE = Paths.get("someDir", "sample");

    @Mock
    MyDataWebServiceInterface service;

    @InjectMocks
    MainThreads.RequestThread reqThread = new MainThreads.RequestThread("sample");

    @Before
    public void setup() throws IOException, InterruptedException {
        if (Files.exists(FILE)) {
            Files.delete(FILE);
            while (Files.exists(FILE)) {
                Thread.sleep(50);
            }
        }
    }

    @Test
    public void shouldWriteFile() throws InterruptedException {
        Mockito.when(service.requestData()).thenReturn(Arrays.asList("one", "two"));
        reqThread.start();
        while (!Files.exists(FILE)) {
            Thread.sleep(50);
        }
        // HERE run assertions about file content
    }
}

现在,测试异步代码通常比同步代码更复杂,因为您经常会遇到非确定性行为、时间问题等。您可能希望在测试中设置超时,但请记住:持续集成工具(jenkins、travis 等)通常会比您的机器运行得慢,这是导致问题的常见原因,因此请不要将其设置得太紧。据我所知,对于非确定性问题没有“万能”的解决方案。

Martin Fowler 有一篇关于测试中非确定性的优秀文章:https ://martinfowler.com/articles/nonDeterminism.html


推荐阅读