Skip to Content
Course content

126: Mocking with Mockito

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up until now, you've likely been writing tests that exercise your code in a straight line. You provide an input, the method runs, and you assert the output. That works great for pure logic, but in a professional Java codebase, your classes rarely live in a vacuum. They depend on other services—database wrappers, external APIs, or complex legacy calculators. This is where things get tricky. If you want to test a service that processes a payment, do you really want your unit test to hit a live Stripe API every time it runs?

The fragility of the "Real Deal"

The naive approach to testing dependencies is to just use the real objects. You instantiate your PaymentService and pass it a real PaymentGateway. On the surface, this feels like the "truest" test because it's exercising the actual system. But in practice, this is a nightmare. First, it's slow. Network calls take milliseconds or seconds, and when you have three thousand tests, those seconds turn into an hour-long build process that everyone hates.

Worse, your tests become "flaky." If the payment API goes down for maintenance, your build fails, even though your code hasn't changed a single line. You're no longer testing your logic; you're testing the uptime of a third-party vendor. Some developers try to solve this by writing "Fakes"—manual classes like FakePaymentGateway that implement the interface but just store data in a list. I've seen this a lot, and while it's better than hitting a live API, you end up spending half your time maintaining a parallel hierarchy of fake classes that eventually drift away from how the real API actually behaves.

// The naive way: Hard-coding a fake or using a real service
public class PaymentServiceTest {
    @Test
    void testProcessPayment() {
        // If this is a real gateway, we might actually charge a card!
        // If it's a manual fake, we had to write a whole separate class for it.
        PaymentGateway gateway = new RealPaymentGateway(); 
        PaymentService service = new PaymentService(gateway);
        
        boolean result = service.processPayment(100.00);
        assertTrue(result);
    }
}

Cutting the cord with Mockito

This is where Mockito comes in. Instead of writing a manual fake or risking a real API call, we create a "mock"—a dynamic proxy of the class we need. I think of a mock as a stunt double. It looks like the real object and has the same methods, but it doesn't actually do anything unless you specifically tell it how to behave.

Using Mockito.mock(), you can create an instance of a dependency on the fly. Then, using the when().thenReturn() syntax, you define exactly what that dependency should do for this specific test case. This isolates your unit of work. You aren't testing if the PaymentGateway works (that's the gateway's own team's job); you're testing how PaymentService reacts when the gateway returns "Success" or "Insufficient Funds."

// The professional way: Using Mockito
import static org.mockito.Mockito.*;

public class PaymentServiceTest {
    @Test
    void testProcessPayment_Successful() {
        // Create the stunt double
        PaymentGateway mockGateway = mock(PaymentGateway.class);
        PaymentService service = new PaymentService(mockGateway);

        // Tell the mock how to behave for this specific scenario
        when(mockGateway.charge(anyDouble())).thenReturn(true);

        boolean result = service.processPayment(100.00);
        
        assertTrue(result);
        // Verify that the service actually called the gateway
        verify(mockGateway).charge(100.00);
    }
}

The danger of lying to your tests

Now, a word of caution. Mocking is a superpower, but it's easy to overdo it. I've seen developers mock almost everything in their test, and they end up with a suite of tests that all pass perfectly—while the actual application crashes in production. Why? Because they mocked the dependency to behave in a way that the real object never would.

If you mock a method to return a User object, but the real API actually returns null in that scenario, your test is lying to you. You've created a "perfect world" in your test suite that doesn't exist in reality. To avoid this, only mock the boundaries of your system (like APIs, Databases, or Message Queues). If you find yourself mocking a simple POJO or a utility class, you're probably over-mocking. Keep your mocks thin, and always ensure the behavior you're stubbing is based on the actual documentation of the service you're mimicking.




📋 Practical Task

Exercise: Mocking the Email Dispatcher in NotificationService

You are working on a NotificationService that sends alerts to users. It depends on an EmailClient interface. The NotificationService.sendAlert(String message) method should only call the EmailClient.sendEmail() method if the message is not null and not empty. If the message is empty, it should return false and not attempt to send an email.

Your Task: Write a JUnit test using Mockito to verify two scenarios:

  • Scenario A: When a valid message is provided, sendEmail is called exactly once and the method returns true.
  • Scenario B: When an empty string is provided, sendEmail is never called and the method returns false.

You will need to:
1. Mock the EmailClient.
2. Use when(...).thenReturn(...) to simulate a successful email send.
3. Use verify(...) to ensure the EmailClient was (or was not) interacted with.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.