Юнит-тесты сопрограмм проходят индивидуально, но не вместе. - PullRequest
1 голос
/ 15 марта 2019

У меня есть два теста сопрограмм, которые оба проходят, когда запускаются индивидуально, но если я запускаю их вместе, второй всегда терпит неудачу (даже если я переключаю их!).Я получаю ошибку:

Требуется, но не вызывается: наблюдатель.onChanged ([SomeObject (someValue = test2)]);На самом деле, с этим макетом было нулевое взаимодействие.

Вероятно, есть что-то фундаментальное, чего я не понимаю в сопрограммах (или тестировании в целом) и делаю что-то не так.

Если я отлаживаютесты, которые я обнаружил, что провальный тест не ожидает завершения внутреннего runBlocking.На самом деле причина, по которой у меня есть внутреннее runBlocking, в первую очередь, заключается в том, чтобы решить именно эту проблему, и она, похоже, работает для отдельных тестов.

Есть какие-либо идеи относительно того, почему это может происходить?

Тестовый класс

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

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()
    private lateinit var mainThreadSurrogate: ExecutorCoroutineDispatcher

    @Mock
    lateinit var repository: DataSource
    @Mock
    lateinit var observer: Observer<List<SomeObject>>

    private lateinit var viewModel: SomeViewModel


    @Before
    fun setUp() {
        mainThreadSurrogate = newSingleThreadContext("UI thread")
        Dispatchers.setMain(mainThreadSurrogate)
        viewModel = SomeViewModel(repository)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        mainThreadSurrogate.close()
    }

    @Test
    fun `loadObjects1 should get objects1`() = runBlocking {
        viewModel.someObjects1.observeForever(observer)
        val expectedResult = listOf(SomeObject("test1")) 
        `when`(repository.getSomeObjects1Async())
        .thenReturn(expectedResult)

        runBlocking {
            viewModel.loadSomeobjects1()
        }

        verify(observer).onChanged(listOf(SomeObject("test1")))
    }

    @Test
    fun `loadObjects2 should get objects2`() = runBlocking {
        viewModel.someObjects2.observeForever(observer)
        val expectedResult = listOf(SomeObject("test2"))
        `when`(repository.getSomeObjects2Async())
        .thenReturn(expectedResult)

        runBlocking {
            viewModel.loadSomeObjects2()
        }

        verify(observer).onChanged(listOf(SomeObject("test2")))
    }
}

ViewModel

class SomeViewModel constructor(private val repository: DataSource) : 
    ViewModel(), CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main

    private var objects1Job: Job? = null
    private var objects2Job: Job? = null
    val someObjects1 = MutableLiveData<List<SomeObject>>()
    val someObjects2 = MutableLiveData<List<SomeObject>>()

    fun loadSomeObjects1() {
        objects1Job = launch {
            val objects1Result = repository.getSomeObjects1Async()
            objects1.value = objects1Result
        }
    }

    fun loadSomeObjects2() {
        objects2Job = launch {
            val objects2Result = repository.getSomeObjects2Async()
            objects2.value = objects2Result
        }
    }

    override fun onCleared() {
        super.onCleared()
        objects1Job?.cancel()
        objects2Job?.cancel()
    }
}

Хранилище

class Repository(private val remoteDataSource: DataSource) : DataSource {

    override suspend fun getSomeObjects1Async(): List<SomeObject> {
        return remoteDataSource.getSomeObjects1Async()
    }

    override suspend fun getSomeObjects2Async(): List<SomeObject> {
        return remoteDataSource.getSomeObjects2Async()
    }
}

1 Ответ

3 голосов
/ 21 марта 2019

Когда вы используете launch, вы создаете сопрограмму, которая будет выполнять асинхронно . Использование runBlocking никак не повлияет на это.

Ваши тесты не пройдены, потому что вещи внутри ваших запусков произойдут , но еще не произошло.

Самый простой способ убедиться, что ваши запуски были выполнены до выполнения каких-либо утверждений, - это вызвать на них .join().

fun someLaunch() : Job = launch {
  foo()
}

@Test
fun `test some launch`() = runBlocking {
  someLaunch().join()

  verify { foo() }
}

Вместо сохранения отдельных Jobs в вашем ViewModel, в onCleared() вы можете реализовать свой CoroutineScope следующим образом:

class MyViewModel : ViewModel(), CoroutineScope {
  private val job = SupervisorJob()
  override val coroutineContext : CoroutineContext
    get() = job + Dispatchers.Main

  override fun onCleared() {
    super.onCleared()
    job.cancel()
  }
}

Все запуски, которые происходят в CoroutineScope, становятся дочерними по отношению к этому CoroutineScope, поэтому, если вы отмените job (что фактически отменяет CoroutineScope), то вы отмените все сопрограммы, выполняющиеся в этой области.

Итак, как только вы очистите реализацию CoroutineScope, вы можете заставить свои ViewModel функции просто возвращать Job s:

fun loadSomeObjects1() = launch {
    val objects1Result = repository.getSomeObjects1Async()
    objects1.value = objects1Result
}

и теперь вы можете легко проверить их с помощью .join():

@Test
fun `loadObjects1 should get objects1`() = runBlocking {
    viewModel.someObjects1.observeForever(observer)
    val expectedResult = listOf(SomeObject("test1")) 
    `when`(repository.getSomeObjects1Async())
    .thenReturn(expectedResult)

    viewModel.loadSomeobjects1().join()

    verify(observer).onChanged(listOf(SomeObject("test1")))
}

Я также заметил, что вы используете Dispatchers.Main для ViewModel. Это означает, что по умолчанию вы будете выполнять все сопрограммы в главном потоке. Вы должны подумать, действительно ли это то, что вы хотите сделать. В конце концов, в главном потоке нужно сделать очень мало вещей, не связанных с пользовательским интерфейсом, и ваша ViewModel не должна напрямую управлять пользовательским интерфейсом.

...