powermockito如何拦截新实例?

How powermockito intercept new instance?

我有两个 类 试图弄清楚 whenNew 是如何工作的。

public class RockService {
    public RockData serv() {
        RockData rockData = new RockData();
        rockData.setName("RockService");
        rockData.setContent("content from rock service");
        return rockData;
    } }

public class RockData {
    String name;
    long id;
    String content;
    // get set method ignored
}

有测试码

@RunWith(PowerMockRunner.class)
@PrepareForTest(RockService.class)
public class MockNewInstanceCreation {

    @Test
    public void mockCreationTest() throws Exception {
        RockData rockData = mock(RockData.class);

        when(rockData.getName()).thenReturn("this is mock");

        whenNew(RockData.class).withNoArguments().thenReturn(rockData);

        RockService rockService = new RockService();

        RockData servData = rockService.serv();
        System.out.println(servData.getName());
        System.out.println(servData.getContent());
    }
}

所以在运行时,如果不是 mock,输出 (RockData's getName()) 将是 "RockService"。但是对于 mock,它 returns "this is mock"。该代码有效,但我仍然不知道 Powermock/Mockito 究竟是如何做到这一点的。

我调试了代码。令我困惑的是RockData rockData = new RockData();执行后,实际创建的正是RockData rockData = mock(RockData.class);创建的实例。这意味着 new RockData() 根本不会创建新实例。它只是返回一个已经创建的实例。而且调试的时候跳到MockGateway.newInstanceCall.

那么Powermockito是如何拦截新实例的呢?

PowerMockRunner 使用特殊的 class 加载程序运行测试 - org.powermock.core.classloader.MockClassLoader

它没有加载真正的 class,而是加载了一个具有相同签名的新的。这意味着不会调用真正的构造函数。

所以new操作符返回的对象不是Mock。它是一个不同的 class 的实例,可以分配给真实的 returns 模拟值。

查看下面的代码:

public class RockService {

    ClassLoader classLoader = this.getClass().getClassLoader();

    System.out.println("Real construct");
    //Different class loader
    System.out.println(classLoader);
    //The same class
    System.out.println(this.getClass());
}

public RockData serv() {

    RockData rockData = new RockData();

    Class<? extends RockData> clazz = rockData.getClass();
    //This is a different class 
    System.out.println("Mocked class: " + clazz.getCanonicalName());
    //And different classloader
    ClassLoader classLoader = clazz.getClassLoader();
    System.out.println(classLoader);
    //Mocked class instance could be assigned to real one
    System.out.println(RockData.class.isAssignableFrom(clazz));

    //it's instance of both RockData.class and mocked class
    System.out.println(clazz.isInstance(rockData));
    System.out.println(RockData.class.isInstance(rockData));

    rockData.setName("RockService");
    rockData.setContent("content from rock service");
    return rockData;
}