Заставить Espresso ждать сетевых вызовов, работы с базой данных и других асинхронных задач - PullRequest
0 голосов
/ 17 мая 2019

У меня есть приложение, которое использует UseCases для доступа к репозиторию. UseCases подписываются на пул потоков, хранилище делает сетевые запросы, используя пул потоков OkHttp, а хранилище также выполняет запись в базу данных, которая использует пул потоков Schedulers.io ().

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

AFAIK, моя установка использует три разных пула потоков. Я думал, что если в моем тесте пользовательского интерфейса я создаю ресурс ожидания, который наблюдает за каждым пулом потоков, то при вызове моего репозитория через UseCase Espresso будет ждать до тех пор, пока все пулы потоков будут бездействовать, прежде чем он переместит мой тестовый код.

Вот пример моего тестового кода:

    val wrapped: IdlingResourceScheduler = Rx2Idler.wrap(application.component.providesIoScheduler(), "IO Scheduler")
    val otherWrapped: IdlingResourceScheduler = Rx2Idler.wrap(application.component.providesJobScheduler(), "J O B")
    val resource = arrayOf<IdlingResource>(okHttpIdlingResource, wrapped, otherWrapped)
    idlingRegistry.goIdle(*resource) {
        onView(withId(R.id.toolbar_main))
                .check(matches(isDisplayed()))
    }

То, что я делаю в приведенном выше коде, - это регистрация ресурса холостого хода для моего пула потоков базы данных, моего пула потоков OkHttp и моего пула потоков UseCase. Поэтому я подумал, что по крайней мере один из этих пулов потоков будет всегда «активным», когда будет сделан вызов в мой репозиторий, и как только репозиторий вернет свое значение, все пулы потоков будут простаивать, и Espresso будет знать, что это хорошо, чтобы продвинуть тест. Как я уже говорил выше, это похоже на устройства под управлением Marshmallow (и, возможно, даже более высоких версий), но не работает на устройствах на Pie.

Вот упрощенный пример моего UseCase:

repository
    .authenticate(params.username, params.password)
    .subscribeOn(My Use Case Thread Pool)
    .observeOn(Main Thread)

Вот упрощенный пример метода auth в моем классе репозитория:

Single.zip(networkLayer.authenticate(username, password), 
    userDao.getUserInfo(), 
    BiFunction { t1, t2 -> Result(t1,t2) }
    .flatMap {
         Single.just(deleteAllDataFromDatabase())
             .flatMap {
                 networkLayer.getRestOfData()
             }
    }

Что я действительно не могу понять, так это то, почему тест выполняется на некоторых версиях, но не на других. Я также не могу понять, почему, если я смотрю все пулы потоков на бездействие, как Espresso может продвинуть тест? Не будет ли хотя бы один из моих пулов потоков что-то делать (либо извлекать данные из сети, записывать в базу данных, либо делать что-то в сценарии использования? Чего мне не хватает?

1 Ответ

0 голосов
/ 17 мая 2019

Вам нужно переместить всю асинхронную логику в одном потоке. Вы можете сделать это, используя это правило тестирования для RxJava / RxAndroid

class ImmediateSchedulersRule : TestRule {
    override fun apply(base: Statement, description: Description): Statement {
        return object : Statement() {
            @Throws(Throwable::class)
            override fun evaluate() {
                RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
                RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() }
                RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
                RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
                try {
                    base.evaluate()
                } finally {
                    RxJavaPlugins.reset()
                    RxAndroidPlugins.reset()
                }
            }
        }
    }
}

И добавьте это правило к своим классам тестирования:

@get:Rule
var immediateRule = ImmediateSchedulersRule()

Кстати, если вы можете реорганизовать свой вариант использования, лучшим решением может быть передача объекта Scheduler в каждый из ваших вариантов использования.

Примерно так:

Имейте планировщики в своем собственном файле и создайте по умолчанию:

interface AppScheduler {
    fun io(): Scheduler
    fun computation(): Scheduler
    fun main(): Scheduler
}

class DefaultAppScheduler : AppScheduler {
    override fun io() = Schedulers.io()
    override fun computation() = Schedulers.computation()
    override fun main() = AndroidSchedulers.mainThread()
}

В вашем случае использования добавлен AppScheduler (или передан вручную, если вы не используете Dagger или аналогичный), и в обычном приложении вы пропустите реализацию DefaultAppScheduler.

class TestUseCase @Inject constructor(
    private val scheduler: AppScheduler,
    private val yourRepository: YourRepository
) : BaseUseCase<Unit, List<Object>>() {

    //I have omitted some details here but you can get the point
    fun createObservable(params: Unit): Flowable<List<Object>> {
        return yourRepository.loadDataList()
            .subscribeOn(scheduler.io())
    } 
}

Затем в тестовом коде вы создадите новую реализацию AppScheduler только для тестирования с использованием trampoline:

class TestAppScheduler : AppScheduler {
    override fun io() = Schedulers.trampoline()
    override fun computation() = Schedulers.trampoline()
    override fun main() = Schedulers.trampoline()
}

Таким образом, ваш тест будет ждать, прежде чем продолжить.

...