Skip to content
Posts en inglés. Usá el traductor del navegador para leerlos en tu idioma.

Mastering Mockito Argument Matchers for Flexible Java Tests

Yammbo
· 8 min read
java unit testing mockito any mockito eq argumentcaptor custom matchers
Mastering Mockito Argument Matchers for Flexible Java Tests

When writing unit tests with Mockito, you often need to verify or stub method calls without being overly specific about every single argument. Hard-coding exact argument values can make tests brittle and verbose, especially when an argument's precise value isn't critical to the test's intent. Mockito argument matchers provide a flexible way to define conditions for method parameters, allowing you to write more robust and readable tests.

Prerequisites

To follow along with this tutorial, you'll need:

  • Java 11 or newer. For installation guidance, refer to the official Java documentation.
  • A Maven or Gradle project configured with JUnit 5 and Mockito 5.x.

Here are the necessary Maven test dependencies:

<dependencies>    <dependency>        <groupId>org.junit.jupiter</groupId>        <artifactId>junit-jupiter</artifactId>        <version>5.10.2</version>        <scope>test</scope>    </dependency>    <dependency>        <groupId>org.mockito</groupId>        <artifactId>mockito-junit-jupiter</artifactId>        <version>5.14.2</version>        <scope>test</scope>    </dependency></dependencies>

For your test classes, ensure you statically import the Mockito argument matchers and core Mockito methods:

import static org.mockito.ArgumentMatchers.*;import static org.mockito.Mockito.*;

We'll use the following simple Foo class throughout our examples:

public class Foo {    public boolean bool(String str, int i, Object obj) {        return false;    }    public int in(boolean b, java.util.List<String> strs) {        return 0;    }    public int bar(byte[] bytes, String[] s, int i) {        return 0;    }    public void log(String message) {        // no-op    }}

Step 1: Understanding Argument Matchers and the Consistency Rule

Mockito argument matchers are helper methods that allow you to specify flexible conditions for method parameters during stubbing (with when()) or verification (with verify()). Instead of matching against an exact value using equals(), you can define broader criteria, such as "any string" or "any integer." For a comprehensive reference, consult the Mockito ArgumentMatchers Javadoc.

The Argument Matcher Consistency Rule

A critical rule in Mockito is that if you use an argument matcher for any parameter in a method call, you must use argument matchers for all parameters in that same call. Mixing raw literal values with matchers will result in an InvalidUseOfMatchersException.

Consider the bool method in our Foo class:

public boolean bool(String str, int i, Object obj) { /* ... */ }

Incorrect usage (mixes matcher with literal):

// This will throw InvalidUseOfMatchersExceptionwhen(mockFoo.bool(anyString(), 1, any())).thenReturn(true);

Here, anyString() and any() are matchers, but 1 is a raw literal. Mockito's internal mechanism for processing matchers differs from how it handles literal values, leading to this restriction.

Correct usage (all matchers):

// All arguments use matcherswhen(mockFoo.bool(anyString(), anyInt(), any())).thenReturn(true);

If you need to specify an exact literal value when other arguments are matchers, use the eq() matcher. This wraps your literal value, making it compatible with the consistency rule.

Correct usage (all matchers, using eq() for literals):

// All arguments use matchers, with 'eq()' for the literal intwhen(mockFoo.bool(anyString(), eq(1), any())).thenReturn(true);

Step 2: Using Built-in Argument Matchers

Mockito provides a rich set of built-in matchers for common data types. These help you express your test intentions clearly.

Generic Matchers

  • any(): Matches any object, regardless of type. Useful for generic parameters or when the exact type is irrelevant.
  • any(Class<T> type): Matches any object of a specific type. For example, any(List.class).

While any() is convenient, prefer more specific typed matchers when possible to improve type safety and avoid unexpected nulls or unboxing issues.

Typed Matchers

These matchers are type-specific and generally safer to use:

  • anyString(): Matches any String.
  • anyInt(), anyLong(), anyBoolean(), etc.: Matches any primitive of the respective type.
  • anyList(), anySet(), anyMap(): Matches any instance of List, Set, or Map respectively.
  • anyCollection(): Matches any Collection.

Example Usage of Built-in Matchers

Let's see these in action with our Foo mock:

import org.junit.jupiter.api.Test;import org.mockito.Mockito;import static org.mockito.ArgumentMatchers.*;import static org.mockito.Mockito.*;import java.util.List;import java.util.Arrays;class FooTest {    @Test    void testBoolWithAnyMatchers() {        Foo mockFoo = mock(Foo.class);        // Stubbing: Return true when bool is called with any String, any int, and any Object        when(mockFoo.bool(anyString(), anyInt(), any())).thenReturn(true);        // Test the method call        boolean result = mockFoo.bool("hello", 5, new Object());        System.out.println("Result of bool: " + result); // true        // Verification: Verify that bool was called with any String, any int, and any Object        verify(mockFoo).bool(anyString(), anyInt(), any());    }    @Test    void testInWithSpecificListAndBoolean() {        Foo mockFoo = mock(Foo.class);        List<String> expectedList = Arrays.asList("item1", "item2");        // Stubbing: Return 10 when in is called with true and a specific List        when(mockFoo.in(eq(true), eq(expectedList))).thenReturn(10);        // Test the method call        int result = mockFoo.in(true, Arrays.asList("item1", "item2"));        System.out.println("Result of in: " + result); // 10        // Verification: Verify that in was called with true and that specific List        verify(mockFoo).in(eq(true), eq(expectedList));        // This would fail verification because the list is different        // verify(mockFoo).in(eq(true), Arrays.asList("different"));    }    @Test    void testLogWithAnyString() {        Foo mockFoo = mock(Foo.class);        // Stubbing: No return value for void method, just set up expectation        doNothing().when(mockFoo).log(anyString());        // Test the method call        mockFoo.log("This is any message");        // Verification: Verify that log was called with any String        verify(mockFoo).log(anyString());    }}

Step 3: Capturing Arguments with ArgumentCaptor

Sometimes, you don't just want to match an argument; you need to inspect the actual value that was passed to a mocked method. This is where ArgumentCaptor comes in handy. It allows you to capture the argument passed to a method and then assert on its properties after the method call has occurred.

ArgumentCaptor is particularly useful when:

  • The argument is an object created within the system under test, and you need to verify its internal state.
  • The argument is a callback or a complex object whose exact instance cannot be easily predicted or constructed for eq().

Example Usage of ArgumentCaptor

Let's capture the message passed to our log method:

import org.junit.jupiter.api.Test;import org.mockito.ArgumentCaptor;import org.mockito.Mockito;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.mockito.Mockito.*;class ArgumentCaptorTest {    @Test    void testLogCapturesMessage() {        Foo mockFoo = mock(Foo.class);        // 1. Declare an ArgumentCaptor for the type of argument you want to capture        ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);        // 2. Call the method on the mock        mockFoo.log("Important log message");        // 3. Verify the method call, passing the captor to the argument position        verify(mockFoo).log(messageCaptor.capture());        // 4. Retrieve the captured value and perform assertions        String capturedMessage = messageCaptor.getValue();        assertEquals("Important log message", capturedMessage);        // You can also capture multiple arguments if the method is called multiple times        mockFoo.log("Another message");        verify(mockFoo, times(2)).log(messageCaptor.capture());        assertEquals("Another message", messageCaptor.getValue()); // Gets the last captured value    }    @Test    void testBoolCapturesMultipleArguments() {        Foo mockFoo = mock(Foo.class);        ArgumentCaptor<String> strCaptor = ArgumentCaptor.forClass(String.class);        ArgumentCaptor<Integer> intCaptor = ArgumentCaptor.forClass(Integer.class);        ArgumentCaptor<Object> objCaptor = ArgumentCaptor.forClass(Object.class);        Object myObject = new Object();        mockFoo.bool("test", 10, myObject);        verify(mockFoo).bool(strCaptor.capture(), intCaptor.capture(), objCaptor.capture());        assertEquals("test", strCaptor.getValue());        assertEquals(10, intCaptor.getValue());        assertEquals(myObject, objCaptor.getValue());    }}

Step 4: Implementing Custom Argument Matchers with argThat()

For scenarios where built-in matchers or ArgumentCaptor aren't sufficient, Mockito allows you to define custom matching logic using argThat(). This method takes an ArgumentMatcher instance (often implemented as a lambda expression or an anonymous class) that defines your specific matching criteria. You can learn more about JUnit 5 assertions and advanced testing techniques in the JUnit 5 User Guide.

Custom matchers are ideal for:

  • Complex validation rules (e.g., an object property must be within a certain range).
  • Reusable matching logic that applies across multiple tests.

Example Usage of Custom Matchers

Let's say we want to verify that a log message contains a specific substring:

import org.junit.jupiter.api.Test;import org.mockito.Mockito;import static org.mockito.ArgumentMatchers.argThat;import static org.mockito.Mockito.*;class CustomMatcherTest {    @Test    void testLogWithCustomMatcher() {        Foo mockFoo = mock(Foo.class);        // Stubbing a void method with a custom matcher        doNothing().when(mockFoo).log(argThat(message -> message.contains("error")));        // This call matches the stubbing condition        mockFoo.log("An error occurred during processing.");        verify(mockFoo).log(argThat(message -> message.contains("error")));        // This call does not match the stubbing condition, so it won't be stubbed        // and if verify is called, it would fail if only the error message was expected.        mockFoo.log("Info message.");        // We can verify that the 'info' message was also logged        verify(mockFoo).log(argThat(message -> message.contains("Info")));    }    @Test    void testBoolWithComplexObjectMatcher() {        Foo mockFoo = mock(Foo.class);        // Imagine a custom object where we need to check multiple properties        class MyCustomObject {            String name;            int value;            public MyCustomObject(String name, int value) { this.name = name; this.value = value; }            public String getName() { return name; }            public int getValue() { return value; }        }        // Stubbing based on properties of MyCustomObject        when(mockFoo.bool(anyString(), anyInt(), argThat(obj ->            obj instanceof MyCustomObject && ((MyCustomObject) obj).getValue() > 0        ))).thenReturn(true);        // This call will match        boolean result1 = mockFoo.bool("data", 5, new MyCustomObject("valid", 10));        System.out.println("Result with valid custom object: " + result1); // true        // This call will not match the stubbing (value <= 0)        boolean result2 = mockFoo.bool("data", 5, new MyCustomObject("invalid", 0));        System.out.println("Result with invalid custom object: " + result2); // false (default for mock)        verify(mockFoo).bool(anyString(), anyInt(), argThat(obj ->            obj instanceof MyCustomObject && ((MyCustomObject) obj).getValue() > 0        ));    }}

Mockito's argument matchers are powerful tools that enhance the flexibility and readability of your unit tests. By understanding when to use generic matchers, typed matchers, eq() for literals, ArgumentCaptor for inspecting values, and argThat() for custom logic, you can write more effective and maintainable tests. These techniques allow you to focus on the behavior you're testing, rather than being bogged down by exact argument values that aren't central to your test case.

For more insights into building robust web applications, explore the resources available at Yammbo.