【问题标题】:How to modify a static private field during tests?如何在测试期间修改静态私有字段?
【发布时间】:2018-12-09 15:59:33
【问题描述】:

我的项目使用JUnitMockitoPowerMockito 创建单元测试。代码如下:

public class FirstController {
    public void doSomething() {
        ServiceExecutor.execute();
    }
}

public class ServiceExecutor {

    private static final List<Service> services = Arrays.asList(
        new Service1(),
        new Service2(),
        ...
    );

    public static void execute() {
        for (Service s : services) {
            s.execute();
        }
    }   
}

@RunWith(PowerMockRunner.class)
@PrepareForTest({ServiceExecutor.class})
public class FirstControllerTest {

        @Before
        public void prepareForTest() {
            PowerMockito.mockStatic(ServiceExecutor.class);
            PowerMockito.doNothing().when(ServiceExecutor.class)
        }

        @Test
        public void doSomethingTest() {
            FirstController firstController = new FirstController();
            firstController.doSomething();
            PowerMockito.verifyStatic(ServiceExecutor.class, Mockito.times(1));
        }   
    }

本期完整源码:https://github.com/gpcodervn/Java-Tutorial/tree/master/UnitTest

我想验证运行的ServiceExecutor.execute() 方法。

当调用execute() 方法时,我尝试模拟ServiceExecutordoNothing()。但我对ServiceExecutor 中的private static final List&lt;Service&gt; services 有疑问。它总是为每个服务构造新的实例。每个服务创建新实例的时间更长,如果我模拟每个Service,我不知道他们以后会有多少服务。

你有什么想法在FirstController 中验证ServiceExecutor.execute() 而不在ServiceExecutor 中运行任何方法吗?

【问题讨论】:

  • 如果您要创建ServiceExecutor 的实例,为什么首先需要将execute 方法设为static
  • 谢谢,@QBrute。这是我的错误。我已经更新了代码。
  • 老实说,在考虑测试之前重构以避免全局状态。 (或者,不要使用全局状态开头。)
  • 没有办法。因为这个服务是由其他项目创建的。在我的控制器中,我想测试这个方法,我必须模拟这个服务。
  • 添加您目前编写的测试代码。列出的相关问题没有帮助吗?

标签: java junit mockito powermock


【解决方案1】:

正如 cmets 所表达的,“真正的”解决方案是更改设计以使其更易于测试。但考虑到您的限制,这里有一个替代想法,可能也可以。

你看,你正在使用

填写你的列表
private static final List<Service> services = Arrays.asList(...)

因此,理论上,您可以使用 PowerMock(ito) 来使用 Arrays.asList() 作为您接管控制的点。换句话说:您可以让asList() 返回您想要用于测试的任何列表!

当然,更好的方法是用可以注入的东西替换静态列表,比如

private final ServicesProvider serviceProvider; 

你有一个独特的类,可以给你这样一个列表。您可以自行测试该类,然后使用普通的 Mockito 将模拟的服务提供者放入您的测试代码中。

【讨论】:

  • 谢谢,@GhostCat。您关于使用 PowerMockito 模拟 Arrays.asList() 的想法,我之前已经尝试过。但它也不起作用,请看一下:github.com/gpcodervn/Java-Tutorial/blob/master/UnitTest/src/…
  • 关于在可以注入的东西中替换静态列表,我不能修改任何代码,因为它依赖于其他项目和其他团队。所以,我想找到模拟静态最终字段的方法。
  • 我明白这一点。但也应该为未来的读者写下答案。不是每个人都受到同样的限制!如前所述,对于您的情况,您可以尝试控制 Arrays.toList()。与干扰对new ServiceX() 的多次调用相比,这会容易得多!当然,这种方法将您与正在使用的 Arrays.asList() 联系起来。
  • 请查看上面评论中的链接。它不起作用,我的代码有什么错误吗?
  • 更新:请理解这要求有点多。链接到一些外部网站,并给出“不工作”作为问题描述在这里真的不是一个好的起点。
【解决方案2】:

所以你知道如何模拟 ServiceExecutor.execute,但你不想模拟它。您想在测试中执行它,但不运行测试中的所有 service.execute() 方法。那不是对 FirstController 的测试,而是对 ServiceExecutor 的测试。所以你可以减少你的问题。

您可以使用反射来更改测试中私有静态字段 ServiceExecutor.services 的值,如下所述:Change private static final field using Java reflection

public class ServiceExecutorTest {

    @Test
    public void doSomethingTest() throws NoSuchFieldException, IllegalAccessException {
        Field field = null;
        List<Service> oldList = null;
        try {
            field = ServiceExecutor.class.getDeclaredField("services");
            field.setAccessible(true);
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

            final Service serviceMock1 = mock(Service.class);
            final Service serviceMock2 = mock(Service.class);
            final List<Service> serviceMockList = Arrays.asList(serviceMock1, serviceMock2);
            oldList = (List<Service>) field.get(null);
            field.set(null, serviceMockList);
            ServiceExecutor.execute();
            // or testing the controller
            // FirstController firstController = new FirstController();
            // firstController.doSomething();
            verify(serviceMock1, times(1)).execute();
            verify(serviceMock2, times(1)).execute();
        } finally {
            // restore original value
            if (field != null && oldList != null) {
                field.set(null, oldList);
            }
        }
    }

    static class Service {
        void execute() {
            throw new RuntimeException("Should not execute");
        }
    }

    static class ServiceExecutor {

        private static final List<Service> services = Arrays.asList(
                new Service());

        public static void execute() {
            for (Service s : services) {
                s.execute();
            }
        }
    }
}

【讨论】:

  • 感谢您的支持。我真的很想验证运行的 ServiceExecutor.execute() 方法,我上面的代码示例是一个错误。我已经运行了你的代码,但我的问题仍然存在同样的问题。由于私有静态 final 字段,每个服务的实例也会被调用。
  • 感谢您抽出宝贵时间@tkruse。然而,这并不像我预期的那样。您的代码是为 ServiceExecutor 类编写的,我想测试 FirstController 类。此外,您可以在Service类的构造函数中添加输出日志,您会看到它也调用了真正的构造函数。即使您可以模拟此服务,也无法满足我的问题“每个服务创建新实例的时间更长,如果我模拟每个服务,我不知道他们以后会有多少服务。”
  • 让大家更容易正确理解我的代码。我已经在 GitHub 上更新了源代码。请看一下这个问题。这是您的代码:github.com/gpcodervn/Java-Tutorial/blob/master/UnitTest/src/….
  • 在您的第一条评论中您说:“我真的很想验证运行的 ServiceExecutor.execute() 方法,我上面的代码示例是一个错误。”现在你说“我想测试 FirstController 类”。下定决心。我认为如果不执行静态变量的初始化程序就不可能加载 ServiceExecutorClass,因此当您在任何地方导入 ServiceExecutor 类时,无论如何都会调用构造函数。
  • 也许我们误解了,作为问题部分,我已经为FirstController创建了一个测试类,而不是ServiceExecutor。感谢您的宝贵时间。
【解决方案3】:

我发现解决方案使用@SuppressStaticInitializationFor 注释。

使用这个注解来抑制静态初始化器(构造器) 一门或多门课。

之所以需要注解是因为我们需要 在加载时知道该类的静态构造函数是否执行 是否应该跳过。不幸的是,我们不能通过课程 注释的 value 参数(从而获得类型安全的值) 因为这样会在 PowerMock 之前加载该类 抑制了它的构造函数。

https://github.com/powermock/powermock/wiki/Suppress-Unwanted-Behavior

最终代码:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ ServiceExecutor.class })
@SuppressStaticInitializationFor("com.gpcoder.staticblock.ServiceExecutor")
public class FirstControllerTest {

    @Before
    public void prepareForTest() throws Exception {
        PowerMockito.mockStatic(ServiceExecutor.class);
        PowerMockito.doNothing().when(ServiceExecutor.class);
    }

    @Test
    public void doSomethingTest() {
        FirstController firstController = new FirstController();
        firstController.doSomething();
        PowerMockito.verifyStatic(ServiceExecutor.class, Mockito.times(1));
    }
}

【讨论】:

    猜你喜欢
    • 2013-05-01
    • 1970-01-01
    • 2017-01-29
    • 1970-01-01
    • 2020-11-12
    • 1970-01-01
    • 2012-09-29
    • 1970-01-01
    • 2015-04-12
    相关资源
    最近更新 更多