I am trying to test the getTopicNames function (below) in two scenarios: If it succeeds and if it does not succeed.
fun getTopicNames(): Either<Exception, Set<String>> =
try {
adminClient.listTopics()
.names()
.get()
.right()
} catch (exception: ExecutionException) {
exception.left()
}
This is the test class in which I am doing those two scenarios. If I run each test individually, they both suceed. If I run the entire class the second to execute fails because for some reason the previous mock on adminClient.listTopics() is being retained.
These are the versions for everything involved:
kotlin: 1.3.72
koin: 2.1.6
junit: 5.6.1
mockk: 1.10.0
class TopicOperationsTest {
#BeforeEach
fun start() {
val testModule = module(createdAtStart = true) {
single { mockk<AdminClient>() }
}
startKoin { modules(testModule) }
}
#AfterEach
fun stop() {
stopKoin()
}
#Test
fun `getTopicNames() returns a Right with the topics names`() {
val adminClient = get(AdminClient::class.java)
val listOfTopicsToReturn = mockk<ListTopicsResult>()
val expectedTopics = setOf("Topic1", "Topic2", "Topic3")
every { adminClient.listTopics() } returns listOfTopicsToReturn
every { listOfTopicsToReturn.names() } returns KafkaFuture.completedFuture(expectedTopics)
println("listOfTopicsToReturn.names(): " + listOfTopicsToReturn.names())
println("adminClient.listTopics(): " + adminClient.listTopics())
println("getTopicNames(): " + getTopicNames())
assertThat(getTopicNames().getOrElse { emptySet() }, `is`(expectedTopics))
}
#Test
fun `getTopicNames() returns a Left if failing to get topic names`() {
val adminClient = get(AdminClient::class.java)
every { adminClient.listTopics() } throws ExecutionException("Some Failure", Exception())
assertThat(getTopicNames(), IsInstanceOf(Either.Left::class.java))
}
}
This is the error I get, caused by the fact that the test that verifies the failure is the first to run:
java.lang.AssertionError:
Expected: is <[Topic1, Topic2, Topic3]>
but: was <[]>
Expected :is <[Topic1, Topic2, Topic3]>
Actual :<[]>
<Click to see difference>
Already tried clearAllMocks() on the BeforeEach method but it does not solve my problem as I just start getting:
io.mockk.MockKException: no answer found for: AdminClient(#1).listTopics()
I found a solution that makes everything work. It is a combination of:
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
Having the mock as a class object
MockKAnnotations.init(this) in the #BeforeEach method
clearMocks() specifying the actual mock to be cleared (should work for multiple mocks too, just separated by commas.
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TopicOperationsTest {
private var adminClientMock = mockk<AdminClient>()
#BeforeEach
fun start() {
MockKAnnotations.init(this)
val testModule = module(createdAtStart = true) {
single { adminClientMock }
}
startKoin { modules(testModule) }
}
#AfterEach
fun stop() {
clearMocks(adminClientMock)
stopKoin()
}
#Test
fun `getTopicNames() returns a Right with the topics names`() {
val adminClient = get(AdminClient::class.java)
val listOfTopicsToReturn = mockk<ListTopicsResult>()
val expectedTopics = setOf("Topic1", "Topic2", "Topic3")
every { adminClient.listTopics() } returns listOfTopicsToReturn
every { listOfTopicsToReturn.names() } returns KafkaFuture.completedFuture(expectedTopics)
assertThat(getTopicNames().getOrElse { emptySet() }, `is`(expectedTopics))
}
#Test
fun `getTopicNames() returns a Left if failing to get topic names`() {
val adminClient = get(AdminClient::class.java)
every { adminClient.listTopics() } throws ExecutionException("Some Failure", Exception())
assertThat(getTopicNames(), IsInstanceOf(Either.Left::class.java))
}
}
Related
I have the following class to handle account update kafka events
#Component
class AccountUpdateQueueListener(
private val accountUpdateUseCase: AccountUpdateUseCase
) {
#KafkaListener(
topics = [Topics.ACCOUNT_UPDATES],
groupId = "api",
containerFactory = "accountUpdatesConsumeFactory",
)
fun onAccountUpdate(
#Payload request: AccountUpdateEventMessage,
#Header("_txId") txId: String,
acknowledgment: Acknowledgment
) {
MDC.put("account_id", request.account.id.toString())
MDC.put("_txId", txId)
try {
accountUpdateUseCase.update(AccountUpdateEventMessage.toCommand(request))
acknowledgment.acknowledge()
} catch (e: AccountNotFoundException) {
...
}
}
I am trying to test the accountUpdateUseCase.update function used in onAccountUpdate function is called with this test class
#SpringBootTest
#ActiveProfiles("testing")
#DirtiesContext
#EmbeddedKafka(partitions = 1, brokerProperties = ["listeners=PLAINTEXT://localhost:9092", "port=9092"])
class AccountUpdateQueueListenerTest {
#MockK
lateinit var accountUpdateUseCase: AccountUpdateUseCase
#InjectMockKs
lateinit var accountUpdateQueueListener: AccountUpdateQueueListener
#Autowired
lateinit var kafkaTemplate: KafkaTemplate<String, AccountUpdateEventMessage>
val accountUpdateEventMessage = AccountUpdateEventMessage(
...
)
#BeforeEach
fun init() {
MockKAnnotations.init(this)
}
#Test
fun `no errors and update is entered`() {
kafkaTemplate.send(Topics.ACCOUNT_UPDATES, "", accountUpdateEventMessage)
verify(exactly = 1) { accountUpdateUseCase.update(any()) }
}
}
The tests failed as accountUpdateUseCase.update. When running this with a debugger it is clear that onAccountUpdate is never entered. However, if the test case is rewritten as follows the debugger actually stopped in onAccountUpdate
#Test
fun `no errors and update is entered`() {
kafkaTemplate.send(Topics.ACCOUNT_UPDATES, "", accountUpdateEventMessage)
verify(exactly = 1) { accountUpdateQueueListener.onAccountUpdate(any(),any(),any()) }
}
My questions are why does the debugger stop in the second test case and how can I rewrite the first test to achieve my original goal?
I am using Mockk in my project. I am trying to mock my extension function but it cannot find my function. I tried some piece of code but it cannot find the extension function inside my test. Can someone guide me, How can I solve this issue. Thanks
ExploreConsultationsActivity.kt
class ExploreConsultationsActivity : AppCompactActvity () {
... // more function
internal fun setupExploreConsultationVisibility(hasFocus: Boolean) {
if (hasFocus) {
.....
} else if (viewModel.queryText.isEmpty()) {
binding.consultationViewSwitcher.displayConsultationViewSwitcherChild(0)
}
}
internal fun ViewSwitcher.displayConsultationViewSwitcherChild(childNumber: Int) {
visible()
displayedChild = childNumber
}
}
ExploreConsultationsActivityTest.kt
class ExploreConsultationsActivityTest {
#get:Rule
val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule()
private val subject by lazy { spyk(ExploreConsultationsActivity()) }
#MockK private lateinit var mockConsultationViewSwitcher: ViewSwitcher
#Before
fun setUp() {
MockKAnnotations.init(this, relaxed = true)
setupMockView()
}
private fun setupMockView() {
mockkStatic(ExploreConsultationsLayoutBinding::class)
every { mockRootView.findViewById<ChipGroup>(R.id.exploreConsultationChips) } returns mockChipGroup
}
#Test
fun `setupExploreConsultationVisibility - `() {
// STUBBING
mockViewModel.queryText = ""
every { mockViewModel.topicSelected } returns ConsultationTopicsArea.ALL
with(mockConsultationViewSwitcher){
any<ViewSwitcher>().displayConsultationViewSwitcherChild(0)
}
// EXECUTION
subject.setupExploreConsultationVisibility(false)
// VERIFICATION
verify {
mockViewModel.filterBy(ConsultationTopicsArea.ALL)
}
}
I am getting this error
I have a case where the unit of code I'm testing is running on a different thread and so the test executes and fails before the unit has finished executing:
class Tests {
private lateinit var result: String
#BeforeAll
fun setup() {
DataService().subscribe {
result = it
}
}
#Test
fun `get result from data service`() {
assert(result.contains("Hello"))
}
}
When the test runs, I get the following exception:
kotlin.UninitializedPropertyAccessException: lateinit property result has not been initialized
How can I ensure that the tests don't run before result has been initialized?
The test in the provided form won't work: #BeforeAll is allowed only on static methods, but then it couldn't access the result field. It can be solved by using #BeforeEach and #TestInstance(TestInstance.Lifecycle.PER_METHOD) [but see the UPDATE at the end of the post]
Regarding the result initialization itself: you can check if it has been initialized using this::result.isInitialized, e.g.:
#TestInstance(TestInstance.Lifecycle.PER_METHOD)
class AwaitTest {
private lateinit var result: String
#BeforeEach
fun setup() {
DataService().subscribe {
result = it
}
}
#Test
fun `get result from data service`() = runBlocking {
awaitInitialization()
assert(result.contains("Hello"))
}
suspend fun awaitInitialization() {
while(!this::result.isInitialized) {
delay(100)
}
}
}
Alternatively you could use awaitility:
#TestInstance(TestInstance.Lifecycle.PER_METHOD)
class AwaitTest {
private lateinit var result: String
#BeforeEach
fun setup() {
DataService().subscribe {
result = it
}
}
#Test
fun `get result from data service`() {
await until { this#AwaitTest::result.isInitialized }
assert(result.contains("Hello"))
}
}
Or even better, move await to the setup() method:
#TestInstance(TestInstance.Lifecycle.PER_METHOD)
class AwaitTest {
private lateinit var result: String
#BeforeEach
fun setup() {
DataService().subscribe {
result = it
}
await until { this#AwaitTest::result.isInitialized }
}
#Test
fun `get result from data service`() {
assert(result.contains("Hello"))
}
}
UPDATE:
Actually a #BeforeAll method doesn't need to be static if the test class is annotated with the#TestInstance(TestInstance.Lifecycle.PER_CLASS):
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AwaitTest {
private lateinit var result: String
#BeforeAll
fun setup() {
DataService().subscribe {
result = it
}
await until { this#AwaitTest::result.isInitialized }
}
#Test
fun `get result from data service`() {
assert(result.contains("Hello"))
}
}
I am running the following test code, but in some weird way I am not understanding it is not clearing the mocks in between the two tests.
I am getting environmentAsRequired as a return of repo.getClusterEnvironment(environmentName) on the second test instead of it throwing NoResultException() which causes the second test to fail because the exception I am asserting does not get thrown.
I have already verified and the method annotated with #BeforeEach is being called in between tests.
Anyone has any idea?
class ConfigUtilsTest {
#BeforeEach
fun start() {
MockKAnnotations.init(this)
val testModule = module() {
single { mockk<Repository>() }
single { mockk<AdminClient>() }
}
startKoin { modules(testModule) }
}
#AfterEach
fun stop() {
stopKoin()
unmockkAll()
}
#Test
fun `fetchOrCreateCluster creates the cluster if it is not on db but all required attributes are`() {
val environmentName = "environmentForCluster"
val clusterName = "NameForCluster"
val clusterURL = "UrlForCluster"
val environmentAsRequired = ClusterEnvironment(0, environmentName)
val expectedResult = Cluster(
clusterURL,
clusterName,
environmentAsRequired,
)
val repo = get(Repository::class.java)
every { repo.getCluster(clusterURL) } throws NoResultException()
every { repo.getClusterEnvironment(environmentName) } returns environmentAsRequired
fetchOrCreateCluster(
clusterName,
environmentName,
clusterURL
) shouldBe expectedResult
}
#Test
fun `fetchOrCreateCluster throws InvalidConfigException creating cluster if required attribute (environment) is not on DB`() {
val clusterName = "NameForCluster"
val clusterURL = "UrlForCluster"
val repo = get(Repository::class.java)
every { repo.getCluster(clusterURL) } throws NoResultException()
every { repo.getClusterEnvironment(environmentName) } throws NoResultException()
shouldThrow<InvalidConfigException> {
fetchOrCreateCluster(
clusterName,
environmentName,
clusterURL
)
}
}
}
I'm using "withTestAppliction" in one of my tests to test if the route works. Before all Tests the DB-Table "cats" should have no entries. To get the DAO I need Koin in this Test but if conflicts with "withTestAppliction" where Koin will also be startet and throws A KoinContext is already started
[Update]
I know I could use something like handleRequest(HttpMethod.Delete, "/cats") but I don't want to expose this Rest-Interface. Not even for testing.
#ExperimentalCoroutinesApi
class CatsTest: KoinTest {
companion object {
#BeforeClass
#JvmStatic fun setup() {
// once per run
startKoin {
modules(appModule)
}
}
#AfterClass
#JvmStatic fun teardown() {
// clean up after this class, leave nothing dirty behind
stopKoin()
}
}
#Before
fun setupTest() = runBlockingTest {
val dao = inject<CatDAO>()
dao.value.deleteAll()
}
#After
fun cleanUp() {
}
#Test
fun testCreateCat() {
withTestApplication({ module(testing = true) }) {
val call = createCat(predictName("Pepples"), 22)
call.response.status().`should be`(HttpStatusCode.Created)
}
}
}
fun TestApplicationEngine.createCat(name: String, age: Int): TestApplicationCall {
return handleRequest(HttpMethod.Post, "/cats") {
addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(listOf(
"name" to name,
"age" to age.toString()
).formUrlEncode())
}
}
After test (after withTestApplication()) call KoinContextHandler.get().stopKoin().
Example: https://github.com/comm1x/ktor-boot/blob/master/test/common/common.kt
It looks similar to the issue I faced. The problem was that the module() passed under the withTestApplication() was trying to create the Koin object again. I replaced the module() with specific modules that I had to load for the tests except for the Koin.
Refer - test sample and
application sample
Had the same problem executing multiple tests in a class. After removing the init/startKoin (since it's initialized in the Application when you test with the emulator).
I am not really sure if this is the correct approach, but it kind of works for me and my build server.
#ExperimentalCoroutinesApi
#RunWith(AndroidJUnit4ClassRunner::class) // or JUnit4..
class MyTest : KoinTest {
private val mockedAppModule: Module = module(override = true)
factory { myRepo }
}
#Before
fun setup() {
loadKoinModules(mockedAppModule)
}
#After
fun tearDown() {
unloadKoinModules(mockedAppModule)
}
#Test
fun testSubscriberRegistration() = runBlockingTest { // only needed if you are using supend functions
// test impl...
}
}