Use Mock to make Unit Test easy

In the article How to Write Testable Code, I touched on the concept of Mocks. So what is a Mock? And how can you leverage it to make Unit Testing significantly simpler and more reliable?

MockK-logo

What is a Mock?

A Mock is a simulated test double whose behavior is pre-programmed dynamically at runtime to mimic the responses of real collaborator classes during test execution. Furthermore, mocks allow you to verify whether specific methods were invoked, with what parameters, and how many times.

Sounds a bit abstract?

Consider this practical test scenario:

Test a method that queries customers with total purchases exceeding $3,000 from a Database.

Without mocking, unit testing this introduces serious hurdles:

  • Must the unit test connect to a live database? What happens if test data changes or the database becomes unreachable during CI runs?
  • How can you verify that the repository method was called with parameter 3,000 rather than 2,999? If the query is executed twice accidentally inside a loop, returning identical data, how would you catch that bug without verifying invocations?

This is where Mocking comes to the rescue!

Beyond databases, Mocks are invaluable when collaborating with:

  • Non-deterministic suppliers (Random numbers, current system timestamps, weather feeds, external clocks…).
  • External infrastructure (Databases, REST/gRPC endpoints, message queues, caching layers…).
  • Slow or resource-heavy computations.
  • Components not yet implemented by other teammates. For example, in Custom Exception, we can unit test SalaryTransfer ahead of time without waiting for a teammate to finish TransferMoney by Mocking the return values :muscle:.
  • Context-dependent state (Thread context, security context, session scope…).

Note: For an in-depth exploration of test double taxonomy, read Martin Fowler’s Mocks Aren’t Stubs.

Mock and Dependency Injection

In traditional tightly coupled designs, business classes instantiate their own data access objects, making database isolation difficult. With IoC and DI (see Inversion of Control and Dependency Injection), classes receive dependencies via injection. During runtime, real implementations are supplied; during unit testing, mock objects are effortlessly injected instead.

Example Unit Test with Mock

Given the following requirement:

Display the full names of customers whose total purchases exceed N from the Database, where N is provided as an input.

We define CustomerRepository:

1
2
3
4
5
interface CustomerRepository : CrudRepository<Customer, Long> {

  fun findByTotalBuyPriceGreaterThan(totalBuyPrice: Int): List<Customer>

}

And map the retrieved Customer entities to full names in CustomerService:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Component
class CustomerService(
  private val customerRepository: CustomerRepository
) {

  fun getFullNameCustomerWithTotalBuyPriceGreaterThan(totalBuyPrice: Int) =
    customerRepository
      .findByTotalBuyPriceGreaterThan(totalBuyPrice)
      .map { "${it.firstName} ${it.lastName}" }

}

Setup MockK in our test class:

1
2
3
4
5
6
7
private val customerRepository: CustomerRepository = mockk()
private val customerService = CustomerService(customerRepository)

@AfterEach
fun tearDown() {
  clearMocks(customerRepository)
}

And here is a representative test case:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Test
fun `test getFullNameCustomerWithTotalBuyPriceGreaterThan return one customer`() {
  val totalBuyPrice = 3_000

  every {
    customerRepository.findByTotalBuyPriceGreaterThan(3_000)
  } returns listOf(
    Customer(firstName = "Tri", lastName = "Le")
  )

  assertEquals(
    listOf("Tri Le"),
    customerService.getFullNameCustomerWithTotalBuyPriceGreaterThan(3_000)
  )

  verify(exactly = 1) { customerRepository.findByTotalBuyPriceGreaterThan(totalBuyPrice) }

}

A standard mock-based test case follows 3 distinct phases (Arrange, Act/Assert, Verify):

  1. Setup (Arrange) (lines 5-9): Configure the mock stub (every { ... } returns ...) specifying what value customerRepository.findByTotalBuyPriceGreaterThan(3_000) should return when called.

  2. Assert (Act & Assert) (lines 11-14): Execute the unit under test and compare actual results against expected values.

  3. Verify (line 16): Ensure the mock collaborator was actually invoked with the expected parameters and exact call count (verify(exactly = 1)). Skipping verification is risky because it fails to catch accidental duplicate calls—imagine accidentally executing multiple duplicate salary transfers as in Clean Code with Exception :sweat:!

Note 1: Here we use Spring Data JPA, which dynamically provisions repository implementations. In other architectures, write concrete classes and let Spring’s DI container inject them at runtime.

Note 2: The full sample code is updated in this repo.

Conclusion

With the power of Mocks, unit testing becomes much cleaner, faster, and reliable.

Thanks for reading!

Reference articles
updatedupdated2026-09-052026-09-05
Load Comments?