Hi folks. I’m struggling with a mock setup and co...
# mockk
l
Hi folks. I’m struggling with a mock setup and could use help from someone who has, you know, used Kotlin, Mockk, or Spring Boot before February when I started. 😬 (Details in 🧵 )
I’m writing a Kafka consumer, which I now want to test. It’s not particularly complicated; it just calls a service which saves stuff to the database, give or take some munging of the incoming message. I want to test the consumer. Normally, I’d approach this by saying “this is an integration test, not unit test, so just use the DB and move on with life.” Problem: Because of how our application is set up (split into 8 separate modules, but the DB is controlled and imported by just one of them), I don’t have actual SQL tables available in the test container DB. (Yes, we’re using postgres test containers. Not my decision.) So if I try to run my test, I get an error that the table is missing (true).
The PE on the team told me to screw it and just mock the DB, even if that makes the test less useful. OK, fine… but how do I mock the DB across the application (which is booting to handle dependencies) rather than just manually for a single service I create in the test itself? It’s the wiring of everything that I’m testing here.
Duck Duck Go-ing led me to try this (abbreviated):
Copy code
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ScraperErrorConsumerTest {

    @Autowired
    lateinit var objectMapper: ObjectMapper

    @Autowired
    lateinit var subject: ScraperErrorConsumer

    @MockBean
    lateinit var repository: ScraperRefreshRepository

    @Test
    fun `error calls are saved`() {
        val uuid = UUID.randomUUID()

        val scrape = ScraperRefresh(
            stateCode = "IL",
            entityName = "Me, Inc",
        )
        scrape.id = uuid

        setupRepositoryMock(mutableMapOf(scrape.id to scrape))

        repository.save(scrape)

        val message = ScraperErrorMessage(
            id = uuid,
            errorType = "Bad",
            title = "Bad",
            request = EntityRefreshEvent(uuid),
        )

        val ack = mockAcknowledgement()

// The actual thing being tested.        subject.consume(objectMapper.writeValueAsString(message), ack)

        val record = repository.findById(uuid)

        // Assertions here
    }

    private fun mockAcknowledgement() = object : Acknowledgment {
        var called = false
        override fun acknowledge() {
            called = true
        }
    }

    private fun setupRepositoryMock(records: MutableMap<UUID, ScraperRefresh>) {
        repository = mockk<ScraperRefreshRepository>()
        every { repository.save(any<ScraperRefresh>()) } answers {
            val updated = it.invocation.args[0] as ScraperRefresh
            records[updated.id] = updated
            updated
        }
        every { repository.findById(any<UUID>()) } answers {
            val uuid = it.invocation.args[0] as UUID
            Optional.ofNullable(records[uuid])
        }
    }
}
The problem is, that doesn’t propagate the mock through to the underlying service that is held by
subject
. So it saves to I’m not sure where (since there shouldn’t be a DB table?), but then the mocked findById() looks at just the mutable map and doesn’t find the updates. If I don’t re-set the
repository
mock in the first line of setupRepositoryMock(), I get an error
Failed matching mocking signature for
left matchers: [any()]
So… I’m confused here. All I can think of is to abandon and just have a pure unit test, but that doesn’t actually buy me anything. I already have tests for the service class so I know it’s covered. It’s specifically the limited logic in the consumer and the wiring in Spring that I want to test, but I cannot for the life of me see how.
Any suggestions?
e
Honestly I’d stick to the mock db approach. With flyway you can set up the schema for your test container. Would that work?
l
We’re already using flyway, but the SQL files are all in one of the other modules at the moment, so I don’t know how to even get to them. The PE said we should probably move them at some point, but that hasn’t happened.
(Stick to mock db, meaning drop mockk and use a real DB for this test?)
e
Yes. Alternatively you can implement a mock repo which saves to some in-memory thing? So you don’t have to mockk
l
Um, maybe? I still barely comprehend JPA… 😬
(I’ve been developing for 25 years, but anything JVM is new to me this year.)
e
You don’t need JPA. The mock repo would just save to a map or a list
l
Right, but I need to conform to the interface for it.
e
Yes
l
Oh! Another Staff just suggested I use a TestConfiguration class to blast the repository from orbit… and that seems to work! (Still using mockk for now.)
👍 1
Trying to replace it with a manual class. So far, not so good. 😕
j
Did you mean to use @mockkbean? @mockbean is only for mockito
l
Well that could be a problem…