如何在测试期间修改静态私有字段?

How to modify a static private field during tests?

我的项目使用 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<Service> services 有问题。它总是为每个服务构造新实例。每个服务创建新实例的时间都更长,我不知道如果我模拟每个 Service.

他们以后会有多少服务

你有没有想法在 FirstController 中验证 ServiceExecutor.execute() 而没有 ServiceExecutor 中的任何方法?

所以你知道如何模拟 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();
            }
        }
    }
}

如评论中所述,"real" 解决方案是更改设计以使其更易于测试。但考虑到您的限制,这里有一个替代想法 可能 也可以工作。

你看,你正在使用

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

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

当然,更好的方法可能是在可以注入的内容中替换该静态列表,例如

private final ServicesProvider serviceProvider; 

你有一个独特的 class 可以给你这样一个列表。您可以自己测试 class,然后使用普通 Mockito 将模拟的服务提供者添加到您的被测代码中。

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

Use this annotation to suppress static initializers (constructors) for one or more classes.

The reason why an annotation is needed for this is because we need to know at load-time if the static constructor execution for this class should be skipped or not. Unfortunately, we cannot pass the class as the value parameter to the annotation (and thus get type-safe values) because then the class would be loaded before PowerMock could have suppressed its constructor.

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));
    }
}