【问题标题】:Unit testing an executor callable对可执行程序调用进行单元测试
【发布时间】:2018-07-30 04:19:28
【问题描述】:

我有一个像这样的控制器类,它将一些执行程序传递给一个可运行的实例。 这不是它的工作原理,只是为了简单起见,我这样做了。

package com.test.executor;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Controller {
    private ExecutorService executor;

    public Controller() {
        executor = Executors.newCachedThreadPool();
    }

    public void doRun() {
        MyRunner runner = new MyRunner(executor);
        Thread myRunner = new Thread(runner);
        myRunner.start();
    }

    public static void main(String[] args) {
        new Controller().doRun();
    }
}

运行器接收执行器的实例,然后传递某些可调用对象。 现在,callables 有点多样化,因为一些 callables 访问数据库/调用一些 web 服务/文件系统

我在如何为这种情况下编写合适的 JUnit 时遇到了一些问题。 我希望有尽可能多的代码覆盖率。

package com.test.executor;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class MyRunner implements Runnable {

    private ExecutorService executor;

    public MyRunner(ExecutorService executor) {
        this.executor = executor;
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        Future<Boolean> ret1 = executor.submit(new SampleCall1());

        try {
            ret1.get(5, TimeUnit.MINUTES);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            e.printStackTrace();
        }

        // Do other things

        Future<List<String>> ret2 = executor.submit(new SampleCall2());

        try {
            ret2.get(5, TimeUnit.MINUTES);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            e.printStackTrace();
        }

        // Do other things

        Future<Long> ret3 = executor.submit(new SampleCall3());

        try {
            ret3.get(5, TimeUnit.MINUTES);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            e.printStackTrace();
        }
    }

    public static class SampleCall1 implements Callable<Boolean> {

        @Override
        public Boolean call() throws Exception {
            // Sample Return Only
            // This will call JSON web service
            return true;
        }

    }

    public static class SampleCall2 implements Callable<List<String>> {

        @Override
        public List<String> call() throws Exception {
            // Sample Return Only
            // This will call Database
            return Collections.EMPTY_LIST;
        }

    }

    public static class SampleCall3 implements Callable<Long> {

        @Override
        public Long call() throws Exception {
            // Sample Return Only
            // This will access some file system
            return 1L;
        }

    }

}

我的问题是为此编写单元测试的正确方法是什么。我正在收集一些关于如何对这个类进行单元测试的建议? 我不确定在我的 junit/mockito 实例中要模拟什么。 我应该模拟每个可调用对象吗?然后为 MyRunner 创建一个测试用例。

我担心依赖性..因为我正在连接到数据库/Web 服务/和文件系统,所以我想寻求一些建议。

更新 2

package com.test.executor;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class MyRunner implements Runnable {

    private ExecutorService executor;

    public MyRunner(ExecutorService executor) {
        this.executor = executor;
    }

    @Override
    public void run() {
        executeTasks1();
        // Do other things

        executeTasks2();
        // Do other things


        executeTasks3();

    }

    public boolean executeTasks1(){
        // TODO Auto-generated method stub
        Future<Boolean> ret1 = executor.submit(new SampleCall1());

        try {
            return ret1.get(5, TimeUnit.MINUTES);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            e.printStackTrace();
        }

    }

    public List<String> executeTasks2(){
        Future<List<String>> ret2 = executor.submit(new SampleCall2());

        try {
            return ret2.get(5, TimeUnit.MINUTES);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            e.printStackTrace();
        }
    }

    public Long executeTasks3(){
        Future<Long> ret3 = executor.submit(new SampleCall3());

        try {
            return ret3.get(5, TimeUnit.MINUTES);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            e.printStackTrace();
        }
    }

    public static class SampleCall1 implements Callable<Boolean> {

        @Override
        public Boolean call() throws Exception {
            // Sample Return Only
            // This will call JSON web service
            return true;
        }

    }

    public static class SampleCall2 implements Callable<List<String>> {

        @Override
        public List<String> call() throws Exception {
            // Sample Return Only
            // This will call Database
            return Collections.EMPTY_LIST;
        }

    }

    public static class SampleCall3 implements Callable<Long> {

        @Override
        public Long call() throws Exception {
            // Sample Return Only
            // This will access some file system
            return 1L;
        }

    }

}

【问题讨论】:

  • 只需单独对 Callables 进行单元测试,并用适当的“答案”模拟执行器来测试“MyRunner”的逻辑。
  • 我不明白你的问题:可调用和执行器的整个概念是将可调用的智能与执行调度逻辑分开。现在,当 unit 测试时,您只需要测试您对接口的期望。 TL;DR 单元独立测试可调用对象,UT 你的控制器,不要 UT 执行器,因为那是 Spring 代码,不要编写混合所有内容的测试,这不是单一的。

标签: java unit-testing junit mockito


【解决方案1】:

不要在代码覆盖上花费太多时间。以某种方式务实。测试什么是值得测试的。提高你的测试质量,而不是数量。关于指标的一些旁注:100% 的线路覆盖率并不像高分支覆盖率那样重要,获得高分支覆盖率可能会花费你很多时间,即使这样:你可能不需要走所有可能的路线。 PIT (mutation testing) 是一个有用的“工具”,它通过更改被测代码来检查您的测试实际上有多好。但是使用该信息作为指导,而不是作为实施更多测试的措施。不要夸大其词! (也许如果你正在开发一个图书馆,你不想再经常改变或者你想让它坚如磐石(并且你有空闲时间),你可以,但否则我不会)

有一段时间我非常准确地测试了所有内容。问题:一旦发生变化(例如:今天我们需要 X,明天是 Y),您的许多测试就会失败。那时需要大量的返工。如果您测试最合适的,一旦需求发生巨大变化,您就可以专注于重要的事情,这将花费更少的时间。

查看您提供的代码,我可能会为每个Callable-实现编写一个测试(类),以确保它们执行我想要的操作(这可能会导致提取Callable-类作为副作用)。对于控制器和跑步者,我还不太确定……MyRunner 对我来说似乎已经过时了……但也许不是。所以可能我只会测试控制器...

关于模拟:我开始尽可能省略模拟。大多数时候我还会添加集成测试,我想知道系统作为一个整体是如何运作的,以及它是否按预期工作。我见过很多测试,其中有很多模拟,通过更改任何代码,没有单元测试失败,尽管我认为有些测试应该失败。我还看到很多单元测试,实际上单元测试看起来像集成测试,但到处都是模拟。模拟首先有什么帮助?单独设置模拟可能需要太多时间。但这又是我的看法。所以即使很多人喜欢测试金字塔有一个广泛的单元测试底部,我认为它更实用,并将一些测试移动到集成测试“层”而不是使用大量的模拟。最后,这也是可用性和速度的问题。您希望您的测试能够快速给出结果,但您仍然希望结果具有最大价值(模拟只给出部分结果)。

【讨论】:

  • 我遇到的代码覆盖率问题是,我们需要在测试中拥有高达 80% 的代码覆盖率。因此,如果我一一测试可调用对象,然后进行大量模拟......我的代码覆盖率真的下降了。有没有一种简化的模式可以让我的代码更易于测试?
  • 谁想要“80% 的代码覆盖率”?那有什么意思?线路覆盖?分支覆盖率?
  • @MarkEstrada 你的覆盖率不应该下降,你只是有更多更小的测试类。任何被嘲笑的东西都应该在单独的测试中进行测试。
  • 我在一些项目中强制要求 80% 的代码覆盖率......但是,达到这些的测试质量不高,导致通过测试,其中有明显的错误......不要'不为数字而奔波...为质量而奔波,但也要:务实;-)
  • 只是架构师对单元测试的要求......当单元测试sut类时。
【解决方案2】:

编写一个巨大的MyRunnableTest 测试类与编写一个巨大的MyRunnable 生产类一样有代码味道。由于您的每个Callables 都是多样化的并且访问不同的 I/O 资源,因此您希望分别测试它们中的每一个。实际的方法例如应根据具体情况选择文件系统操作的单元测试或与嵌入式 H2 数据库的集成测试。

提取Callables 应该会留下一个较小的MyRunnable 类。您现在可以将run() 拆分为更小的方法或类并分别测试它们。此时你可以模拟ExecutorService

您应该在 Controller 类的测试中测试 ExecutorService 对象,并在其中创建实际对象。

Coverage 只是一种帮助您衡量测试质量的工具。高覆盖率本身并不是目标。如果你把它发挥到极致,你可以很容易地获得 100% 的代码覆盖率,并且在你的测试中没有一个断言。

您可以应用Test Driven Development 等其他技术和PIT mutation testing 等工具来改进您的测试。但是,您应该首先让您的生产代码易于测试。

【讨论】:

  • 看看我的 MyRunner 课程..你认为它做了很多事情吗?我想不出一种更可测试的方法,因为逻辑就是这样。我使用可调用对象来分隔每个任务。
  • 我太慢了...但我觉得我的答案的某些部分也与您的相匹配很好 ;-)
  • 我什至问过自己,为什么你需要MyRunner。对每个可调用对象进行测试的结果也是,如果它们的大小增加,您可能会开始提取它们。
  • @Roland @MarkEstrada 我可能会将这些可调用对象转换为常规 DAO。 ExecutorService 应该注入 DAO,而不是 Controller。这将简化MyRunnable,因为它不必处理将被DAO隐藏的异步语义(理想情况下是某种扩展的AbstractAsyncDao)。
  • DAO?这是什么意思?
猜你喜欢
  • 2010-12-26
  • 1970-01-01
  • 2012-03-21
  • 2011-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-01
  • 2010-10-30
相关资源
最近更新 更多