Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to mock method reference? #213

Open
dhoffer opened this issue Apr 12, 2018 · 8 comments
Open

How to mock method reference? #213

dhoffer opened this issue Apr 12, 2018 · 8 comments
Assignees

Comments

@dhoffer
Copy link

dhoffer commented Apr 12, 2018

How would I mock a JDK8 method reference? I want to know that the right method name was passed.

E.g. here is the method to test:

    public void initHandlers(Vertx vertx, OpenAPI3RouterFactory routerFactory) throws SQLException
    {
        super.initHandlers(vertx, routerFactory);

        routerFactory.addHandlerByOperationId(J_TASKER_START_RUN_ID, this::startRun);
        routerFactory.addFailureHandlerByOperationId(J_TASKER_START_RUN_ID, this::validationError);

        routerFactory.addHandlerByOperationId(J_TASKER_CANCEL_RUN_ID, this::cancelRun);
        routerFactory.addFailureHandlerByOperationId(J_TASKER_CANCEL_RUN_ID, this::validationError);

        routerFactory.addHandlerByOperationId(J_TASKER_RUN_STATUS_ID, this::runStatus);
        routerFactory.addFailureHandlerByOperationId(J_TASKER_RUN_STATUS_ID, this::validationError);
    }

Here is my current test but it's missing any real purpose because I can't figure out how to specify the correct method reference.

 @Test
    public void test_initHandlers() throws Exception
    {
        EasyMock.expect(routerFactory.addHandlerByOperationId(EasyMock.eq(J_TASKER_START_RUN_ID), EasyMock.anyObject())).andReturn(routerFactory);
        EasyMock.expect(routerFactory.addFailureHandlerByOperationId(EasyMock.eq(J_TASKER_START_RUN_ID), EasyMock.anyObject())).andReturn(routerFactory);

        EasyMock.expect(routerFactory.addHandlerByOperationId(EasyMock.eq(J_TASKER_CANCEL_RUN_ID), EasyMock.anyObject())).andReturn(routerFactory);
        EasyMock.expect(routerFactory.addFailureHandlerByOperationId(EasyMock.eq(J_TASKER_CANCEL_RUN_ID), EasyMock.anyObject())).andReturn(routerFactory);

        EasyMock.expect(routerFactory.addHandlerByOperationId(EasyMock.eq(J_TASKER_RUN_STATUS_ID), EasyMock.anyObject())).andReturn(routerFactory);
        EasyMock.expect(routerFactory.addFailureHandlerByOperationId(EasyMock.eq(J_TASKER_RUN_STATUS_ID), EasyMock.anyObject())).andReturn(routerFactory);

        replayAll();

        instance.initHandlers(vertx, routerFactory);

        verifyAll();
    }
@henri-tremblay
Copy link
Contributor

Method references are not always the same. So it is a good question. I will have to dig into it.

If the same method reference is passed it works.

public class MyTest {

    public int value() {
        return 42;
    }

    public int caller(IntSupplier i) {
        return i.getAsInt();
    }

    @Test
    public void test() {
        MyTest mock = mock(MyTest.class);
        IntSupplier supplier = this::value;
        expect(mock.caller(supplier)).andReturn(1);
        replay(mock);
        assertThat(mock.caller(supplier)).isEqualTo(1);
        verify(mock);
    }
}

@henri-tremblay henri-tremblay self-assigned this Apr 12, 2018
@dhoffer
Copy link
Author

dhoffer commented Apr 12, 2018

In my case I have 3 specific method references and then one general purpose one, I need to be sure each are set correctly.

@henri-tremblay
Copy link
Contributor

Can't you test that calling it gives the right behavior?

@dhoffer
Copy link
Author

dhoffer commented Apr 13, 2018

No, I have no idea how to specify the method reference.

I have tried a bunch of things like this:

`
@test
public void test_initHandlers() throws Exception
{
expect(routerFactory.addHandlerByOperationId(J_TASKER_START_RUN_ID, instance::startRun)).andReturn(routerFactory);
expect(routerFactory.addFailureHandlerByOperationId(J_TASKER_START_RUN_ID, instance::validationError)).andReturn(routerFactory);

    expect(routerFactory.addHandlerByOperationId(J_TASKER_CANCEL_RUN_ID, instance::cancelRun)).andReturn(routerFactory);
    expect(routerFactory.addFailureHandlerByOperationId(J_TASKER_CANCEL_RUN_ID, instance::validationError)).andReturn(routerFactory);

    expect(routerFactory.addHandlerByOperationId(J_TASKER_RUN_STATUS_ID, instance::runStatus)).andReturn(routerFactory);
    expect(routerFactory.addFailureHandlerByOperationId(J_TASKER_RUN_STATUS_ID, instance::validationError)).andReturn(routerFactory);

    replayAll();

    instance.initHandlers(vertx, routerFactory);

    verifyAll();
}

`

Where instance is the JTaskerHandler class instance under test.

But that fails with this:
java.lang.AssertionError:
Unexpected method call OpenAPI3RouterFactory.addHandlerByOperationId("JTasker_startRun", com.issinc.odin.services.handler.jtasker.JTaskerHandler$$Lambda$10/199657303@74bf1791):
OpenAPI3RouterFactory.addHandlerByOperationId("JTasker_startRun", com.issinc.odin.services.handler.jtasker.JTaskerHandlerTest$$Lambda$4/917768476@49c66ade): expected: 1, actual: 0

So it doesn't like that. I would be okay if it was just matching the 'name' of the method but I have no idea how to do that either.

@henri-tremblay
Copy link
Contributor

I've put a bunch of experts on the topic. So far the answer is: "Not possible". The method reference is transformed into a lambda which is a class of its own. And the name of the referenced method isn't kept apart in the bytecode of the core of the lambda. It seems to be a Java quirk.

@dhoffer
Copy link
Author

dhoffer commented Apr 13, 2018 via email

@henri-tremblay
Copy link
Contributor

It's not EasyMock. It's Java that doesn't allow it. No equals on method reference possible.

You can do two things:

  • A custom matcher that matches the result of the lambda
  • capture and test the result

Both already works.

public class MatchLambda {

    public int value() {
        return 1;
    }

    public int otherValue() {
        return 2;
    }

    public int foo(IntSupplier supplier) {
        return supplier.getAsInt();
    }

    @Test
    public void test() {
        MatchLambda mock = strictMock(MatchLambda.class);

        expect(mock.foo(customMatcher(1))).andReturn(10);
        expect(mock.foo(customMatcher(2))).andReturn(20);

        Capture<IntSupplier> capture = Capture.newInstance();

        expect(mock.foo(capture(capture))).andReturn(30);

        replay(mock);

        assertThat(mock.foo(this::value)).isEqualTo(10);
        assertThat(mock.foo(this::otherValue)).isEqualTo(20);

        assertThat(mock.foo(this::value)).isEqualTo(30);

        assertThat(capture.getValue().getAsInt()).isEqualTo(1);

        verify(mock);
    }

    private IntSupplier customMatcher(int value) {
        reportMatcher(new CustomMatcher(value));
        return null;
    }

    public class CustomMatcher implements IArgumentMatcher {

        private final int valueReturned;

        public CustomMatcher(int valueReturned) {
            this.valueReturned = valueReturned;
        }

        @Override
        public boolean matches(Object argument) {
            if(!(argument instanceof IntSupplier)) {
                return false;
            }
            IntSupplier supplier = (IntSupplier) argument;
            return supplier.getAsInt() == valueReturned;
        }

        @Override
        public void appendTo(StringBuffer buffer) {
            buffer.append("Wrong method reference: " + valueReturned);
        }
    }
}

@redgecase
Copy link

I had a scenario where I was passing a method reference to another method

My solution was as follows:

Set an expectation on the method you expect to pass

       Sheet mockSheet = EasyMock.createMock(Sheet.class);
       expect(readerService.checkSheetStructure(mockSheet)).andReturn(false);

Set the expectation on the method to which it is passed and capture the lambda

      Capture<Function<Sheet,Boolean>> checkSheetStructureCapture = new Capture<>();
        expect(readerService.processInputStreamAsWorkbook(eq(stream), capture(checkSheetStructureCapture), capture(stopIfCapture), eq(false)))
            .andReturn(false);

Invoke the tested method , which satisfies the second expectation.

        replay(upload,stream,readerService);
        boolean result = service.validateFileStructure(upload);

invoke the captured lambda to satisfy the first expectation and check the right method reference got passed.

        assertTrue(checkSheetStructureCapture.hasCaptured());
        checkSheetStructureCapture.getValue().apply(mockSheet);
        
        verify(upload,stream,readerService);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants