模拟一个 ObjectInputStream

Mocking an ObjectInputStream

当我尝试模拟 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 对象作为构造函数参数传递,我敢打赌这就是问题所在 - 没有为 ObjectInputStream 分配 InputStream。

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

您不能模拟 final 方法,因为它不可覆盖并且 readObject()final :

public final Object readObject(){...}

为了实现您的目标,您可以重构您的实际代码。
例如,您可以引入一个包装器 class 来保存一个 ObjectInputStream 实例并将处理委托给它。
通过这种方式,您可以模拟此包装器 class.
readObject() 方法 您也可以使用比 Mockito 提供更多功能的 Powermock,但我真诚地避免使用它。

这是由于readObject()final造成的。

Mockito 无法模拟 final 方法。所以发生的事情是 指定 调用模拟对象的尝试……完全出错了。是的,抛出 NPE 的事实不仅仅是误导。

您可以尝试使用 Mockito 2 中允许模拟 final 方法的 experimental 功能。如果这对您不起作用,PowerMock(ito) 或 JMockit 很可能会完成这项工作。

郑重声明:这些微妙的问题是不再使用 Java 内置序列化的另一个好理由。

绕过这个的唯一其他方法:针对相应的 接口 ObjectInput,而不是特定的实现 class.

进行编程

因为您可以轻松地模拟 界面

当然,这意味着更改您的生产代码。这可能不是一个坏主意,因为它 将您的业务逻辑与 实际 序列化形式分离。如果您稍后选择序列化为 JSON 字符串,使用 GSON - 您只需替换该接口的实现即可。

正如其他人所述,readObject()ObjectInputStream 中的 final

我感谢其他人确实建议 PowerMock 强制模拟这个 class!

更好的解决方案是遵循 针对接口编程 模式。方法 readObject() 在由 ObjectInputStream 实现的接口 ObjectInput 中声明,因此您可以更改 class 的签名以使用接口 ObjectInput 而不是具体的classObjectInputStream

模拟接口 ObjectInput 是小菜一碟...


could you add an MCVE of how this is meant to look like? Nobody did this so far. – 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");
    }   
}

my particular problem is with writeObject(). Specifically verify(serverInstance).ObjectOutPutStreamToClient.writeObject(someValue); Do you have a nifty solution for this as well? I recon this is harder because writeObject is void. – 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");
    }   
}