简单的单元测试使用模拟一直失败;模拟对象没有传递给类?

huangapple 未分类评论57阅读模式
英文:

Simple Unit test with Mocks keeps failing; mock object not being passed to class?

问题

以下是翻译好的部分:

我有一个简单的单元测试失败了。希望我能够用简单的术语解释一下,因为我已经看了好几个小时,我知道问题在哪里,但是我对 Mock 的基本理论不太熟悉,所以我有点困惑,无法修复它。我会简要总结一下问题,然后将代码粘贴在下面。

基本上,在我的测试方法 getAllValidModelsTest() 中,它使用一个循环来遍历对象类型 DeviceModel 的枚举值。只有 5 个:[EX3400_24P, EX4300_32F, EX4300_48MP, SRX_345, FAUX]。

所以在循环内部,在 Assert 语句(Junit)之前,它对 getDevice(deviceId) 进行了静态方法调用,然后从那里返回一个 Device 对象。在 getAllValidModelsTest() 方法的循环内部,第一行将 elementMock 对象模拟为返回当前正在枚举的 model,该模型是从在枚举 DeviceModel 类上调用 .values() 返回的 DeviceModels[] 数组中获取的。

所以我的问题是,在循环的第二次迭代中(从 1 开始计数),断言失败了,因为 DeviceModel[] 数组中的第 0 个元素显然是 EX4300_32F,但是在 @Before 注释的 setUp 方法中,它被模拟为返回 EX3400_24P。但奇怪的是,在 getAllValidModelsTest() 方法内的循环内部,当在 elementMock 对象上调用 .getModel 时,它再次被 重写/模拟 为返回当前正在迭代的模型,因此它应该返回 相同 的值……

这是 SwitchDeviceFactoryTest.java 类的结构(包含单元测试的类):

@PowerMockIgnore({"javax.net.ssl.*"})
@RunWith(PowerMockRunner.class)
@PrepareForTest({DataGatewayFactory.class, SwitchConfig.class, RouterConfig.class})
public class SwitchDeviceFactoryTest {
    // ...(其他变量和方法)
}

这是我的测试方法之前运行的 setUp 方法。唯一重要的变量应该是 elementMock 对象,特别是被模拟为返回 EX3400_24P 对象的对象:

@Before
public void setup() throws Exception {
    // ...(其他设置)
    Mockito.when(elementMock.getModel()).thenReturn(DeviceModel.EX3400_24P.getModel());
    // ...(其他设置)
}

测试方法如下:

@Test
public void getAllValidModelsTest() throws Exception {
    for (DeviceModel model: DeviceModel.values()) {
        // ...(其他设置)
        Device device = DeviceFactory.getDevice(deviceId);
        assertEquals(model, device.getModel());
    }
}

这段代码不合理的地方是,我在重构代码时,只改变了 2 行代码(elementCrud 和 elementMock 的 .doReturn 和 .when 调用),在 develop 分支上运行得非常完美。

当我进行调试时,我可以看到在循环的第二次迭代中,.getModel 在 static getDevice 方法内返回 EX3400_24P 对象,但它应该返回 model.getModel(),这是在 DeviceModels 的 .values() 枚举数组上正在迭代的第二个对象…因此应该是 EX4300_32F。

在 develop 分支上,这个代码运行得非常完美…就好像 Mockito 模拟对象在进入 DeviceFactory 类的 getDevice 方法内部时,忘记了它应该做什么,一旦在我的 getAllValidModelsTest() 方法中调用它(即 Device device = DeviceFactory.getDevice(deviceId);)时。

这是来自 DeviceFactory 类的 .getDevice 方法:

public static Device getDevice(String serialNumber) throws Exception {
    // ...(其他设置)
    DeviceModel model = DeviceModel.valueOfLabel(element.getModel()); // 此处返回错误的模型...第二次迭代时它返回 EX3400_24P
    // ...(其他设置)
}

我确实在 setUp() 方法中注释掉/删除了模拟返回 EX3400_24P 的代码段,但是此时测试会在此处抛出一个空指针异常。

.getModel 方法如何知道在进入 DeviceFactory.java 类之前,返回我在之前的类(SwitchDeviceFactoryTest.java)中模拟返回的值?如果我没有将它作为变量传递给 getDevice() 方法,它是如何记住的?

我是否需要使用 PowerMock 或者其他方法,因为这是一个静态方法?这会如何改变情况?

请帮忙解决!

英文:

I have a simple Unit Test that is failing. Hopefully I can explain this in simple terms as I've been looking at it for hours and I see what the issue is, but I am not too familiar the underlying theory behind Mocks so I am a bit confused and cannot fix it. I will summarize the issue very quickly and then paste the code below.

Basically, in my test method called getAllValidModelsTest(), it uses a for loop to iterate thru enum values of object type DeviceModel. There are only 5: [EX3400_24P, EX4300_32F, EX4300_48MP, SRX_345, FAUX].

So inside the for loop, before the Assert statement (Junit), it makes a static method call to getDevice(deviceId) and it should from there return a Device object. The first line under the for loop in the getAllValidModelsTest() mocks the elementMock object to return the current model that is being iterated over in the DeviceModels[] array that was returned from the .values() call on the enums DeviceModel class.

So my issue is, when it jumps in the 2nd iteration in my for loop (counting from 1), the Assert fails , because the 0th element in the DeviceModel[] array is obviously EX4300_32F, but in the @Before setUp annotation it is being mocked to return EX3400_24P. But the weird thing is, under the for loop inside the getAllValidModelsTest() method, it is being overridden/mocked again to return to the current model that is being iterated through when .getModel is called on the elementMock object, so it should be returning the SAME value...

This is how the class SwitchDeviceFactoryTest.java is constructed (the class with the Unit Test):

    @PowerMockIgnore({"javax.net.ssl.*"})
    @RunWith(PowerMockRunner.class)
    @PrepareForTest({DataGatewayFactory.class, SwitchConfig.class, RouterConfig.class})
    public class SwitchDeviceFactoryTest {
       
        String deviceId = "testdevice";
        String ip = "1.1.1.1";
        DataGateway dbMock = Mockito.mock(DataGateway.class);
        SwitchConfig swConfigMock = PowerMockito.mock(SwitchConfig.class);
        RouterConfig routerConfigMock = PowerMockito.mock(RouterConfig.class);
        TransportDeviceSecretsInfo secrets = new TransportDeviceSecretsInfo();
        TransportDeviceSecretsData secretsData = new TransportDeviceSecretsData("root","rootPw", "sshUser", "sshPass", "snmpAuthPass", "snmpPrivPass");
        IElement elementMock = Mockito.mock(IElement.class);
    
        ITransportDeviceSecretsCrud transportDeviceSecretsCrud = mock(ITransportDeviceSecretsCrud.class);
        ISwitchConfigCrud switchConfigCrud = mock(ISwitchConfigCrud.class);
        IRouterConfigCrud routerConfigCrud = mock(IRouterConfigCrud.class);
        IElementCrud elementCrud = mock(IElementCrud.class);

This is my setUp method that runs before the test. The only variables that should be of importance are the elementMock object, specifically the one being mocked to return the EX3400_24P object:

@Before
public void setup() throws Exception {
    secrets.setSecretsData(secretsData);
    PowerMockito.mockStatic(DataGatewayFactory.class);
    Mockito.when(DataGatewayFactory.getInstance()).thenReturn(dbMock);
    Mockito.when(dbMock.getTransportDeviceSecretsCrud()).thenReturn(transportDeviceSecretsCrud);
    Mockito.when(transportDeviceSecretsCrud.getServerSecretsInfo(anyString())).thenReturn(Optional.of(secrets));
    Mockito.when(transportDeviceSecretsCrud.getReportedSecretsInfo(anyString())).thenReturn(Optional.of(secrets));

    when(dbMock.getElementCrud()).thenReturn(elementCrud);
    doReturn(Optional.of(elementMock)).when(elementCrud).getById(anyString());
    Mockito.when(elementMock.getModel()).thenReturn(DeviceModel.EX3400_24P.getModel());

    Mockito.when(elementMock.getType()).thenReturn(ElementType.SWITCH);
    Mockito.when(dbMock.getSwitchConfigCrud()).thenReturn(switchConfigCrud);
    Mockito.when(switchConfigCrud.get(anyString())).thenReturn(Optional.of(swConfigMock));
    Mockito.when(swConfigMock.getIp()).thenReturn(ip);
    Mockito.when(dbMock.getRouterConfigCrud()).thenReturn(routerConfigCrud);
    Mockito.when(routerConfigCrud.get(anyString())).thenReturn(Optional.of(routerConfigMock));
    Mockito.when(routerConfigMock.getIp()).thenReturn(ip);

And the test method:

@Test
public void getAllValidModelsTest() throws Exception {
    for (DeviceModel model: DeviceModel.values()) {
        when(elementMock.getModel()).thenReturn(model.getModel());
        if (model == DeviceModel.SRX_345)
            when(elementMock.getType()).thenReturn(ElementType.ROUTER);
        else
            when(elementMock.getType()).thenReturn(ElementType.SWITCH);
        Device device = DeviceFactory.getDevice(deviceId);
        assertEquals(model, device.getModel());
    }
}

The thing that doesn't make sense, is I was refactoring code, and only changed 2 lines (the elementCrud and elementMock .doReturn and .when calls) and it works perfectly fine on the develop branch.

When I debug, I can see that on the 2nd iteration of the for loop, .getModel returns EX3400_24P object inside the static getDevice method, when it should be returning model.getModel() , which would be the 2nd object being iterated on in the .values() enum array of DeviceModels... so it should be EX4300_32F.

On the develop branch, this works perfectly.... It's as if the Mockito mock object forgets what it's suppose to do when it jumps inside the DeviceFactory class inside the getDevice method once its called in my getAllValidModelsTest() method (i.e. Device device = DeviceFactory.getDevice(deviceId);)

Here is the .getDevice method from the DeviceFactory class:

public static Device getDevice(String serialNumber) throws Exception {
    IElement element = dataGateway.getElementCrud().getById(serialNumber).get();
    DeviceModel model = DeviceModel.valueOfLabel(element.getModel()); // right here is where it returns the wrong model... it returns EX3400_24P on the 2nd iteration
    log.info("Found device {} in database", serialNumber);

    if (serialNumber.startsWith(FakeDevicePrefix.ATGTEST.toString()) || serialNumber.startsWith(FakeDevicePrefix.FAKE.toString())) {
        log.info("Detected FAKE/ATG serial number. Using FAUX device.");
        model = DeviceModel.FAUX;
    }

    switch (element.getType()) {
        case SWITCH:
            SwitchConfig config = dataGateway.getSwitchConfigCrud().get(serialNumber).get();
            return getDevice(serialNumber, config.getIp(), model);
        case ROUTER:
            RouterConfig rconfig = dataGateway.getRouterConfigCrud().get(serialNumber).get();
            return getDevice(serialNumber, rconfig.getIp(), DeviceModel.SRX_345);
        case PTP:
        default:
            log.warn("Unsupported device type {}", element.getType().toString());
            throw new Exception("Unsupported device type " + element.getType().toString());
    }
}

I did indeed comment out/remove the piece of code that mocks it to return EX3400_24P in the setUp() method with @Before annotation , but the tests fails with a NULL POINTER EXCEPTION at this point.

How does the .getModel method know to return what I mocked it to return in the previous class (SwitchDeviceFactoryTest.java) before it jumps into the DeviceFactory.java class? How does it remember that if I'm not passing it in as a variable into the getDevice() method?

Do I need to use PowerMock or something because this is a static method? How does this change anything?

Please help!

huangapple
  • 本文由 发表于 2020年3月15日 09:07:10
  • 转载请务必保留本文链接:https://java.coder-hub.com/60688821.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定