Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
288: Test Doubles: Stubs, Fakes, Spies, and Mocks
In a perfect world, your Java classes would be completely isolated. In the real world, your code depends on databases, external APIs, and email servers. If you try to run a unit test that actually hits a production SMTP server, you're going to have a bad time—slow tests, flaky results, and a very angry IT department.
That's where "Test Doubles" come in. Think of them like stunt doubles in a movie. The lead actor (your real production class) is too expensive or dangerous to put in the scene, so we swap them out for someone who looks the part but can handle the fall. Depending on what we're testing, we need different types of doubles: Stubs, Fakes, Spies, and Mocks.
Let's build an OrderProcessor. Its job is to take an order and notify the customer via an EmailService. Here is our starting point:
public interface EmailService {
boolean isServerUp();
void sendEmail(String address, String message);
}
public class OrderProcessor {
private final EmailService emailService;
public OrderProcessor(EmailService emailService) {
this.emailService = emailService;
}
public void processOrder(Order order) {
if (emailService.isServerUp()) {
emailService.sendEmail(order.getCustomerEmail(), "Order Confirmed!");
}
}
}
Getting past the gatekeeper with a Stub
First, I want to test the processOrder method. But wait—the method checks isServerUp(). If I use the real service, it might be down, and my test will fail for reasons that have nothing to do with my logic. I just need that method to return true so I can get to the actual code I'm testing.
This is a Stub. A stub provides canned answers to calls made during the test. It doesn't care about the input; it just returns a fixed value.
// A simple Stub implementation
EmailService stubService = new EmailService() {
@Override
public boolean isServerUp() {
return true; // Always say yes
}
@Override
public void sendEmail(String address, String message) {}
};
OrderProcessor processor = new OrderProcessor(stubService);
processor.processOrder(myOrder);
// The test continues because the stub "lied" and said the server was up.
Simulating a mail server with a Fake
Stubs are great for bypassing checks, but what if I want to verify that an email was actually "sent" without actually sending one? I could use another stub, but I'll need a way to store the result. This is where a Fake comes in. A fake is a working implementation, but usually takes a shortcut that makes it unsuitable for production (like using a List instead of a real database or mail server).
public class FakeEmailService implements EmailService {
public List<String> sentEmails = new ArrayList<>();
@Override
public boolean isServerUp() { return true; }
@Override
public void sendEmail(String address, String message) {
sentEmails.add(address); // Just save it in memory
}
}
The "State vs. Interaction" mistake
Here is where I usually trip up when I'm rushing. I tried using the FakeEmailService above to verify the behavior. My test looked like this: assertTrue(fakeService.sentEmails.contains("user@example.com"));.
The problem? I'm testing the state of the fake, not the interaction of the processor. If I change how the fake stores emails (e.g., switching to a Map), my test breaks, even though the OrderProcessor logic didn't change. I'm testing my double instead of my code. This is a classic trap. To fix this, I need to know exactly how the method was called, not just what the end result was in a list.
Recording calls with a Spy
To solve that, I'll use a Spy. A spy wraps a real object (or a stub) and records everything that happens to it. It's like a flight data recorder for your objects.
In a manual spy, I'd add a counter or a flag to track the call. In modern Java development, we usually use libraries like Mockito to do this for us, but the concept is: "Tell me how many times sendEmail was called and with what arguments."
// Conceptually, a Spy remembers the interaction
EmailService spyService = spy(new FakeEmailService());
processor.processOrder(myOrder);
// I'm verifying the interaction, not the internal list of the fake
verify(spyService).sendEmail("user@example.com", "Order Confirmed!");
Defining expectations with Mocks
Finally, we have the Mock. People often use "mock" as a generic term for all the above, but a true mock is different. While a spy records what happened after the fact, a mock is programmed with expectations before the code runs. You're essentially saying: "I expect this specific method to be called exactly once with these specific parameters. If it isn't, fail the test immediately."
Mocks are powerful but dangerous. If you over-use them, your tests become "brittle"—they break every time you rename a private method or change an internal call, even if the final outcome is still correct. Use stubs for inputs, fakes for persistence, and mocks/spies sparingly for critical interactions.
📋 Practical Task
Implementing a Payment Gateway Double
You are building a PaymentProcessor that depends on an external PaymentGateway interface. The PaymentGateway has a method boolean charge(String creditCard, double amount).
The PaymentProcessor should only mark an order as "Paid" if the charge method returns true. If it returns false, the order should be marked as "Payment Failed".
Your Task:
- Create a Stub implementation of
PaymentGatewaythat allows you to control the return value ofcharge(e.g., by passing the desired result into the stub's constructor). - Write a test case where the stub returns
trueand verify the order is marked "Paid". - Write a second test case where the stub returns
falseand verify the order is marked "Payment Failed".
There are no comments for now.