Skip to main content
Pinpoint
Testing

Mockito Tutorial: Modern Mocking for Java

Pinpoint Team8 min read

Mockito is the most widely used mocking framework in the Java ecosystem, and its popularity is not accidental. It provides a clean, readable API for isolating units of code from their dependencies, which is the fundamental challenge in writing useful Java tests. Whether your team runs Spring Boot microservices or a monolithic backend, understanding mockito patterns well is the difference between a test suite that catches regressions and one that just adds build time. This tutorial covers modern mockito usage for teams that want tests they can actually trust.

Why mockito matters for Java teams

Java's type system and dependency injection patterns create a specific testing challenge. When a service class depends on a repository, an HTTP client, and a message publisher, testing that service in isolation requires replacing those dependencies with controlled substitutes. Without a mocking framework, you would write manual stub classes for each dependency, which quickly becomes a maintenance burden as interfaces evolve.

Mockito eliminates that burden by generating mock implementations at runtime. You declare which methods should return which values, the framework handles the rest, and your test focuses on the behavior you actually care about. The 2024 JetBrains Developer Survey reported that 72 percent of Java developers use Mockito, making it the de facto standard for unit testing in the ecosystem.

The framework's staying power comes from its design philosophy: mocks should read like specifications. When you see when(repository.findById(1L)).thenReturn(Optional.of(user)), you immediately understand what the test is setting up. That readability matters because tests serve as documentation. Six months from now, someone reading that test should understand the expected behavior without digging through the implementation.

Core patterns: mocking, stubbing, and verification

Mockito provides three distinct capabilities that teams sometimes conflate. Understanding when to use each one prevents the over-mocked test suites that pass reliably but catch nothing.

Stubbing defines what a mock returns when called. This is the most common operation: you set up a dependency to return specific data so you can test how your service handles that data. The when/thenReturn pattern covers most cases. For methods that should throw exceptions, when/thenThrow lets you test error handling paths without orchestrating actual failures.

Verification checks that a mock was called in a specific way. Use verify when the side effect is the behavior you are testing. If your service should publish an event after saving a record, verify(publisher).publish(expectedEvent) confirms that the publish call happened with the right payload. Verification answers the question "did my code do the right thing with its dependencies?"

Argument capture lets you inspect the exact arguments passed to a mock. ArgumentCaptor is useful when the argument is constructed inside the method under test and you need to assert on its properties. Instead of trying to match the exact object, you capture whatever was passed and assert on its fields individually.

A practical guideline: stub inputs, verify outputs, capture when you need to inspect constructed objects. If you find yourself doing all three on the same mock in the same test, the test is probably trying to verify too much at once.

Modern mockito: BDD style and strict stubs

Mockito has evolved significantly since its early versions. Two features in particular deserve attention from teams writing new tests: BDD-style syntax and strict stubbing.

The BDD API (accessed through BDDMockito) replaces when/thenReturn with given/willReturn. The behavior is identical; the difference is purely readability. In a test structured as given/when/then, using given(repository.findById(1L)).willReturn(user) reads more naturally in the setup section. Teams that follow behavior-driven conventions find this alignment reduces cognitive friction when reading tests.

Strict stubbing, enabled by default since Mockito 2.x, flags unnecessary stubs as test failures. If you stub a method but the code under test never calls it, Mockito raises an error. This catches a common maintenance problem: stubs that were relevant when the test was written but became dead code after a refactor. Without strict stubs, those orphaned setups accumulate and make tests harder to understand.

Some teams disable strict stubbing because it creates noise during refactoring. A better approach is to keep it enabled and use lenient() selectively for the specific stubs that may or may not be called depending on execution path. This preserves the safety net for most of your test suite while accommodating the edge cases.

Integration with Spring Boot testing

Most Java backend teams run Spring Boot, which has its own testing annotations that interact with Mockito. Understanding the boundary between @MockBean and @Mock prevents a common performance pitfall.

@Mock (from Mockito) creates a mock in memory. It is fast, has no framework overhead, and works in any plain JUnit test. @MockBean (from Spring) creates a mock and registers it in the Spring application context, replacing whatever bean was there before. This is powerful because it lets you mock a single dependency inside a fully wired Spring context, but it forces Spring to rebuild the context for that test class.

The practical impact: each unique combination of @MockBean declarations creates a new cached context. A test suite with 50 test classes that each mock different beans can end up booting 50 separate Spring contexts, turning a 2-minute suite into a 15-minute one. The fix is to standardize your mock combinations. Create a base test class or a shared configuration that mocks the same external dependencies across related test classes so Spring can reuse a single context.

For pure unit tests of service classes, prefer @Mock with @InjectMocks. This skips the Spring context entirely, keeping tests fast and focused. Reserve @MockBean for integration tests where you need the real Spring wiring but want to control specific external boundaries. The breakdown of when to use each approach connects to the broader question of where automation adds value versus where it adds cost.

Antipatterns that erode test value

After reviewing hundreds of Java test suites, several mockito antipatterns appear with enough frequency to warrant explicit callouts:

  • Mocking the class under test. If you are using spy() or @Spy on the class you are testing, something is wrong with the design. The class probably does too much and needs to be decomposed. Spying on the system under test means you are partially mocking behavior you should be verifying.
  • Mocking value objects. If a class is a simple data carrier with no external dependencies, just instantiate it. Mocking a DTO or an entity creates indirection without any isolation benefit, and it makes the test harder to read.
  • Verifying every method call. Verification should focus on meaningful side effects, not on proving that every line of code executed. Over-verification makes tests brittle because any internal change, even a harmless reordering, breaks assertions.
  • Deep stubbing chains. Calling when(a.getB().getC().getValue()).thenReturn(x) is a sign that your code violates the Law of Demeter. The fix is in the production code, not in the test setup.
  • Using any() as a default matcher. When every stub uses argument matchers like any(), the test does not validate that the right arguments are being passed. Use specific values when they matter and any() only when they genuinely do not.

Where unit tests stop and human testing starts

A well-structured mockito test suite gives you confidence that your service classes handle their dependencies correctly. It validates business logic, error handling, and data transformation at the unit level. What it cannot validate is whether those units compose into a product that works correctly from the user's perspective.

The gap between "all unit tests pass" and "the feature works in production" is where most escaped defects live. Your mockito tests verify that the order service calculates the total correctly and calls the payment gateway with the right amount. They do not verify that the checkout page displays the total in the right currency, handles a declined card gracefully, or works when the user navigates back and forward during the flow. Those are integration and experience concerns that require a different testing approach, specifically structured regression testing and exploratory sessions.

The most effective Java teams treat mockito tests as the fast feedback layer: hundreds of tests running in seconds, catching logic errors before code leaves the developer's machine. On top of that, they layer integration tests that verify component interactions and human QA that evaluates the end-to-end experience. Each layer catches bugs the other layers miss.

If your team has strong unit coverage but production bugs still slip through, the issue is rarely that you need more mocks. It is that you need a testing layer that exercises your application the way real users do. For teams ready to add that layer, see how managed QA complements existing test suites without requiring new tooling or headcount.

Ready to level up your QA?

Book a free 30-minute call and see how Pinpoint plugs into your pipeline with zero overhead.