Модульное тестирование Android с использованием Mockito: не удается получить правильное поведение для макетов

Я тестирую свой класс репозитория, используя Mockito, в частности функцию getProducts():

class Repository private constructor(private val retrofitService: ApiService) {

    companion object {
        @Volatile
        private var INSTANCE: Repository? = null

        fun getInstance(retrofitService: ApiService): Repository {
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Repository(retrofitService)
                }
                INSTANCE = instance
                return instance
            }
        }
    }

    suspend fun getProducts(): ProductsResponse = withContext(IO) {
        retrofitService.getProducts()
    }
}

Это мой тестовый класс:

@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class RepositoryTest {

    // Class under test
    private lateinit var repository: Repository

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    // Set the main coroutines dispatcher for unit testing.
    @ExperimentalCoroutinesApi
    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    @Mock
    private lateinit var retrofitService: ApiService

    @Before
    fun createRepository() {
        MockitoAnnotations.initMocks(this)
        repository = Repository.getInstance(retrofitService)
    }

    @Test
    fun test() = runBlocking {

        // GIVEN
        Mockito.`when`(retrofitService.getProducts()).thenReturn(fakeProductsResponse)

        // WHEN
        val productResponse: ProductsResponse = repository.getProducts()

        println("HERE = ${retrofitService.getProducts()}")

        // THEN
        println("HERE: $productResponse")
        MatcherAssert.assertThat(productResponse, `is`(fakeProductsResponse))
    }
}

И мой АпиСервис:

interface ApiService {
    @GET("https://www...")
    suspend fun getProducts(): ProductsResponse
}

Когда я вызываю repository.getProducts(), он возвращает null, несмотря на то, что я явно установил retrofitService.getProducts() для возврата fakeProductsResponse, который вызывается внутри метода getProducts() репозитория. Он должен вернуть fakeProductsResponse, но возвращает ноль.

Я неправильно издеваюсь или в чем может быть проблема? Спасибо...

Обновлено: это мой MainCoroutineRule, если вам это нужно

@ExperimentalCoroutinesApi
class MainCoroutineRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()):
    TestWatcher(),
    TestCoroutineScope by TestCoroutineScope(dispatcher) {
    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(dispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        cleanupTestCoroutines()
        Dispatchers.resetMain()
    }
}

Я так думаю, потому что вы запускаете его в withContext(IO). Лучшим решением будет создание новых неограниченных диспетчеров. но другое решение - добавить Thread.sleep(3000) в свой тест

Yehezkiel L 11.12.2020 06:26
1
1
315
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это может быть не полное решение вашей проблемы, но я вижу, что ваш MainCoroutineRule переопределяет mainDispatcher Dispatchers.setMain(dispatcher).

Но в

suspend fun getProducts(): ProductsResponse = withContext(IO)

вы явно устанавливаете диспетчер ввода-вывода.

Я рекомендую всегда устанавливать диспетчер из свойства, которое вы передаете через конструктор:

class Repository private constructor(
  private val retrofitService: ApiService,
  private val dispatcher: CoroutineDispatcher) {

    companion object {
       
        fun getInstance(retrofitService: ApiService,
                        dispatcher: CoroutineDispatcher = Dispatchers.IO): Repository {
            // ommit code for simplicity

            instance = Repository(retrofitService, dispatcher)
            // ...
            }
        }
    }

    suspend fun getProducts(): ProductsResponse = withContext(dispatcher) {
        retrofitService.getProducts()
    }
}

Имея параметр по умолчанию, вам не нужно передавать его в свой обычный код, но вы можете заменить его в своем модульном тесте:

class RepositoryTest {

    private lateinit var repository: Repository

    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    @Mock
    private lateinit var retrofitService: ApiService

    @Before
    fun createRepository() {
        MockitoAnnotations.initMocks(this)
        repository = Repository.getInstance(retrofitService, mainCoroutineRule.dispatcher)
    }
}

Для своих собственных модульных тестов я использую функцию блокировки TestCoroutineDispatcher из CoroutineRule, например:

@Test
fun aTest() = mainCoroutineRule.dispatcher.runBlockingTest {
    val acutal = classUnderTest.callToSuspendFunction()

    // do assertions
}

Я надеюсь, что это поможет вам немного.

Вы правы насчет диспетчеров, это была ошибка, которую я не смог найти. Однако это лишь частично решило проблему, так как похоже, что чего-то еще не хватает, возможно, материала Mockito. В любом случае спасибо, это было бесценно!

Sam 14.12.2020 22:31

Другие вопросы по теме