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

ArgumentMatcher like assertArg() for multiple different calls? #3307

Open
Iconjurer opened this issue Mar 26, 2024 Discussed in #3170 · 3 comments
Open

ArgumentMatcher like assertArg() for multiple different calls? #3307

Iconjurer opened this issue Mar 26, 2024 Discussed in #3170 · 3 comments

Comments

@Iconjurer
Copy link

Discussed in #3170

Originally posted by jpgoelz November 8, 2023
When verifying multiple different calls to a method of a mock, it is possible to call verify() twice for each invocation.

This will work when not using matchers:

    @Test
    @DisplayName("verify two different calls")
    void verifyTwoDifferentCalls() {
        someMock.someMethod(1, "any string");
        someMock.someMethod(2, "any string");

        verify(someMock).someMethod(1, "any string");
        verify(someMock).someMethod(2, "any string");
    }

This will also work when using simple matchers:

    @Test
    @DisplayName("verify two different calls with eq()")
    void verifyTwoDifferentCallsWithEq() {
        someMock.someMethod(1, "any string");
        someMock.someMethod(2, "any string");

        verify(someMock).someMethod(eq(1), anyString());
        verify(someMock).someMethod(eq(2), anyString());
    }

However, it does not work when using assertArg():

    @Test
    @DisplayName("verify two different calls with asserArg()")
    void verifyTwoDifferentCallsWithAsserArg() {
        someMock.someMethod(1, "any string");
        someMock.someMethod(2, "any string");

        verify(someMock).someMethod(
            assertArg(someInt -> assertThat(someInt).isEqualTo(1)),
            anyString()
        );
        verify(someMock).someMethod(
            assertArg(someInt -> assertThat(someInt).isEqualTo(2)),
            anyString()
        );
    }

The reason behind this is probably the inner assertion failing on at least one of the calls.
image

So far, the only way I know how to test something like this is to use ArgumentCaptors:

    @Test
    @DisplayName("verify two different calls with ArgumentCaptor")
    void verifyTwoDifferentCallsWithArgumentCaptor() {
        someMock.someMethod(1, "any string");
        someMock.someMethod(2, "any string");

        final ArgumentCaptor<Integer> someIntCaptor = ArgumentCaptor.forClass(Integer.class);

        verify(someMock, times(2)).someMethod(someIntCaptor.capture(), anyString());

        assertThat(someIntCaptor.getAllValues()).containsExactly(1, 2);
    }

Now, my question: Is there any way to do this without using an ArgumentCaptor?

@hajubal
Copy link

hajubal commented Mar 28, 2024

In the code above, the 'someMock.someMethod' function is called twice with different arguments.
The two verify() functions written in the verification code will be called twice.

verify(someMock).someMethod(
    assertArg(someInt -> assertThat(someInt).isEqualTo(1)),
    anyString()
);

In the above code, assert false is thrown in assertThat() when called with '2' entered as the first parameter. I think it's a question of usage.

@mmakos
Copy link

mmakos commented Mar 28, 2024

There is one way but it is not very elegant, so just treat it as a fun fact 🙂

You can make some static utility method similar to assertArg:

public static <T> T assertArgs(Consumer<T>... assertions) {
  AtomicInteger counter = new AtomicInteger();
  return ArgumentMatchers.argThat(actual -> {
    int index = counter.getAndIncrement();
    if (assertions.length > index) {
      assertions[index].accept(actual);
    }
    return true;
  });
}

Then in your test you can write:

@Test
void verifyTwoDifferentCallsWithAssertArgs() {
  someMock.someMethod(1, "any string");
  someMock.someMethod(2, "any string");

  verify(someMock, times(2)).someMethod(
          assertArgs(
                  firstInt -> assertThat(firstInt).isEqualTo(1),
                  secondInt -> assertThat(secondInt).isEqualTo(2)),
          anyString());
}

There is probably one more better way to do it, but I didn't check it yet: you could try to handle you assertion in assertArgs so it doesn't break your test, but returns true/false + assertion fail description. Then if verification fails, you have to pass assertion fail description to verification fail message (this can be probably done by properly implementing Matcher or something).

@Iconjurer
Copy link
Author

Iconjurer commented Mar 28, 2024

Expected behaviour should be consistent with the other argument matchers. In the simplest case that means it should not fail if there is at least a single matching verification. In assertArg(...)'s case this would mean there should be at least a single verification that does not throw a throwable of some sort. (junit's AssertionFailedError extends Error, not exception)

@Test
void assertArgShouldNotFailExample() {
    Consumer<Integer> mock = mock();
    mock.accept(1);
    mock.accept(2);
    
    verify(mock).accept(assertArg(i -> assertThat(i).isEqualTo(2)));
}

Because the below two examples do not fail either:

@Test
void verifyExampleA() {
    Consumer<Integer> mock = mock();
    mock.accept(1);
    mock.accept(2);

    verify(mock).accept(argThat(i -> i == 2));
}
@Test
void verifyExampleB() {
    Consumer<Integer> mock = mock();
    mock.accept(1);
    mock.accept(2);

    verify(mock).accept(eq(2));
}

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