【问题标题】:Callback not executed in Android unit test with injected mock object使用注入的模拟对象在 Android 单元测试中未执行回调
【发布时间】:2018-09-13 20:20:13
【问题描述】:

我是 Android 单元测试的新手,并且已经阅读了几个教程来熟悉 mockito 和 robolectric。

我的应用程序正在使用 Dagger 2 将我的 EventService 注入我的 MainActivity。对于我的MainActivityUnitTest,我设置了一个TestServicesModule 来提供EventService 的模拟版本,以便我可以使用Robolectric 对我的MainActivity 运行单元测试

在单元测试中执行我的EventService.getAllEvents(callback: ServiceCallback) 上的ServiceCallback 时遇到问题。我已经在 MainActivityUnitTest 类的 @Setup 中验证了 EventService 正在作为模拟对象注入。我已经阅读了几篇教程和博客文章,据我所知,我做的一切都是正确的。 MainActivity 中的refreshData() 函数被成功调用,我可以看到正在执行对eventsService.getAllEvents(callback) 的调用。但是doAnswer {} lambda 函数永远不会被执行。

这是我的相关代码:

AppComponent.kt

@Singleton
@Component(modules = [
    AppModule::class,
    ServicesModule::class,
    FirebaseModule::class
])
interface AppComponent {
    fun inject(target: MainActivity)
}

ServicesModule.kt

@Module
open class ServicesModule {
    @Provides
    @Singleton
    open fun provideEventService(db: FirebaseFirestore): EventsService {
        return EventsServiceImpl(db)
    }
}

EventsService.kt

interface EventsService {
    fun getAllEvents(callback: ServiceCallback<List<Event>>)
    fun getEvent(id: String, callback: ServiceCallback<Event?>)
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    @Inject lateinit var eventsService: EventsService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        (application as App).appComponent.inject(this)
        ...
    }

    override fun onStart() {
        super.onStart()
        refreshData()
    }

    eventsService.getAllEvents(object: ServiceCallback<List<Event>> {
        override fun onCompletion(result: List<Event>) {
            viewModel.allEvents.value = result
            loading_progress.hide()
        }
    })
}

现在我们进入测试:

TestAppComponent.kt

@Singleton
@Component(modules = [
    TestServicesModule::class
])
interface TestAppComponent : AppComponent {
    fun inject(target: MainActivityUnitTest)
}

TestServicesModule.kt

@Module
class TestServicesModule {

    @Provides
    @Singleton
    fun provideEventsService(): EventsService {
        return mock()
    }
}

MainActivityUnitTest.kt

@RunWith(RobolectricTestRunner::class)
@Config(application = TestApp::class)
class MainActivityUnitTest {

    @Inject lateinit var eventsService: EventsService

    @Before
    fun setup() {
        val testComponent = DaggerTestAppComponent.builder().build()
        testComponent.inject(this)
    }

    @Test
    fun givenActivityStarted_whenLoadFailed_shouldDisplayNoEventsMessage() {
        val events = ArrayList<Event>()

        doAnswer {
            //this block is never hit during debug
            val callback: ServiceCallback<List<Event>> = it.getArgument(0)
            callback.onCompletion(events)
        }.whenever(eventsService).getAllEvents(any())

        val activity = Robolectric.buildActivity(MainActivity::class.java).create().start().visible().get()
        val noEventsView = activity.findViewById(R.id.no_events) as View

        //this always evaluates to null because the callback is never set from the doAnswer lambda
        assertThat(callback).isNotNull()
        verify(callback)!!.onCompletion(events)
        assertThat(noEventsView.visibility).isEqualTo(View.VISIBLE)
    }
}

编辑:添加 App 和 TestApp

open class App : Application() {
    private val TAG = this::class.qualifiedName
    lateinit var appComponent: AppComponent

    override fun onCreate() {
        super.onCreate()
        appComponent = initDagger(this)
    }

    open fun initDagger(app: App): AppComponent {
        return DaggerAppComponent.builder().appModule(AppModule(app)).build()
    }
}

class TestApp : App() {
    override fun initDagger(app: App): AppComponent {
        return DaggerTestAppComponent.builder().build()
    }
}

【问题讨论】:

    标签: android kotlin mockito dagger-2 robolectric


    【解决方案1】:

    您似乎在使用不同的组件来注入您的测试和活动。由于它们是不同的组件,我怀疑您正在使用 2 个不同的 eventsService 实例。

    您的测试使用本地 DaggerTestAppComponent。

    @Inject lateinit var eventsService: EventsService
    
    @Before
    fun setup() {
        val testComponent = DaggerTestAppComponent.builder().build()
        testComponent.inject(this)
    }
    

    当您的 Activity 使用应用程序中的 appComponent 时。

    class MainActivity : AppCompatActivity() {
        @Inject lateinit var eventsService: EventsService
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            (application as App).appComponent.inject(this)
            ...
        }
    

    为了克服这个问题,您可以考虑添加应用程序类的测试版本,这将允许您将应用程序中的 AppComponent 替换为您的 TestAppComponent。 Robolectric 应该允许您按如下方式创建测试应用程序:http://robolectric.org/custom-test-runner/

    【讨论】:

    • 所以我有一个 TestApp 类,它扩展了我的 App 类,它覆盖了测试套件的 Dagger。我的理解是这就是目的?这样 TestAppComponent 会覆盖 AppComponent 为 Activity 依赖项提供的依赖项吗?我将在上面的问题中添加相关代码。
    • 另外,当我在调试测试时在 Activity 中的调用上放置断点时,它会报告 eventService 是 EventsService$MockitoMock 的实例
    • 如果您在活动中看到模拟,则您的 TestApp 已设置。您只需要使用相同的组件来注入您的测试,以便使用相同的 eventsService。您可以在 TestApp 中使用组件设置,而不是在测试中创建新组件。我认为使用 robolectric 您可以通过 RuntimeEnvironment.application 访问它 - 这应该是 TestApp 类型,这将允许您访问 appComponent 进行注入。
    • 这正是问题所在。谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多