Stubs, fakes, and mocks are the unsung helpers behind green bars in JUnit. They keep tests fast, focused, and fearless. If your tests feel slow or fragile, odds are your doubles are doing the wrong job.
Java 8 is rolling across teams, JUnit 4 is still the default, and Mockito is everywhere. With all that, the basics still decide if your tests stick or break. Let’s keep it practical and grounded.
Why do test doubles matter in Java JUnit projects?
Because units need isolation. If your service pulls rates from an API, hits a database, or sends mail, a true unit test should not do any of that. It should run in memory and finish in milliseconds.
That is where stubs, fakes, and mocks step in. Same idea, different jobs. Get the names right, and your tests become simpler by default.
What is a stub?
A stub is a simple stand in that returns fixed data. It does not have logic or memory. It is there to feed your unit a known value so you can assert the outcome.
public interface ExchangeRateClient {
BigDecimal rate(String from, String to);
}
public class MoneyConverter {
private final ExchangeRateClient client;
public MoneyConverter(ExchangeRateClient client) { this.client = client; }
public BigDecimal convert(BigDecimal amount, String from, String to) {
return amount.multiply(client.rate(from, to));
}
}
public class StubExchangeRateClient implements ExchangeRateClient {
@Override
public BigDecimal rate(String from, String to) {
return new BigDecimal("0.74"); // fixed value for test
}
}
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import java.math.BigDecimal;
public class MoneyConverterTest {
@Test
public void convertsUsingStubbedRate() {
MoneyConverter c = new MoneyConverter(new StubExchangeRateClient());
BigDecimal eur = c.convert(new BigDecimal("100"), "USD", "EUR");
assertEquals(new BigDecimal("74.00"), eur.setScale(2));
}
}Use a stub when the dependency is a data source and you only care about a known return value.
When should I use a fake?
A fake is a lightweight working version. It has simple logic or an in memory store. It behaves close to the real thing without network or disk.
public class User {
private final String id;
private final String email;
public User(String id, String email) { this.id = id; this.email = email; }
public String id() { return id; }
public String email() { return email; }
}
public interface UserRepository {
void save(User user);
User findById(String id);
}
import java.util.HashMap;
import java.util.Map;
public class FakeUserRepository implements UserRepository {
private final Map<String, User> data = new HashMap<>();
@Override public void save(User user) { data.put(user.id(), user); }
@Override public User findById(String id) { return data.get(id); }
}
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class UserServiceTest {
@Test
public void createsAndLoadsUserWithFakeRepo() {
FakeUserRepository repo = new FakeUserRepository();
repo.save(new User("42", "neo@matrix.io"));
assertEquals("neo@matrix.io", repo.findById("42").email());
}
}Pick a fake when the dependency is a store or cache and you want realistic behavior with zero setup cost.
When do mocks fit?
A mock is about behavior. You set expectations and verify calls. This is handy when the output is not the point, but the interaction is.
public interface EmailGateway {
void send(String to, String subject);
}
public class SignupService {
private final EmailGateway email;
public SignupService(EmailGateway email) { this.email = email; }
public void signup(String emailAddress) {
// ... create account ...
email.send(emailAddress, "Welcome");
}
}
import org.junit.Test;
import static org.mockito.Mockito.*;
public class SignupServiceTest {
@Test
public void sendsWelcomeEmailOnSignup() {
EmailGateway gateway = mock(EmailGateway.class);
SignupService service = new SignupService(gateway);
service.signup("alice@example.com");
verify(gateway).send("alice@example.com", "Welcome");
verifyNoMoreInteractions(gateway);
}
}Keep mocks focused on observable interactions. Do not verify every tiny call. Verify the calls that matter to business rules.
How do I pick the right test double?
- Stub when you need a fixed return value.
- Fake when you need simple behavior that feels real.
- Mock when the goal is to assert a call was made.
- No double when the real thing is fast and in memory.
What about Spring, databases, and HTTP?
Do not spin the full context for a unit. Wire services by hand in tests. For controllers and repositories, write a few integration tests that boot the app and hit real boundaries, then keep the rest as pure unit tests with doubles.
How do I write readable JUnit tests?
import org.junit.Test;
import static org.junit.Assert.*;
public class PriceCalculatorTest {
@Test
public void totalIncludesTax() {
// given
TaxService tax = amount -> new BigDecimal("0.10"); // Java 8 lambda for a tiny stub
PriceCalculator calc = new PriceCalculator(tax);
// when
BigDecimal total = calc.total(new BigDecimal("50.00"));
// then
assertEquals(new BigDecimal("55.00"), total);
}
}
interface TaxService { BigDecimal rate(BigDecimal amount); }
class PriceCalculator {
private final TaxService tax;
PriceCalculator(TaxService tax) { this.tax = tax; }
BigDecimal total(BigDecimal base) {
BigDecimal rate = tax.rate(base);
return base.add(base.multiply(rate));
}
}Arrange Act Assert is still king. Keep one reason to fail per test. Name tests like a sentence that explains the rule.
What are common smells?
- Overmocking: verifying private chatter inside the unit.
- Slow tests: network calls leaking into unit suites.
- Brittle stubs: many tests break after a tiny signature change.
- Hidden global state: singletons that remember things across tests.
Which tools should I reach for today?
JUnit 4 keeps things simple. Mockito covers most mocking needs and its syntax reads well. Hamcrest or plain JUnit assertions are fine. If you write Groovy tests, Spock is lovely for specs. For hard static stuff, some folks use PowerMock, but reach for that only when design is boxed in.
Quick cheat sheet
- Business rule result test: use stubs or a fake.
- Side effect test like email or queue: use a mock and verify.
- Repository logic test: use a fake in memory repo.
- End to end path: write a small set of integration tests and keep them separate.
Takeaway
Pick the simplest double that proves the rule. Stub for data, fake for behavior, mock for interaction. Keep the unit tight and fast, and push the heavy lifting to a small set of broader tests.
Your future self will thank you when the build stays quick, the diff is small, and the bar stays green.