模拟时单元测试协程空指针异常

unit test coroutine nullpointerexception when mocking

我正在对以下内容进行单元测试class

class LoadTrendingSearchUseCaseImp @Inject constructor(
    private val searchCriteriaProvider: SearchCriteriaProvider,
    private val coroutineDispatcherProvider: CoroutineDispatcherProvider
) : LoadTrendingSearchUseCase {

    override suspend fun execute(): List<String> {
        return withContext(coroutineDispatcherProvider.io()) {
            searchCriteriaProvider.provideTrendingSearch().trendingSearches
        }
    }
}

interface SearchCriteriaProvider {
    suspend fun provideTrendingSearch(): CatalogSearchPage
}

class SearchCriteriaProviderImp() : SearchCritieraProvider {
    override suspend fun provideTrendingSearch(): CatalogSearchPage {
        return withContext(coroutineDispatcherProvider.io()) {
           /* long running task */
        }
    }
}

interface CoroutineDispatcherProvider {
    fun io(): CoroutineDispatcher = Dispatchers.IO

    fun default(): CoroutineDispatcher = Dispatchers.Default

    fun main(): CoroutineDispatcher = Dispatchers.Main

    fun immediate(): CoroutineDispatcher = Dispatchers.Main.immediate

    fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
}

class CoroutineDispatcherProviderImp @Inject constructor() : CoroutineDispatcherProvider

这是我的实际测试:

class LoadTrendingSearchUseCaseImpTest {
    private val searchCriteriaProvider: SearchCriteriaProvider = mock()
    private val coroutineDispatcherProvider = CoroutineDispatcherProviderImp()
    private lateinit var loadTrendingSearchUseCaseImp: LoadTrendingSearchUseCaseImp

    @Before
    fun setUp() {
        loadTrendingSearchUseCaseImp = LoadTrendingSearchUseCaseImp(
            searchCriteriaProvider,
            coroutineDispatcherProvider
        )
    }

    @Test
    fun `should provide trending searches`() {
        runBlockingTest {
            // Arrange
            // EXCEPTION HERE                whenever(searchCriteriaProvider.provideTrendingSearch().trendingSearches).thenReturn(
                emptyList()
            )

            // Act
            val actualResult = loadTrendingSearchUseCaseImp.execute()

            // Assert
            assertThat(actualResult).isEmpty()
        }
    }
}

实际错误信息:

  java.lang.NullPointerException
    .product_search.usecase.imp.LoadTrendingSearchUseCaseImpTest$should provide trending searches.invokeSuspend(LoadTrendingSearchUseCaseImpTest.kt:30)

您尝试在存根方法时链接调用。

whenever(searchCriteriaProvider.provideTrendingSearch().trendingSearches)
  .thenReturn(emptyList())

在存根期间,正在调用实际的方法。

  • searchCriteriaProvider.provideTrendingSearch() return 为空,因为此调用尚未存根
  • 后续调用 null.trendingSearches 导致 NPE

您需要对链中的每个调用进行存根

whenever(searchCriteriaProvider.provideTrendingSearch())
  .thenReturn(catalogSearchPage)
whenever(catalogSearchPage.trendingSearches)
  .thenReturn(emptyList())

显然,这假设

  • catalogSearchPage 也是一个 mock
  • trendingSearches 是一个 属性

或者,您可以为 catalogSearchPage 构造一个 POJO,并在第一个存根中 return 它。