Search code examples
androiddagger-2daggermockkdaggermock

MockK class verification breaking with Dagger


Overview

Expected: Testing Dagger created repository class with empty constructor

Issue: Mockk verification is breaking with mockkConstructor and confirmVerified using Dagger dependency injection to create a repository class.

When implementing a repository with Dagger's Android constructor pattern, an empty constructor with @Inject is required in order for the Dagger app component, AppComponent.kt, to know to create the class.

When confirmVerified is commented out, the tests pass. When confirmVerified is implemented, the methods from prior tests are being called and tracked causing verification to fail.

Working as expected — Singleton repository without Dagger creation

In the unit test mockkObject is used to mock SomeRepository.kt object because it is an object created manually in Kotlin.

SomeRepository.kt

object SomeRepository {
    someMethod(...){...}  
}

SomeParamUnitTest.kt

@ExtendWith(SomeTestExtension::class)
class SomeParamUnitTest(val testDispatcher: TestCoroutineDispatcher) {

    private fun TestLoad() = teedLoadTestCases()
    private lateinit var someViewModel: SomeViewModel

    @BeforeAll
    fun beforeAll() {
        mockkObject(SomeRepository)
        ...
    }

    @AfterAll
    fun afterAll() {
        unmockkAll()
    }

    @ParameterizedTest
    @MethodSource("TestLoad")
    fun `Feed Load`(test: FeedLoadTest) = testDispatcher.runBlockingTest {
        mockComponents(test)
        someViewModel = SomeViewModel(...)
        assertContentList(...) // Working as expected.
        verifyTests(test)
    }

    private fun mockComponents(test: FeedLoadTest) {
        ...
        coEvery {
            SomeRepository.someMethod(test.isRealtime, any())
        } returns mockSomeMethod(test.mockFeedList, test.status)
        ...
    }

    private fun verifyTests(test: FeedLoadTest) {
        coVerify {
            SomeRepository.someMethod(test.isRealtime, any())
        }
        confirmVerified(SomeRepository)
    }
}

Not working — confirmVerified with injected Dagger repository

Instead of using mockkObject for SomeRepository.kt in the unit test, mockkConstructor is implemented per the MockK author's documentation because the Dagger injected repository has a constructor. In addition, anyConstructed is used to mock SomeRepository.kt's methods and listen for verification of the methods being called.

Implementation

App.kt

class App : Application() {
    val appComponent = DaggerAppComponent.create()
    ...
}

AppComponent.kt

@Singleton
@Component
interface AppComponent {
    fun someRepository(): SomeRepository

    // The repository is injected inside of a fragment, then passed as a parameter into the ViewModel.
    fun inject(someFragment: SomeFragment)
}

SomeRepository.kt

@Singleton
class SomeRepository @Inject constructor() {
    someMethod(...){...}  
}

SomeParamUnitTest.kt

@ExtendWith(SomeTestExtension::class)
class SomeParamUnitTest(val testDispatcher: TestCoroutineDispatcher) {

    private fun TestLoad() = teedLoadTestCases()
    private lateinit var repository: SomeRepository
    private lateinit var someViewModel: SomeViewModel

    @BeforeAll
    fun beforeAll() {
        mockkConstructor(SomeRepository::class)
        ...
    }

    @AfterAll
    fun afterAll() {
        unmockkAll()
    }

    @ParameterizedTest
    @MethodSource("TestLoad")
    fun `Feed Load`(test: FeedLoadTest) = testDispatcher.runBlockingTest {
        repository = SomeRepository()
        mockComponents(test)
        someViewModel = SomeViewModel(...)
        assertContentList(...) // Working as expected.
        verifyTests(test)
    }

    private fun mockComponents(test: FeedLoadTest) {
        ...
        coEvery {
            anyConstructed<SomeRepository>().someMethod(test.isRealtime, any())
        } returns mockSomeMethod(test.mockFeedList, test.status)
        ...
    }

    private fun verifyTests(test: FeedLoadTest) {
        coVerify {
            anyConstructed<SomeRepository>().someMethod(test.isRealtime, any())
        }
        confirmVerified(repository)
    }
}

Tests failed

For each parameterized test that runs, the verified and recorded call counts are cumulatively increasing.

java.lang.AssertionError: Verification acknowledgment failed

Verified call count: 368 Recorded call count: 57

Not verified calls:

Stack traces:

at io.mockk.impl.recording.CommonVerificationAcknowledger.acknowledgeVerified(CommonVerificationAcknowledger.kt:36) at io.mockk.MockKDsl.internalConfirmVerified(API.kt:272) at io.mockk.MockKKt.confirmVerified(MockK.kt:314) at app.coinverse.contentviewmodel.tests.FeedLoadTests.verifyTests(FeedLoadTests.kt:201) at app.coinverse.contentviewmodel.tests.FeedLoadTests.access$verifyTests(FeedLoadTests.kt:35) at app.coinverse.contentviewmodel.tests.FeedLoadTests$Feed Load$1.invokeSuspend(FeedLoadTests.kt:67) at app.coinverse.contentviewmodel.tests.FeedLoadTests$Feed Load$1.invoke(FeedLoadTests.kt) at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241) at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:142) at kotlinx.coroutines.DispatchedKt.resumeCancellable(Dispatched.kt:423) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:154) at kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:89) at kotlinx.coroutines.BuildersKt.async(Unknown Source) at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:82) at kotlinx.coroutines.BuildersKt.async$default(Unknown Source) at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49) at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:78) at app.coinverse.contentviewmodel.tests.FeedLoadTests.Feed Load(FeedLoadTests.kt:56) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:675) at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:125) at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:132) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:124) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestTemplateMethod(TimeoutExtension.java:81) at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115) at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:104) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:62) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:43) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:35) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:202) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:198) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32) at org.junit.platform.engine.support.hierarchical.NodeTestTask$DefaultDynamicTestExecutor.execute(NodeTestTask.java:198) at org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor.execute(TestTemplateTestDescriptor.java:138) at org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor.lambda$execute$2(TestTemplateTestDescriptor.java:106) at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) at java.util.stream.ReferencePipeline$11$1.accept(ReferencePipeline.java:373) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580) at java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:270) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418) at java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:270) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418) at java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:270) at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418) at org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor.execute(TestTemplateTestDescriptor.java:106) at org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor.execute(TestTemplateTestDescriptor.java:41) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) at java.util.ArrayList.forEach(ArrayList.java:1257) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) at java.util.ArrayList.forEach(ArrayList.java:1257) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229) at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197) at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128) at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

Attempted Solutions

1. Move mockkConstructor from beforeAll to beforeEach JUnit 5 test lifecycle method.

This seems to be an improvement as the verified and recorded tests are not accumulating each parameterized test round like before. Rather, the correct method signature is not being found.

Tests failed

java.lang.AssertionError: Verification acknowledgment failed

Verified call count: 0 Recorded call count: 2

Not verified calls: 1) SomeRepository(mockkConstructor()).getMainFeedNetwork(false, Timestamp(seconds=1581980238, nanoseconds=352000000)) 2) SomeRepository(mockkConstructor()).getMainFeedRoom(Timestamp(seconds=1581980238, nanoseconds=352000000))

2. Remove test class level instance variable for repository and pass repository via method parameters.

The repository is created as a new value inside of each parameterized test, then passed as a parameter to mockComponents and verifyTests.

The stack trace errors do not indicate any methods are missing. Not verified calls is empty.

Tests failed

java.lang.AssertionError: Verification acknowledgment failed

Verified call count: 2 Recorded call count: 2

Not verified calls:

3. Clear/unmock repository in beforeEach using clearConstructorMockk and unmockkConstructor.

clearConstructorMockk and unmockkConstructor were tested individually and received the same failure messages as Attempted Solution #2.


Solution

  • Mock the repository class using mockkClass() and store as instance value

    @ExtendWith(SomeTestExtension::class)
    class SomeParamUnitTest(val testDispatcher: TestCoroutineDispatcher) {
    
        private fun TestLoad() = teedLoadTestCases()
        private val repository = mockkClass(SomeRepository::class)
        private lateinit var someViewModel: SomeViewModel
    
        @AfterAll
        fun afterAll() {
            unmockkAll()
        }
    
        @ParameterizedTest
        @MethodSource("TestLoad")
        fun `Feed Load`(test: FeedLoadTest) = testDispatcher.runBlockingTest {
            mockComponents(test)
            someViewModel = SomeViewModel(...)
            assertContentList(...) // Working as expected.
            verifyTests(test)
        }
    
        private fun mockComponents(test: FeedLoadTest) {
            ...
            coEvery {
                repository.someMethod(test.isRealtime, any())
            } returns mockSomeMethod(test.mockFeedList, test.status)
            ...
        }
    
        private fun verifyTests(test: FeedLoadTest) {
            coVerify {
                repository.someMethod(test.isRealtime, any())
            }
            confirmVerified(repository)
        }
    }