mockito
mockito copied to clipboard
Add `withMocks` to mimic Kotlin's `also` usage
When we're mocking a single method from a class, the instantiation and definitions aren't fully encapsulated (i.e. the when method could accept every previous mocked type (i.e. we have to make sure we're writing when(originalMock.getText()) and not when(transformedMock.getText())), the mock class creation and method return definitions aren't grouped):
@MethodSource
@ParameterizedTest
void validate_setTransformed(String result, String originalGetText) {
var originalMock = mock(JTextField.class);
var transformedMock = mock(JTextField.class);
when(originalMock.getText()).thenReturn(originalGetText);
newCaseConverter(originalMock, transformedMock).setTransformed();
verify(transformedMock).setText(result);
}
with the following trivial helper we could emulate https://kotlinlang.org/docs/scope-functions.html#also:
public static <T> T withMock(Class<T> classToMock, Consumer<T> consumer) {
T mock = mock(classToMock);
consumer.accept(mock);
return mock;
}
and the usage could look like this:
@MethodSource
@ParameterizedTest
void validate_setTransformed(String result, String originalGetText) {
var originalMock = withMock(JTextField.class, it -> when(it.getText()).thenReturn(originalGetText));
withMock(JTextField.class, transformedMock -> {
newCaseConverter(originalMock, transformedMock).setTransformed();
verify(transformedMock).setText(result);
});
}
I am not sure to what extent this is an extensible pattern or specific to some use cases. Ideally, our API should be composable by others and I feel like this particular method would fit a specific project, but not the general userbase of Mockito. The pattern works well for single-method mocks, but for multi-method it can already become a bit trickier, especially as the lambda argument would have a different name compared to the variable. This would make code traversal more difficult, as you know have two names referring to the same mock.
Would it be better if you define this helper in your project and reuse it there?
Ideally, our API should be composable
Indeed, in the above example the newCaseConverter needed 2 parameters, which can be composed either as written above (showcasing the one-liner or the nested approach) or...
for multi-method it can already become a bit trickier
Not necessarily, we could always use the Groovy/Kotlin default lambda parameter name of it or nest the declarations:
withMock(JTextField.class, originalMock -> {
when(originalMock.getText()).thenReturn(originalGetText));
withMock(JTextField.class, transformedMock -> {
newCaseConverter(originalMock, transformedMock).setTransformed();
verify(transformedMock).setText(result);
});
});
or if we'd need the result of the method call, we can follow Kotlin's https://kotlinlang.org/docs/scope-functions.html#let and hide the method parameter setup from the rest of the method:
var caseConverter = withMock(JTextField.class, originalMock -> {
when(originalMock.getText()).thenReturn(originalGetText);
return withMock(JTextField.class, transformedMock -> {
var result = newCaseConverter(originalMock, transformedMock);
result.setTransformed();
verify(transformedMock).setText(expected);
return result;
});
});
System.out.println(caseConverter); // originalMock & transformedMock parameters not visible
and
public static <T, R> R withMock(Class<T> classToMock, Function<T, R> consumer) {
T mock = mock(classToMock);
return consumer.apply(mock);
}
public static <T> T withMock(Class<T> classToMock, Consumer<T> consumer) {
T mock = mock(classToMock);
consumer.accept(mock);
return mock;
}
Kotlin saw value in these helpers and I miss being able to group the mockable parts (can't just extract to methods since we often need multiple return values), I think it would simplify some setups. I'm already using it in my project, through I'll share :)
@TimvdLippe, it seems that https://stackoverflow.com/a/26319364 breaks this pattern in some cases anyway - so while the following works:
return mockWith(Connection.class, _mockConnection -> {
var mockPreparedStatement = mockWith(PreparedStatement.class, _mockPreparedStatement -> {
var mockResultSet = mockWith(ResultSet.class, _mockResultSet -> {
when(_mockResultSet.next()).thenReturn(true);
when(_mockResultSet.getString(eq(1))).thenReturn(name);
when(_mockResultSet.getString(eq(2))).thenReturn(occupation);
});
when(_mockPreparedStatement.executeQuery()).thenReturn(mockResultSet);
});
when(_mockConnection.prepareStatement(eq("SELECT name, occupation FROM people WHERE name = ?"))).thenReturn(mockPreparedStatement);
});
inlining the values to have a hierarchical modelling breaks Mockito with Unfinished stubbing detected probably because of the above issue:
return mockWith(Connection.class, mockConnection ->
when(mockConnection.prepareStatement(eq("SELECT name, occupation FROM people WHERE name = ?")))
.thenReturn(mockWith(PreparedStatement.class, mockPreparedStatement ->
when(mockPreparedStatement.executeQuery())
.thenReturn(mockWith(ResultSet.class, mockResultSet -> {
when(mockResultSet.next()).thenReturn(true);
when(mockResultSet.getString(eq(1))).thenReturn(name);
when(mockResultSet.getString(eq(2))).thenReturn(occupation);
})))));
ps. was using mockWith as:
public static <T> T mockWith(Class<T> classToMock, ThrowingConsumer<T> consumer) {
T mock = mock(classToMock);
consumer.accept(mock);
return mock;
}
public interface ThrowingConsumer<T> extends Consumer<T> {
@Override
default void accept(T elem) {
try {
acceptThrows(elem);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
void acceptThrows(T elem) throws Exception;
}
Given the above and the pitfalls that are associated to it, I think it is safer to not ship this API. Still, if you would like to use this in your personal project, I do recommend it 😄