模拟 ObjectInputStream

3

当我尝试模拟ObjectInputStream对象时,我遇到了NullPointerException的错误。

更精确地说,当调用以下代码时:

when(inputStream.readObject()).thenReturn(new Person("Doe", "John", "123"));

代码片段:

@RunWith(MockitoJUnitRunner.class)
public class ConnectionTest{
  ...
  @Mock
  private ObjectInputStream inputStream;
  ...
  @Test
  public void test(){
   ...
    when(inputStream.readObject()).thenReturn(new Person("Doe", "John", "123"));
  ...
  }
}

通常在初始化ObjectInputStream时,您需要将一个InputStream对象作为构造函数参数传递。我猜这就是问题所在 - 没有InputStream被分配给ObjectInputStream。

那么我应该如何正确地模拟ObjectInputStream呢?


下次请提供一个最小可复现代码示例(MCVE)。仅通过在问题中省略...并让thenReturn返回一个简单的字符串,例如-您将允许读者只需复制/粘贴您的代码即可重现。不要在示例代码中放置任何阻止用户运行它的内容-特别是当要求解决“这些代码行不起作用”的问题时。 - GhostCat
感谢您的快速接受。记录一下:除了使用其他模拟框架(我实际上建议不要这样做) - 请参阅我的更新答案,以获取另一种方法。如果您需要进一步帮助,请随时提问(今天我就到这里)。 - GhostCat
现在一切都好了。感谢你的帮助。 - sidzej
3个回答

4
这是由于readObject()final的原因所致。
Mockito无法模拟final方法。因此,试图在模拟对象上指定该调用的尝试......只会出错。是的,抛出NPE的事实更加令人误解。
您可以尝试使用Mockito 2中允许模拟final方法的那个实验性特性。如果这对您不起作用,则PowerMock(ito)或JMockit最有可能能够完成任务。
只是为了记录一下:这种微妙的问题是不再使用Java内置序列化的又一个好理由。
唯一的其他绕过方法:针对相应的接口ObjectInput进行编程,而不是具体的实现类。
因为您可以轻松地模拟该interface
但是,这当然意味着更改您的生产代码。这可能不是一个坏主意,因为它将您的业务逻辑与序列化的实际形式分离。如果您以后选择将其序列化为JSON字符串,并使用GSON,则只需替换该接口的实现即可。

3
正如其他人所说,readObject()ObjectInputStream 中是 final 的。
我感谢其他人建议使用 PowerMock 来强制模拟这个类!
更好的解决方案是遵循 “针对接口编程” 模式。方法 readObject() 声明在 ObjectInput 接口中,由 ObjectInputStream 实现。因此,您可以更改类的签名,使用接口 ObjectInput 而不是具体类 ObjectInputStream
模拟接口 ObjectInput 很容易...

你能提供一个完整的示例吗?到目前为止,没有人这样做。 - Tobias Kolb

这就是它应该看起来的样子:

生产代码:

public class Person {
    public Person(String string, String string2, String string3) {
    }
}

class ClassUnderTest {
    private final ObjectInput objectInput;

    public ClassUnderTest(ObjectInput inputStream) {
        objectInput = inputStream;
    }

    public Person readFromObjectStreamAsSideEffect() {
        try {
            return (Person) objectInput.readObject();
        } catch (ClassNotFoundException | IOException e) {
            throw new RuntimeException("some meaningful explanation.", e);
        }
    }
}

测试代码

@ExtendWith(MockitoExtension.class) // allows for other runner...
public class ConnectionTest {

    @Mock
    ObjectInput inputStream;

    // @InjectMocks
    // compiler will NOT complain if constructor arguments are missing, so I discourage this.

    ClassUnderTest cut; // do not initialize here, mock is still NULL.

    @BeforeEach
    private void setup() {
        cut = new ClassUnderTest(inputStream);
    }

    @Test
    public void getPreparedObjectFromInputStreamy() throws Exception {
        Person preparedValueObject = new Person("Doe", "John", "123");
        when(inputStream.readObject()).thenReturn(preparedValueObject);

        Person result = cut.readFromObjectStreamAsSideEffect();

        assertEquals(preparedValueObject, result, "hint for reason of failing");
    }   
}

我的问题是关于writeObject()。具体来说,verify(serverInstance).ObjectOutPutStreamToClient.writeObject(someValue)。你有没有一个好的解决方案?我认为这更难,因为writeObjectvoid。 - Tobias Kolb

不是我,而是Mockito有一个解决方案:

生产代码

public class Person {
    public Person(String string, String string2, String string3) {
    }
    // having toString() too improves fail message of test.
}

class ClassUnderTest {
    private final ObjectOutput objectOutput;

    public ClassUnderTest(ObjectOutput objectOutputStream) {
        objectOutput = objectOutputStream;
    }

    public void writeObjects(List<Person> persons) {
        try {
            for (Person person : persons) {
                objectOutput.writeObject(person);
            }
        } catch (IOException e) {
            throw new RuntimeException("some meaningfull explanation.", e);
        }
    }
}

测试代码

@ExtendWith(MockitoExtension.class)
public class ConnectionTest {

    @Mock
    ObjectOutput outputStream;

    ClassUnderTest cut;

    @BeforeEach
    private void setup() {
        cut = new ClassUnderTest(outputStream);
    }

    @Test
    public void getPreparedObjectFromInputStreamy() throws Exception {
        List<Person> listToWrite = Arrays.asList(//
                new Person("Doe", "John", "123"),
                new Person("Doe", "Jane", "456"));

        cut.writeObjects(listToWrite);

        ArgumentCaptor<Person> passedArgument = ArgumentCaptor.forClass(Person.class);
        verify(outputStream, times(listToWrite.size())).writeObject(passedArgument.capture());
        assertTrue(passedArgument.getAllValues().contains(listToWrite.get(0)), "hint for reason of failing");
        assertTrue(passedArgument.getAllValues().contains(listToWrite.get(1)), "hint for reason of failing");
    }   
}

1
啊,有一个接口可以做到那个。我忘了! - GhostCat
1
@TobiasKolb:完成了。顺便说一下,你的回复是我列表上的第1,111个... ;o) - Timothy Truckle
1
@TobiasKolb:你可能已经在这里添加了自己的问题... - Timothy Truckle
我是否必须在类的构造函数中传递OOS才能模拟它?因为我不需要它,我更愿意在被测试的类(Server)中初始化它,因为我在其他地方都不需要它。 - blkpingu
请问如何模拟ObjectOutputStream.writeObject方法?@TimothyTruckle - blkpingu
显示剩余3条评论

1
您无法模拟一个final方法,因为它是不可覆盖的,而readObject()final的:
public final Object readObject(){...}

为了实现你的目标,你可以重构你现有的代码。
例如,你可以引入一个包装类,它持有一个ObjectInputStream实例并将处理委托给它。
这样,你就可以模拟这个包装类的readObject()方法。
你也可以使用Powermock,它比Mockito提供更多的功能,但我真诚地避免使用它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接