Котлин корутины. Часть 3. Тестирование корутин через поведение

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

Библиотека

Недавно была запущена библиотека kotlinx-coroutines-test, которая предоставляет множество утилит для упрощения тестирования сопрограмм на Android. Библиотека на момент подготовки урока имела статус @ExperimentalCoroutinesApi и может измениться до окончательного выпуска.

Библиотека предоставляет возможность установить Dispatchers.Main для запуска тестов вне устройства, а также диспетчер тестирования, который позволяет тестовому коду контролировать выполнение корутин.

Он предлагает вам возможность:

Автоматический переход времени для обычных функций приостановки
Явно контролировать время для тестирования нескольких корутин
Немедленно выполнить тела запуска или асинхронные блоки кода
Приостановка, ручное прохождение и перезапуск выполнения корутин в тесте
Сообщение о невыполненных исключениях как об ошибках теста
Чтобы узнать больше, прочитайте документацию для kotlinx-coroutines-test.

Поскольку библиотека в настоящее время помечена как экспериментальная, эта статья покажет вам, как писать тесты с использованием стабильных API, пока она не станет стабильной.

В конце этого раздела вы можете найти тестовый код, переписанный с помощью kotlinx-coroutines-test.

Просмотрите существующий тест

Откройте MainViewModelTest.kt в папке androidTest.

@RunWith(JUnit4::class)
class MainViewModelTest {

   /**
    * In this test, LiveData will immediately post values without switching threads.
    */
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   /**
    * Before the test runs initialize subject
    */
   @Before
   fun setup() {
       subject = MainViewModel()
   }
}

Перед каждым тестом происходят две вещи:

  1. Правило — это способ запуска кода до и после выполнения теста в JUnit. InstantTaskExecutorRule — это правило JUnit, которое настраивает LiveData для немедленной публикации в основном потоке во время выполнения теста.
  2. Во время настройки setup() теста поле subject  инициализируется новой MainViewModel.

После этой настройки теста определяется один тест:

@Test
fun whenMainViewModelClicked_showSnackbar() {
   runBlocking {
       subject.snackbar.captureValues {
           subject.onMainViewClicked()
           assertSendsValues(2_000, "Hello, from threads!")
       }
   }
}

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

Этот тест зависит только от общедоступного API ViewModel: когда вызывается onMainViewClicked, «Hello, from threads!»  будет отправлен в снэк-бар.

Мы не изменили общедоступный API, вызов метода все еще обновляет снэк-бар, поэтому изменение реализации на корутины не нарушит тест.

Запустите существующий тест

  1. Щелкните правой кнопкой мыши имя класса MainViewModelTest в вашем редакторе, чтобы открыть контекстное меню.
  2. В контекстном меню выберите execute.pngRun ‘MainViewModelTest’
  3. Для будущих запусков вы можете выбрать эту тестовую конфигурацию в конфигурациях рядом с кнопкой execute.png на панели инструментов.

По умолчанию конфигурация будет называться MainViewModelTest.

При запуске теста вы получите ошибку подтверждения, если вы выполнили предыдущее упражнение

expected: Hello, from threads!
but was : Hello, from coroutines!

Обновление не пройдено, чтобы пройти тест

Этот тест не пройден, потому что мы изменили поведение нашего метода. Вместо того, чтобы сказать «Hello, from threads!» он говорит «Hello, from coroutines!»

Обновите тест до нового поведения, изменив утверждение.

@Test
fun whenMainViewModelClicked_showSnackbar() {
   runBlocking {
       subject.snackbar.captureValues {
           subject.onMainViewClicked()
           assertSendsValues(2_000, "Hello, from coroutines!")
       }
   }
}

При повторном запуске теста с помощью execute.png на панели инструментов тест пройдёт

Протестировав только общедоступный API, мы смогли изменить наш тест с фонового потока на корутину без каких-либо изменений в структуре нашего теста.

В следующем упражнении вы узнаете, как преобразовать существующие API обратного вызова для работы с корутинами.

Как насчет этой задержки?

Этот тест все еще имеет одну серьезную проблему. Требуется целая секунда, чтобы выполнить! Это связано с тем, что вызов delay (1_000) жестко закодирован в onMainViewClicked.

Тесты должны выполняться как можно быстрее, и этот может определенно выполняться быстрее. TestCoroutineDispatcher, предоставляемый kotlinx-coroutines-test, позволяет вам контролировать «виртуальное время» и вызывать функцию с задержкой в ​​одну секунду, фактически не ожидая ни секунды.

Вот вышеописанный тест, переписанный для использования экспериментального TestCoroutineDispatcher.

/**
 * Example of the same test written using the experimental test
 * kotlinx-coroutines-test API.
 */

@RunWith(JUnit4::class)
class MainViewModelTest {

   /**
    * In this test, LiveData will immediately post values without switching threads.
    */
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()
   /**
     * This dispatcher will let us progress time in the test.
     */
   var testDispatcher = TestCoroutineDispatcher()

   lateinit var subject: MainViewModel

   /**
    * Before the test runs initialize subject
    */
   @Before
   fun setup() {
       // set Dispatchers.Main, this allows our test to run 
       // off-device
       Dispatchers.setMain(testDispatcher)
       subject = MainViewModel()
   }
   
   @After
   fun teardown() {
       // reset main after the test is done
       Dispatchers.resetMain()
       // call this to ensure TestCoroutineDispater doesn't 
       // accidentally carry state to the next test
       dispatcher.cleanupTestCoroutines()
   }

   // note the use of runBlockingTest instead of runBlocking
   // this gives the test the ability to control time.
   @Test
    fun whenMainViewModelClicked_showSnackbar() = testDispatcher.runBlockingTest {
        subject.snackbar.observeForTesting {
            subject.onMainViewClicked()
            // progress time by one second
            advanceTimeBy(1_000)
            // value is available immediately without making the test wait
            Truth.assertThat(subject.snackbar.value)
                 .isEqualTo("Hello, from coroutines!")
        }
    }

    // helper method to allow us to get the value from a LiveData
    // LiveData won't publish a result until there is at least one observer
    private fun <T> LiveData<T>.observeForTesting(
            block: () -> Unit) {
        val observer = Observer<T> { Unit }
        try {
            observeForever(observer)
            block()
        } finally {
            removeObserver(observer)
        }
    }

}

Продолжение:

Котлин корутины. Часть 4. Переход callback API на корутины

 

 

Понравилась статья? Поделиться с друзьями:
Добавить комментарий