“I never use mocking frameworks like Mockito. Why? Either I have my test data under control, or I write the methods in a functional way.”
When I say this, it usually provokes strong reactions. Mocking has become such a standard part of unit testing that it seems almost rebellious to suggest otherwise. But after many years of software development, I’ve realized that relying heavily on mocking frameworks creates more problems than it solves.
Let me explain why I avoid mocks and how you can, too.
The Problem With Mocks
Mocking frameworks like Mockito or EasyMock were designed to help us isolate units of code. The intention is good. But in practice, they often lead to:
- Tight coupling between tests and implementation details.
- False positives green tests even though your app is broken.
- Testing the mock instead of the implementation
In other words, mocks solve one problem (isolation) but introduce others (coupling and complexity).
My Approach: No Mocks
I follow two simple principles:
- Functional design: Write business logic as pure functions without side effects. A function takes input, returns output, and is easy to test.
- Control the data: Use real dependencies where possible, especially for persistence. I use Testcontainers to spin up real databases in tests.
There is on exception: For external HTTP services, I use WireMock. But also here I use it to mock the actual HTTP layer, not my internal code.
What is a pure function?
A pure function in programming is a function that:
- Only depends on its input arguments.
- Has no side effects (no modifying global variables, no I/O, no changing arguments, no database access, etc.).
- Always returns the same result if you give it the same input.
A Simple Example: With and Without Mocks
Imagine a simple use case: calculating the final price after applying a discount.
The source code is available on GitHub: https://github.com/simasch/no-mocks
With Mock
This is the service with the addItem
method, which also contains the price calculation.
@Service public class OrderService { @Transactional public OrderItem addItem(long purchaseOrderId, long productId, int quantity) { if (!orderRepository.purchaseOrderExists(purchaseOrderId)) { throw new IllegalArgumentException("Purchase order does not exist"); } var product = productRepository.findProduct(productId) .orElseThrow(() -> new IllegalArgumentException("Product does not exist")); var productPriceConfiguration = productRepository.findProductPriceConfiguration(productId); var calculatedPrice = productPriceConfiguration .map(priceConfiguration -> { if (quantity >= priceConfiguration.getMinQuantity()) { var discountFactor = BigDecimal.ONE.subtract( new BigDecimal(priceConfiguration.getDiscountPercentage()) .divide(new BigDecimal(100), 3, RoundingMode.HALF_UP)); return product.price().multiply(discountFactor).setScale(3, RoundingMode.HALF_UP); } else { return product.price(); } }) .orElse(product.price()); var orderItem = orderRepository.addItem(purchaseOrderId, productId, quantity, calculatedPrice); return new OrderItem(orderItem.getId(), orderItem.getQuantity(), calculatedPrice, product); } }
And the test:
@ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private OrderRepository orderRepository; @Mock private ProductRepository productRepository; @InjectMocks private OrderService orderService; @Test void add_item_with_discount() { var purchaseOrderId = 1L; var productId = 1L; var quantity = 11; var productPrice = new BigDecimal("3.44"); var calculatedPrice = new BigDecimal("3.096"); // 10% discount var product = new Product(productId, "", productPrice); var priceConfig = new ProductPriceConfigurationRecord(1L, 10, 10, productId); var expectedOrderItem = new OrderItemRecord(1L, quantity, calculatedPrice, purchaseOrderId, productId); when(orderRepository.purchaseOrderExists(purchaseOrderId)).thenReturn(true); when(productRepository.findProduct(productId)).thenReturn(Optional.of(product)); when(productRepository.findProductPriceConfiguration(productId)).thenReturn(Optional.of(priceConfig)); when(orderRepository.addItem(eq(purchaseOrderId), eq(productId), eq(quantity), eq(calculatedPrice))) .thenReturn(expectedOrderItem); var orderItem = orderService.addItem(purchaseOrderId, productId, quantity); assertThat(orderItem).isNotNull(); assertThat(orderItem.price()).isEqualTo(calculatedPrice); } }
At first glance, it looks fine. But in fact, this test primarily tests whether the mocks are set up correctly. Plus I have to write a lot of mocking code to make sure the repositories are return the correct data.
Functional, Mock-free Code
Let’s refactor to a pure function. I extracted the price calculation into a class:
class PriceCalculator { BigDecimal calculatePrice(BigDecimal price, int quantity, int minQuantity, int discountPercentage) { if (quantity >= minQuantity) { var discountFactor = BigDecimal.ONE.subtract( new BigDecimal(discountPercentage).divide(new BigDecimal(100), 3, RoundingMode.HALF_UP)); return price.multiply(discountFactor).setScale(3, RoundingMode.HALF_UP); } else { return price; } } }
The service using the PriceCalculator
@Service public class OrderService { private final PriceCalculator priceCalculator = new PriceCalculator(); @Transactional public OrderItem addItem(long purchaseOrderId, long productId, int quantity) { if (!orderRepository.purchaseOrderExists(purchaseOrderId)) { throw new IllegalArgumentException("Purchase order does not exist"); } var product = productRepository.findProduct(productId) .orElseThrow(() -> new IllegalArgumentException("Product does not exist")); var productPriceConfiguration = productRepository.findProductPriceConfiguration(productId); var calculatedPrice = productPriceConfiguration .map(priceConfiguration -> priceCalculator.calculatePrice(product.price(), quantity, priceConfiguration.getMinQuantity(), priceConfiguration.getDiscountPercentage())) .orElse(product.price()); var orderItem = orderRepository.addItem(purchaseOrderId, productId, quantity, calculatedPrice); return new OrderItem(orderItem.getId(), orderItem.getQuantity(), calculatedPrice, product); } }
Now the test is a real unit test that only tests the price calculation and doesn’t need any mocks.
@Test void calculate_price_with_discount() { var price = priceCalculator.calculatePrice(new BigDecimal("3.44"), 11, 10, 10); assertThat(price).isEqualTo(new BigDecimal("3.096")); }
Benefits:
- No mocking framework needed.
- No dependency injection.
- Pure business logic that’s easy to reason about.
But What About Databases and External Services?
You might think: “Sure, that works for simple functions, but what about databases and external services?”
Exactly. Don’t mock them either.
For databases:
- Use Testcontainers to spin up a real, isolated database for tests.
- Test real queries, migrations, and data, not mocked repositories.
For HTTP APIs:
- Use WireMock to stub external service responses at the HTTP level, which is exactly where your app would call them in production.
This way, your tests interact with real infrastructure, not hand-waved mocks.
There will be a blog post about how to use Testcontainers and WireMock with Spring Boot soon.
The Advantages
- Refactor with confidence: No mocks tied to implementation.
- Realistic tests: Simulate the real world as much as possible.
- Simple mental model: Business logic is just pure functions.
- Better maintainability: Tests change less often.
When I Might Still Mock
I won’t claim I never mock anything. But I reserve mocking for:
- Time-based code (e.g., faking
Instant.now()
). - Randomness (e.g., seeding random generators).
- Uncontrollable external systems (without stable APIs).
I prefer using custom fakes or stubs over general-purpose mocking frameworks.
Conclusion
Mocks aren’t inherently evil. But if you design your code to be functional and use real infrastructure in your tests, you might not need mocking frameworks at all. You might discover that your tests have become simpler, faster to write, and more robust.
What’s your experience with mocks? Would you dare to go mock-free?