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

(Argument)Matchers regression from 1.10.19 to 2.18.3 for varargs #1498

Closed
ViToni opened this issue Sep 24, 2018 · 1 comment · Fixed by #2807
Closed

(Argument)Matchers regression from 1.10.19 to 2.18.3 for varargs #1498

ViToni opened this issue Sep 24, 2018 · 1 comment · Fixed by #2807

Comments

@ViToni
Copy link

ViToni commented Sep 24, 2018

While updating from 1.10.19 to 2.18.3 I have a regression regarding Matchers and varargs.
Passing tests now fail because the Matchers don't work anymore in the verify step.

Below is a reduced test case to reprocude the issue. Version 2.22.0 and ArgumentMatcher were also tested with the same (failing) result.

Error:

Argument(s) are different! Wanted:
spyableLogger.debug(
	<any string>,
	<any java.lang.Object[]>
);
-> at tests.MockitoVarargsTest.testLoggerGetsCalled_Matchers(MockitoVarargsTest.java:29)
Actual invocation has different arguments:
spyableLogger.debug(
	"Something happened",
	java.lang.RuntimeException: Oops
);
-> at tests.MockitoVarargsTest.callLoggerDebug(MockitoVarargsTest.java:59)

	at tests.MockitoVarargsTest.testLoggerGetsCalled_Matchers(MockitoVarargsTest.java:29)

Test case:

package tests;

import java.util.Objects;

import org.junit.Test;
//import org.mockito.ArgumentMatchers;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;

public class MockitoVarargsTest {
    
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Test
    public void testLoggerGetsCalled_Matchers() {
        final Logger spyableLogger = new SpyableLogger(logger);
        final Logger loggerSpy = Mockito.spy(spyableLogger);

        final RuntimeException e = new RuntimeException("Oops");

        final String message = "Something happened";
        callLoggerDebug(loggerSpy, message, e);

        // Mockito 1.10.19 works
        // Mockito 2.18.3 fails
        Mockito.verify(loggerSpy, Mockito.times(1)).debug(Matchers.anyString(), Matchers.any(Object[].class));
        Mockito.verify(loggerSpy, Mockito.never()).trace(Matchers.anyString(), Matchers.any(Object[].class));
        Mockito.verify(loggerSpy, Mockito.never()).warn(Matchers.anyString(), Matchers.any(Object[].class));
        Mockito.verify(loggerSpy, Mockito.never()).error(Matchers.anyString(), Matchers.any(Object[].class));

        final String message2 = String.format("%s %s", "Something", "happened");
        Mockito.verify(loggerSpy, Mockito.times(1)).debug(Matchers.eq(message2), Matchers.any(Object[].class));
    }

//    @Test
//    public void testLoggerGetsCalled_ArgumentMatchers() {
//        final Logger spyableLogger = new SpyableLogger(logger);
//        final Logger loggerSpy = Mockito.spy(spyableLogger);
//
//        final RuntimeException e = new RuntimeException("Oops");
//
//        final String message = "Something happened";
//        callLoggerDebug(loggerSpy, message, e);
//
//        // Mockito 2.18.3 fails
//        Mockito.verify(loggerSpy, Mockito.times(1)).debug(ArgumentMatchers.anyString(), ArgumentMatchers.any(Object[].class));
//        Mockito.verify(loggerSpy, Mockito.never()).trace(ArgumentMatchers.anyString(), ArgumentMatchers.any(Object[].class));
//        Mockito.verify(loggerSpy, Mockito.never()).warn(ArgumentMatchers.anyString(), ArgumentMatchers.any(Object[].class));
//        Mockito.verify(loggerSpy, Mockito.never()).error(ArgumentMatchers.anyString(), ArgumentMatchers.any(Object[].class));
//
//        final String message2 = String.format("%s %s", "Something", "happened");
//        Mockito.verify(loggerSpy, Mockito.times(1)).debug(ArgumentMatchers.eq(message2), ArgumentMatchers.any(Object[].class));
//    }

    void callLoggerDebug(final Logger logger, final String message, final Object... args) {
        logger.debug(message, args);
    }

    /**
     * {@link Logger}s returned by {@link LoggerFactory} seem to be final and thus cannot be spied on.
     * We use a delegating non-final class to spy on logging calls.
     */
    private static class SpyableLogger implements Logger {
        final Logger delegate;

        public SpyableLogger(final Logger logger) {
            delegate = Objects.requireNonNull(logger, "'logger' must not be null");
        }

        @Override
        public void debug(Marker arg0, String arg1, Object arg2, Object arg3) {
            delegate.debug(arg0, arg1, arg2, arg3);
        }

        @Override
        public void debug(Marker arg0, String arg1, Object... arg2) {
            delegate.debug(arg0, arg1, arg2);
        }

        @Override
        public void debug(Marker arg0, String arg1, Object arg2) {
            delegate.debug(arg0, arg1, arg2);
        }

        @Override
        public void debug(Marker arg0, String arg1, Throwable arg2) {
            delegate.debug(arg0, arg1, arg2);
        }

        @Override
        public void debug(Marker arg0, String arg1) {
            delegate.debug(arg0, arg1);
        }

        @Override
        public void debug(String arg0, Object arg1, Object arg2) {
            delegate.debug(arg0, arg1, arg2);
        }

        @Override
        public void debug(String arg0, Object... arg1) {
            delegate.debug(arg0, arg1);
        }

        @Override
        public void debug(String arg0, Object arg1) {
            delegate.debug(arg0, arg1);
        }

        @Override
        public void debug(String arg0, Throwable arg1) {
            delegate.debug(arg0, arg1);
        }

        @Override
        public void debug(String arg0) {
            delegate.debug(arg0);
        }

        @Override
        public void error(Marker arg0, String arg1, Object arg2, Object arg3) {
            delegate.error(arg0, arg1, arg2, arg3);
        }

        @Override
        public void error(Marker arg0, String arg1, Object... arg2) {
            delegate.error(arg0, arg1, arg2);
        }

        @Override
        public void error(Marker arg0, String arg1, Object arg2) {
            delegate.error(arg0, arg1, arg2);
        }

        @Override
        public void error(Marker arg0, String arg1, Throwable arg2) {
            delegate.error(arg0, arg1, arg2);
        }

        @Override
        public void error(Marker arg0, String arg1) {
            delegate.error(arg0, arg1);
        }

        @Override
        public void error(String arg0, Object arg1, Object arg2) {
            delegate.error(arg0, arg1, arg2);
        }

        @Override
        public void error(String arg0, Object... arg1) {
            delegate.error(arg0, arg1);
        }

        @Override
        public void error(String arg0, Object arg1) {
            delegate.error(arg0, arg1);
        }

        @Override
        public void error(String arg0, Throwable arg1) {
            delegate.error(arg0, arg1);
        }

        @Override
        public void error(String arg0) {
            delegate.error(arg0);
        }

        @Override
        public String getName() {
            return delegate.getName();
        }

        @Override
        public void info(Marker arg0, String arg1, Object arg2, Object arg3) {
            delegate.info(arg0, arg1, arg2, arg3);
        }

        @Override
        public void info(Marker arg0, String arg1, Object... arg2) {
            delegate.info(arg0, arg1, arg2);
        }

        @Override
        public void info(Marker arg0, String arg1, Object arg2) {
            delegate.info(arg0, arg1, arg2);
        }

        @Override
        public void info(Marker arg0, String arg1, Throwable arg2) {
            delegate.info(arg0, arg1, arg2);
        }

        @Override
        public void info(Marker arg0, String arg1) {
            delegate.info(arg0, arg1);
        }

        @Override
        public void info(String arg0, Object arg1, Object arg2) {
            delegate.info(arg0, arg1, arg2);
        }

        @Override
        public void info(String arg0, Object... arg1) {
            delegate.info(arg0, arg1);
        }

        @Override
        public void info(String arg0, Object arg1) {
            delegate.info(arg0, arg1);
        }

        @Override
        public void info(String arg0, Throwable arg1) {
            delegate.info(arg0, arg1);
        }

        @Override
        public void info(String arg0) {
            delegate.info(arg0);
        }

        @Override
        public boolean isDebugEnabled() {
            return delegate.isDebugEnabled();
        }

        @Override
        public boolean isDebugEnabled(Marker arg0) {
            return delegate.isDebugEnabled(arg0);
        }

        @Override
        public boolean isErrorEnabled() {
            return delegate.isErrorEnabled();
        }

        @Override
        public boolean isErrorEnabled(Marker arg0) {
            return delegate.isErrorEnabled(arg0);
        }

        @Override
        public boolean isInfoEnabled() {
            return delegate.isInfoEnabled();
        }

        @Override
        public boolean isInfoEnabled(Marker arg0) {
            return delegate.isInfoEnabled(arg0);
        }

        @Override
        public boolean isTraceEnabled() {
            return delegate.isTraceEnabled();
        }

        @Override
        public boolean isTraceEnabled(Marker arg0) {
            return delegate.isTraceEnabled(arg0);
        }

        @Override
        public boolean isWarnEnabled() {
            return delegate.isWarnEnabled();
        }

        @Override
        public boolean isWarnEnabled(Marker arg0) {
            return delegate.isWarnEnabled(arg0);
        }

        @Override
        public void trace(Marker arg0, String arg1, Object arg2, Object arg3) {
            delegate.trace(arg0, arg1, arg2, arg3);
        }

        @Override
        public void trace(Marker arg0, String arg1, Object... arg2) {
            delegate.trace(arg0, arg1, arg2);
        }

        @Override
        public void trace(Marker arg0, String arg1, Object arg2) {
            delegate.trace(arg0, arg1, arg2);
        }

        @Override
        public void trace(Marker arg0, String arg1, Throwable arg2) {
            delegate.trace(arg0, arg1, arg2);
        }

        @Override
        public void trace(Marker arg0, String arg1) {
            delegate.trace(arg0, arg1);
        }

        @Override
        public void trace(String arg0, Object arg1, Object arg2) {
            delegate.trace(arg0, arg1, arg2);
        }

        @Override
        public void trace(String arg0, Object... arg1) {
            delegate.trace(arg0, arg1);
        }

        @Override
        public void trace(String arg0, Object arg1) {
            delegate.trace(arg0, arg1);
        }

        @Override
        public void trace(String arg0, Throwable arg1) {
            delegate.trace(arg0, arg1);
        }

        @Override
        public void trace(String arg0) {
            delegate.trace(arg0);
        }

        @Override
        public void warn(Marker arg0, String arg1, Object arg2, Object arg3) {
            delegate.warn(arg0, arg1, arg2, arg3);
        }

        @Override
        public void warn(Marker arg0, String arg1, Object... arg2) {
            delegate.warn(arg0, arg1, arg2);
        }

        @Override
        public void warn(Marker arg0, String arg1, Object arg2) {
            delegate.warn(arg0, arg1, arg2);
        }

        @Override
        public void warn(Marker arg0, String arg1, Throwable arg2) {
            delegate.warn(arg0, arg1, arg2);
        }

        @Override
        public void warn(Marker arg0, String arg1) {
            delegate.warn(arg0, arg1);
        }

        @Override
        public void warn(String arg0, Object arg1, Object arg2) {
            delegate.warn(arg0, arg1, arg2);
        }

        @Override
        public void warn(String arg0, Object... arg1) {
            delegate.warn(arg0, arg1);
        }

        @Override
        public void warn(String arg0, Object arg1) {
            delegate.warn(arg0, arg1);
        }

        @Override
        public void warn(String arg0, Throwable arg1) {
            delegate.warn(arg0, arg1);
        }

        @Override
        public void warn(String arg0) {
            delegate.warn(arg0);
        }

    }

}
TimvdLippe pushed a commit that referenced this issue Dec 22, 2022
Using the new `type()`, we can differentiate between matching all varargs
or only one argument of the varargs.

# Benefits:

Because this approach leaves `VarargsMatcher` untouched, it does not require additional existing matchers to implement `VarargsMatcher` to fix issues such as #567. Where as the first PR would require `Null` and `NotNull` to be marked `VarargsMatcher`.

This PR creates new variants of `isNotNull` and `isNull` to address #567. 
Having `InstanceOf` override `type()` provides a workable solution to #1593.
Having `equals` override `type` addresses #1222.

# Downsides

The obvious downside is that this changes the public `ArgumentMatcher` interface, though in a backwards compatible way.

## Known limitation

The main limitation I'm aware of, is not a new limitation. It is that it is not possible to assert only a single parameter is passed to the vararg parameter, when using a `VarargMatcher`, e.g. `any()`. (ref: #1593). For example:

```java
// Given method:
int vararg(String... args);

// I want to mock this invocation:
mock.vararag("one param");

// ...but not these:
mock.vararg();
mock.vararg("more than", "one param");
```

There is no current way to do this.  This is because in the following intuitive mocking:

```java
given(mock.vararg(any(String.class))).willReturn(1);
```

... matches zero or more vararg parameters, as the `any()` method is using `VarargMatcher`. It seems to me that `VarargMatcher` is... a little broken!  This is maybe something that should be consider a candiate for fixing in the next major version bump.  

While it is not possible to fix any `VarargMatcher` based matchers in a backwards compatible way, this the approach in this PR it is possible to mock/verify exactly one vararg param using `isA`, rather than `any`:

```java
    @test
    public void shouldMatchExactlyOnParam() {
        mock.varargs("one param");

        verify(mock).varargs(isA(String.class));
    }

    @test
    public void shouldNotMatchMoreParams() {
        mock.varargs("two", "params");

        verify(mock, never()).varargs(isA(String.class));
    }

    @test
    public void shouldMatchAnyNumberOfParams() {
        mock.varargs("two", "params");

        verify(mock).varargs(isA(String[].class));
    }

```

... because `isA` does not implement `VarargsMatcher`, and so can work as expected once it implements `type()`.

Fixes #2796
Fixes #567
Fixes #584
Fixes #1222
Fixes #1498
TimvdLippe pushed a commit that referenced this issue Dec 28, 2022
Using the new `type()`, we can differentiate between matching all varargs
or only one argument of the varargs.

# Benefits:

Because this approach leaves `VarargsMatcher` untouched, it does not require additional existing matchers to implement `VarargsMatcher` to fix issues such as #567. Where as the first PR would require `Null` and `NotNull` to be marked `VarargsMatcher`.

This PR creates new variants of `isNotNull` and `isNull` to address #567. 
Having `InstanceOf` override `type()` provides a workable solution to #1593.
Having `equals` override `type` addresses #1222.

# Downsides

The obvious downside is that this changes the public `ArgumentMatcher` interface, though in a backwards compatible way.

## Known limitation

The main limitation I'm aware of, is not a new limitation. It is that it is not possible to assert only a single parameter is passed to the vararg parameter, when using a `VarargMatcher`, e.g. `any()`. (ref: #1593). For example:

```java
// Given method:
int vararg(String... args);

// I want to mock this invocation:
mock.vararag("one param");

// ...but not these:
mock.vararg();
mock.vararg("more than", "one param");
```

There is no current way to do this.  This is because in the following intuitive mocking:

```java
given(mock.vararg(any(String.class))).willReturn(1);
```

... matches zero or more vararg parameters, as the `any()` method is using `VarargMatcher`. It seems to me that `VarargMatcher` is... a little broken!  This is maybe something that should be consider a candiate for fixing in the next major version bump.  

While it is not possible to fix any `VarargMatcher` based matchers in a backwards compatible way, this the approach in this PR it is possible to mock/verify exactly one vararg param using `isA`, rather than `any`:

```java
    @test
    public void shouldMatchExactlyOnParam() {
        mock.varargs("one param");

        verify(mock).varargs(isA(String.class));
    }

    @test
    public void shouldNotMatchMoreParams() {
        mock.varargs("two", "params");

        verify(mock, never()).varargs(isA(String.class));
    }

    @test
    public void shouldMatchAnyNumberOfParams() {
        mock.varargs("two", "params");

        verify(mock).varargs(isA(String[].class));
    }

```

... because `isA` does not implement `VarargsMatcher`, and so can work as expected once it implements `type()`.

Fixes #2796
Fixes #567
Fixes #584
Fixes #1222
Fixes #1498
@big-andy-coates
Copy link
Contributor

Fixed by #2807

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

Successfully merging a pull request may close this issue.

2 participants