Testing with JUnit 5 & Mockito

Verifying Interactions

15 min Lesson 7 of 13

Verifying Interactions

In the previous lesson you learned how to configure Mockito stubs so collaborators return canned values. Stubbing answers the question "what does the collaborator return?" — but a large class of bugs is not about return values at all. They are about whether your code called the right method, with the right arguments, the right number of times. That is what interaction verification is for.

Why Verify Interactions?

Consider an OrderService that is supposed to send a confirmation email after a successful order. The email service returns void — there is nothing to stub. Your only way to assert the behaviour is to verify that emailService.sendConfirmation(order) was actually invoked. Without that assertion, the test passes even if you delete the call entirely.

Interaction tests complement state tests. Prefer asserting on observable state (return values, exceptions, persisted data) first. Add interaction assertions only when the invocation itself is the behaviour under test — for example, publishing an event, logging, or delegating to a void collaborator.

Basic verify()

The simplest form asserts that a method was called exactly once:

import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock EmailService emailService; @Mock OrderRepository repo; @InjectMocks OrderService service; @Test void sendsConfirmationEmailAfterOrder() { Order order = new Order(1L, "item-42", 2); when(repo.save(order)).thenReturn(order); service.place(order); // assert the email was sent exactly once verify(emailService).sendConfirmation(order); } }

verify(mock).method(args) — no extra call to times() — defaults to times(1). If the method was never called, or called more than once, the test fails with a clear message showing the actual invocations.

Controlling Invocation Count

Mockito ships several VerificationMode factories:

// called at least once verify(emailService, atLeastOnce()).sendConfirmation(any()); // called exactly three times verify(repo, times(3)).findById(anyLong()); // never called verify(auditLog, never()).warn(anyString()); // called at most twice verify(cache, atMost(2)).get(anyString());
Prefer never() over negative state assertions. If your code must not invoke a collaborator in some branch (e.g. do not bill the customer when the cart is empty), verify(billing, never()).charge(...) is the clearest way to document and enforce that constraint.

verifyNoMoreInteractions() and verifyNoInteractions()

After all your targeted verify() calls, you can assert that nothing unexpected happened:

@Test void doesNotTouchAuditLogOnHappyPath() { service.place(order); verify(repo).save(order); verify(emailService).sendConfirmation(order); // fail if any other method on these mocks was called verifyNoMoreInteractions(repo, emailService); }
Do not overuse verifyNoMoreInteractions(). Applying it everywhere makes tests fragile — every internal refactoring that touches call patterns breaks unrelated tests. Reserve it for public API contracts where unexpected calls are a genuine defect, such as a service that must not perform extra queries.

Argument Matchers

Sometimes you need to verify a call without caring about every argument. Mockito provides a rich library of argument matchers in org.mockito.ArgumentMatchers:

import static org.mockito.ArgumentMatchers.*; // any non-null string verify(logger).log(anyString()); // specific value verify(repo).findById(eq(42L)); // any object of a type verify(emailService).send(any(EmailMessage.class)); // string that starts with a prefix verify(logger).log(startsWith("[ERROR]")); // custom predicate verify(repo).save(argThat(order -> order.getTotal() > 0));

Matchers can be mixed — but there is one rule: if you use a matcher for any argument, you must use matchers for all arguments. You cannot mix raw values and matchers in the same call.

// WRONG — mixes raw value with matcher verify(service).process(42L, anyString()); // throws InvalidUseOfMatchersException // CORRECT — wrap the literal in eq() verify(service).process(eq(42L), anyString());

ArgumentCaptor — Capturing for Deep Inspection

Argument matchers are binary — a call either matches or it does not. When you need to inspect the captured argument in detail (assert on multiple fields, log it for debugging, reuse it), use ArgumentCaptor:

@Test void buildsCorrectEmailMessage() { Order order = new Order(1L, "item-42", 2); when(repo.save(order)).thenReturn(order); service.place(order); // capture what was passed to sendConfirmation ArgumentCaptor<EmailMessage> captor = ArgumentCaptor.forClass(EmailMessage.class); verify(emailService).sendConfirmation(captor.capture()); EmailMessage sent = captor.getValue(); assertAll( () -> assertEquals("orders@example.com", sent.getFrom()), () -> assertTrue(sent.getBody().contains("item-42")), () -> assertEquals(order.getCustomerEmail(), sent.getTo()) ); }

With the annotation-based style (requires @ExtendWith(MockitoExtension.class)) you can declare the captor as a field:

@Captor ArgumentCaptor<EmailMessage> emailCaptor;

When the same collaborator method is called multiple times, captor.getAllValues() returns the full list in invocation order:

verify(emailService, times(3)).sendConfirmation(emailCaptor.capture()); List<EmailMessage> messages = emailCaptor.getAllValues(); assertEquals("retry@example.com", messages.get(2).getTo());

Verification Order

By default, verify() does not care about call order. When the sequence matters — for example, you must flush a cache before writing to the database — use InOrder:

@Test void flushesBeforeWriting() { service.updateWithFlush(entity); InOrder order = inOrder(cache, repo); order.verify(cache).flush(); order.verify(repo).save(entity); }

Professional Trade-offs

  • Interaction tests are white-box. They couple the test to the implementation, not the contract. A refactoring that is externally equivalent but restructures internal calls will break interaction tests. Use them deliberately.
  • Argument captors for complex assertions are fine, but if you find yourself capturing and asserting on five fields, consider whether the collaborator should receive a dedicated value object that you can construct and compare directly with eq().
  • Prefer one verification concern per test. Mixing state assertions with multiple interaction verifications obscures what the test is actually about.

Summary

verify() is the tool for asserting that your code called a collaborator as expected. Argument matchers let you write flexible verifications without coupling to exact values. ArgumentCaptor lets you pull the actual argument out of the mock so you can run detailed assertions on it. InOrder enforces sequence. Use all of these deliberately — over-verification makes tests brittle and hard to refactor.