mockito icon indicating copy to clipboard operation
mockito copied to clipboard

Add `withMocks` to mimic Kotlin's `also` usage

Open l0rinc opened this issue 3 years ago • 3 comments
trafficstars

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);
    });
}

l0rinc avatar Aug 13 '22 09:08 l0rinc

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?

TimvdLippe avatar Aug 13 '22 11:08 TimvdLippe

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 :)

l0rinc avatar Aug 14 '22 07:08 l0rinc

@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;
}

l0rinc avatar Aug 20 '22 15:08 l0rinc

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 😄

TimvdLippe avatar Oct 22 '22 12:10 TimvdLippe