【问题标题】:Unit testing a Threaded Application对线程应用程序进行单元测试
【发布时间】:2019-01-01 19:06:45
【问题描述】:

我正在考虑如何使用 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 的理解不是很好,所以我正在寻找一种单元测试的方法 线程应用程序。

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

【问题讨论】:

  • 你能模拟出线程吗?也可以考虑使用线程池?
  • 这实际上是我的问题......我不知道如何模拟我的请求。我没有使用线程池,所以让我们说我只会产生一个线程
  • 你确实意识到,你的代码现在不是多线程的,你应该使用 start() 方法而不是 run() 方法。
  • @HimanshuBhardwaj 哦对不起..我只是输入所有内容...有很多代码正在进行...所以我删除了不相关的部分...谢谢您指出这一点
  • 是否可以对此类线程进行单元测试?所以那个线程做了很多事情......我在 Mockito 上找不到关于如何单元测试或模拟这样的事情的方法。有什么提示吗?

标签: java junit mockito


【解决方案1】:

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

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

这是对类的重写,以便您可以模拟 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

【讨论】:

  • 我希望我可以保存此回复,以便以后参考。或者,如果做不到这一点,在某些人的额头上纹身(颠倒以便他们可以在镜子里看到)......:D
【解决方案2】:

一个独特的非答案:在 2018 年,您不再使用“原始”线程。

Java 现在提供了更好的抽象,例如ExecutorService。猜猜看:当您将代码提交 tasks 到这样的服务时,您可能可以使用same-thread executor 服务对其进行测试。

意义:通过使用此类抽象并将您的交付分解为特定服务,您可能(几乎)不仅可以全面测试小单元,还可以测试任务如何进入您的系统并进行处理。

换句话说:你对你的“任务”进行单元测试,然后当任务进入这样的执行器时,你对任务的集成进行“单元”测试。然后你只剩下一点真正的功能/集成测试来检查“真正的并行”解决方案是否符合预期。

其他任何事情都会很快变得复杂。在普通单元测试中使用真正的线程可能会导致不一致的行为,或者增加运行时间(例如等待线程异步执行某项操作的测试)。

在您的示例中:您的测试将简单地坐在那里并定期检查预期的文件是否包含预期的内容。导致:在失败之前应该等待多长时间?等待的时间不够长意味着您的测试偶尔会失败,因为代码有时只需要更长的时间。如果您等待的时间过长,就会增加运行测试所需的总时间。您不希望最终完成数百个单元测试,因为“等待其他线程”需要 10、20 秒。

【讨论】:

  • 我没有诚实地使用执行器服务,但我会看看重构我的代码。但是对于我的单元测试学习,是不是不能对这种情况进行单元测试呢?我的意思是我目前的实现。谢谢。
猜你喜欢
  • 2010-09-11
  • 1970-01-01
  • 2012-03-21
  • 2011-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多