Any day in the week, if you ask me "should i mock my beans for testing", i'll say no.
But there are scenarios where mock things is the best approach to keep trusting the code.
One could ask "why waste time writing tests?" and i could answer "to save time".
The test suite present in the source code is the first documentation a programmer can access when joining a new project, it is supposed to cover all main scenarios and also a fully working test suite with decent coverage means there is some degree of correctness in the code presented to us.
Sometimes the scenario where application is supposed to run is quite complex, involving databases, external rest services, event brokers, queues and so on.
Because of that, the test scenario itself become utterly complex and hard to reproduce.
This is also why there is difference between unit test suites and integration test suites.
Although hard, it usually worths not mock complex test scenarios. Question is how to create a near real test scenario.
Most frameworks supports different application profiles and using them properly is the key for testing without mock things.
The most common integration test scenario is the database. If possible, avoid to mock your database and create tests against the real thing. Or the most real as possible thing.
For example, you can use a h2 embedded database to run your tests. Just prepare your test application profile to use it.
The drawback here is the dialect differences between the production database and the testing database. Sometimes the application uses very specific functions that aren't present in all database flavors. H2 will behave mostly well.
You can see an example of h2 for tests and something else for production here.
There is also testcontainers, but it relies on a proper docker installation into the test runner machine.
This one is easier or the hardest one, depending on context.
Keep a second REST service endpoint solely for testing purposes.
Keep a staging endpoint.
It can be easy if you call the shots on the service being consumed, but hard if there is no such thing already offered by your supplier.
Some redis test solutions can spin it up for you so your tests will have a redis queue available for tests.
You where been warned.
Even so, if you still wat to mock things here goes a few advices in order to do a decent damage control.
Mocks are lies. If you're going to lie, at least come up with a good one.
The following test mock passes but it's a lousy lie:
package foo.company.service
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
class FooBarServiceTest {
@InjectMocks
lateinit var fooBarService: FooBarService
@Mock
lateinit var fooBarRepository: FooBarRepository
@BeforeEach
fun setUp() {
MockitoAnnotations.openMocks(this)
Mockito.`when`(fooBarRepository.list().thenReturn(emptyList())
}
@Test
fun `should get products`() {
val products = FooBarService.list()
assert(products.size == 0)
}
}
Instead of return an empty list, return one with reasonable values, similar to the ones from the real database if possible.
Mock repositories, let controllers, services and any other layers run for real:
package foo.company.controller
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.mockito.kotlin.anyOrNull
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
@WebMvcTest(ProductsController::class)
class ProductsControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var metadataRepository: MetadataRepository
@MockBean
private lateinit var authService: AuthService
@Value("\${mock.token}")
private var mockToken: String = ""
@BeforeEach
fun setUp() {
Mockito.`when`(authService.validate(anyOrNull()))
.thenReturn(true)
Mockito.`when`(metadataRepository.getMetadata())
.thenReturn(mapOf("data" to "true"))
}
@Test
fun shouldGetMetadata() {
mockMvc
.perform(
MockMvcRequestBuilders.get("/metadata")
.header("Authorization", "Bearer $mockToken")
).andExpect(
MockMvcResultMatchers
.status()
.isOk()
).andExpect(
MockMvcResultMatchers.content()
.contentType(MediaType.APPLICATION_JSON)
).andExpect(
MockMvcResultMatchers
.jsonPath("$.data")
.value("true")
)
}
}
Sometimes some silver lining is needed (ex. a third party authentication) but less mock in mocks is always good.
Finally, remember that the real application keeps evolving, so your mocks. It's a necessity to combe back to your mocked data regularly to tidy things and keep your mocks as real as possible.
Mock integration tests remains something i still don't recommend, however the scenarios we face on day to day modern development might forces us to compromise and deliver good instead of excellent.
See you later and happy hacking!