-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
EspressoIdlingResource integration #242
Comments
Something like the folowing ? suspend fun IdlingResource.awaitIdle() {
if (isIdleNow) return
suspendCoroutine<Unit> { cont ->
registerIdleTransitionCallback { cont.resume(Unit) }
}
} |
Or maybe you meant a way to create an IdlingResource from a Job ? fun Job.asIdlingResource() = object : IdlingResource {
override fun getName() = "Coroutine job ${this@asIdlingResource}"
override fun isIdleNow() = isCompleted
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
invokeOnCompletion { callback.onTransitionToIdle() }
}
} |
one problem is that the application can be idle when a coroutine is still running (you're observing a receive channel) |
updated original description with details. I'd like the |
This is what I came up with: class JobCheckingDispatcherWrapper(private val parent: CoroutineDispatcher) :
CoroutineDispatcher() {
private val jobs = Collections.newSetFromMap(WeakHashMap<Job, Boolean>())
var completionEvent: (() -> Unit)? = null
override fun dispatch(context: CoroutineContext, block: Runnable) {
context[Job]?.let { addNewJob(it) }
parent.dispatch(context, block)
}
@InternalCoroutinesApi
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
context[Job]?.let { addNewJob(it) }
parent.dispatchYield(context, block)
}
private fun addNewJob(job: Job): Boolean {
job.invokeOnCompletion {
completionEvent?.invoke()
}
return jobs.add(job)
}
@ExperimentalCoroutinesApi
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
context[Job]?.let { addNewJob(it) }
return parent.isDispatchNeeded(context)
}
val isAnyJobRunning: Boolean
get() {
jobs.removeAll { !it.isActive }
return jobs.isNotEmpty()
}
} It seem to work okay for me. It wraps around existing dispatcher and steals its job objects to check whether those are running. I cannot share What this class does not handle though is your |
I would actually like to have a solution that can work similar to how we can swap Dispatchers.Main (so ability to replace IO and Default as well). In my toy app, I wrote a dispatcher like this and replace default and IO dispatchers with it for each test (via reflection 👎 ). I would very much prefer a solution that does work with Dispatchers.IO, Default and Main out of the box so that developers do not need to pass down dispatchers around. It might be still preferred, just shouldn't be mandatory just to make things testable. below is the tracked dispatcher:
|
This is related to the discussion in #890 about testing coroutine framework. It seem that an ability to replace built-in dispatchers Consider the scenario by @ZakTaccardi: When a repository level |
I was checking how this can be implemented but not sure which direction to take. Also, seems like there are 2 use cases here:
I think both are necessary. 2 is useful when interacting with external systems where nothing might be running in the app but it might still be awaiting for a system callback to continue. |
It seems to me that if we add ability to |
We would also need |
#242 (comment) works fine. Should we implement the same ServiceLocator discovery mechanism for the Default dispatcher? (similar to the one in Main?) Or do you have a different implementation in mind? |
The roadmap I have in mind:
|
Wouldn't that be hardcoding implementation details? IO dispatchers is now subview of default dispatchers, but it might not be in the future. |
@elizarov I'm starting to take a look at this a bit more and there's a common test case to consider as a use case:
I'm currently thinking the |
Can you please clarify the code in the above comment. When you say "here I want to assert that a task was dispatched to IO" do you really mean that you want to assert that the task was already complete in IO dispatcher (and it is now idle) or what? What is the expected behavior of test coroutine dispatcher when the task goes to "outside" dispatcher? Shall it wait for all other dispatchers to become idle or... ? |
Ah yes I see the confusion, let me clarify a bit with the rest of the test.
The actual API surface for how to write that assertion could take a few forms. Imo idle status is probably the easiest one to understand.
As an alternative, the arguments passed to withContext could be intercepted directly by the test (in an argument captor style). That would be a great solution that doesn't involve diving into dispatcher implementation to test this - however it is not obvious how one would intercept that since withContext is a top level function. As an alternative, all of this can be solved by wrapping either withContext or Dispatchers.Default behind an appropriate abstraction. However, it would be better if the APIs exposed a testable surface. The important part, to me, is that I need to have some way of determining that IO was dispatched to instead of Default in a test to ensure the correctness of the code. |
Re: Espresso dispatchers (the original issue). After thinking it over I think @yigit has the right direction in #242 (comment) In most cases, an Espresso test does not need (or want) time control and should simply wrap the existing Dispatchers with instrumentation. Both of the wrappers Yigit spelled out make sense in different cases, with the default option of "idle when nothing is currently running or queued." A delegate pattern would be a great to add this tracking:
The second option, "idle when all jobs that have ever passed through the dispatcher are complete" is a more complicated (and surprising)
In both cases, I don't think the correct choice would be to use Q: Are there any use cases that would require a
|
I agree with you: If a UI test did need to control the return order of multiple coroutines, it could do so without TestCoroutineDispatcher by supplying fakes and mocks. But, would it be hard to allow What if an app uses |
Yea, once you have anything other than a one shot request you'd have to use a From a larger perspective - the underlying resource that's causing the suspend (e.g. Retrofit, Room etc) should also expose an idling resource in this case to tell espresso work is happening, or the code should be updated to use a counting idling resource. If the underlying resource exposed an idling resource, the flow would be complicated but create the desired effect. Consider a streaming database read. (all idle) -> (coroutine idle, database busy) -> (database busy, coroutine busy) -> (database idle, coroutine busy) -> result sent to UI which blocks main -> (all idle) Q: Integrating with
|
We have been using a delegate pattern as mentioned in objcode's comment. It is a similar idea to the code written by @yigit but allows tests to behave more similarly to production code by wrapping production dispatchers. class EspressoTrackedDispatcher(private val wrappedCoroutineDispatcher: CoroutineDispatcher) : CoroutineDispatcher() {
private val counter: CountingIdlingResource = CountingIdlingResource("EspressoTrackedDispatcher for $wrappedCoroutineDispatcher")
init {
IdlingRegistry.getInstance().register(counter)
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
counter.increment()
val blockWithDecrement = Runnable {
try {
block.run()
} finally {
counter.decrement()
}
}
wrappedCoroutineDispatcher.dispatch(context, blockWithDecrement)
}
fun cleanUp() {
IdlingRegistry.getInstance().unregister(counter)
}
} I'm then using the above Dispatcher in a TestRule: class DispatcherIdlerRule: TestRule {
override fun apply(base: Statement?, description: Description?): Statement =
object : Statement() {
override fun evaluate() {
val espressoTrackedDispatcherIO = EspressoTrackedDispatcher(Dispatchers.IO)
val espressoTrackedDispatcherDefault = EspressoTrackedDispatcher(Dispatchers.Default)
MyDispatchers.IO = espressoTrackedDispatcherIO
MyDispatchers.Default = espressoTrackedDispatcherDefault
try {
base?.evaluate()
} finally {
espressoTrackedDispatcherIO.cleanUp()
espressoTrackedDispatcherDefault.cleanUp()
MyDispatchers.resetAll()
}
}
}
} In the absence of object MyDispatchers {
var Main: CoroutineDispatcher = Dispatchers.Main
var IO: CoroutineDispatcher = Dispatchers.IO
var Default: CoroutineDispatcher = Dispatchers.Default
fun resetMain() {
Main = Dispatchers.Main
}
fun resetIO() {
IO = Dispatchers.IO
}
fun resetDefault() {
Default = Dispatchers.Default
}
fun resetAll() {
resetMain()
resetIO()
resetDefault()
}
} With this approach, we've unblocked our Espresso testing. Caveat: For our use case we don't currently need it to behave more effectively but the above code tells Espresso that the app is idle during a Hope this helps for anyone else struggling with this issue |
I have found that simply monitoring a Imagine the following scenario : 2 coroutines pass data between each other using EDIT: this seems to be the scenario that @objcode raised an issue for #1202 (comment) |
You can use CoroutineDispatcher as a hook to get all the jobs though, and then listen to all jobs manually: #242 (comment) |
Thank you guys, I'm glad that so many people shared their implementation of dispatcher wrappers. You're amaizing 👍 I used to work with Rx using Rx Idler which is basically scheduler wrapper, example: override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
disposable = Single.create<Int> {
Thread.sleep(3000)
it.onSuccess(1)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.computation())
.subscribe { next ->
textView.text = next.toString()
}
}
@Test
fun testExample() {
onView(withId(R.id.textView)).check(matches(withText("1")))
} Example works fine because result of computation schedules to UI thread and then computation scheduler marked as Idle. Almost always (I run this test for a half an hour in cycle with 100% success execution, but I think it should fails sometime) UI thread have enough time to update view with result of computation before test checks text view. I tried to do something similar for coroutines using dispatcher wrapper provided by @matejdro private val _state = MutableLiveData<ScreenState>()
val state by lazy<LiveData<ScreenState>> {
_state.value = ScreenState.Loading
viewModelScope.launch {
_state.value = ScreenState.Loaded(useCase.getFeatureData())
}
_state
}
class FeatureUseCaseImpl @Inject constructor(
@IODispatcher private val ioDispatcher: CoroutineDispatcher
): FeatureUseCase {
override suspend fun getFeatureData(): FeatureData = withContext(ioDispatcher) {
delay(1500)
FeatureData("boo")
}
}
@Test
fun testDataUpdated() {
onView(withId(R.id.textView)).check(matches(withText("boo")))
} When you run the test you can see how UI is updated with the text 'boo', but test fails. The trick is coroutines have different order of execution rather then in Rx. With coroutines dispatcher wrapper is notified that task is completed and only then result is dispatched to Main dispatcher to update UI. As a result, test checks UI few milliseconds before it's updated. I tried to fix it by modifying @matejdro `s dispatcher wrapper: private const val ONE_FRAME = 17L
private fun addNewJob(job: Job): Boolean {
job.invokeOnCompletion {
if (isAnyJobRunning.not()) {
GlobalScope.launch(Dispatchers.Main) {
delay(ONE_FRAME * 2)
completionEvent?.invoke()
}
}
}
return jobs.add(job)
} This workaround works, but not very stable, if you run test in cycle for about half an hour it would fails at least once. @matejdro , does your dispatcher work stable at your project? Can you suggest something to improve stability? |
What is the current status about this? I believe that something like @LUwaisA 's solution is the right way to go.
An example of how this could be implemented is In the meantime, a workaround is to replace |
Check this file. It might be useful. It's fully decoupled from development code. No single line was written in development package. Easy to integrate in any app. https://github.com/sanjeevirajm/simple_idling_resource_android/blob/main/README.md A thread named "idlingMonitor" will keep checking whether any other background thread is running for every 20ms and notify EspressoIdlingResource. |
Please tell me that "Close" was a mistake... 🫣 What would be necessary to get first-party support? Would Google (android-test) consider taking this? Are all the APIs existing and open to implement this correctly without hacking? |
Unfortunately, the issue is out of our scope. We haven't even decided on #982 during all these years, and there is always more close-to-the-core important work that cannot be pushed towards the 3rd-party-integration side. |
I had a working solution that worked with version 1.5.2 by creating my own subclass of TestDispatcher. But now trying to upgrade to 1.6.0 I find my old solution is impossible, because a TestScope cannot have a custom implementation of CoroutineDispatcher, it must be an instance of TestCoroutineDispatcher, but that class is locked down and you cannot extend it in any way. |
@dalewking could you please file a separate issue regarding |
To answer the last comment the easiest change would be to simply make TestDispatcher an interface or at least expose its constructor so users can provide their own implementation for tests. In my case I wanted to simply use delegation to wrap a regular instance of TestDispatcher so that I could then control and Espresso Idling Resource. Someone asked me for my solution to this with 1.5.2 and here is what I did with 1.5.2, but this solution is not possible with anything after 1.5.2 because TestDispatcher cannot be subclassed due to its constructor being internal and to use Test coroutines you have to have an instance of TestDispatcher:
|
Only for the top level. You can do
I'm not sure it would help. I didn't check specifically, but it looks like, if we just made |
@dkhalanskyjb I might be wrong here, but that's not how Espresso works. In Espresso tests, there's no Example: After |
Then it's irrelevant to the discussion with @dalewking, whose complaint is specifically about being unable to pass a wrapped dispatcher as an argument to Though the explanation is helpful, thanks! |
That's funny because nowhere did I ever mention runTest. But I do use runTest (actually runBlockingTest because I am stuck at 1.5.2 because of this issue) in my espresso tests. I have methods to launch a fragment for the test that is wrapped in one. May not strictly be necessary but is convenient. But that is beside the point in this discussion because runTest is not the only way to get a testing coroutine scope to be used. My code is set up to inject scopes into the code beneath the UI using dependency injection so I inject testing scopes that way. There is also the possibility of injecting a test scope using the Dispatchers support in kotlinx-coroutines-test (which I don't use due to being on 1.5.2, which in reality is 1.5.2 copied into my code base to allow it to support iOS). The real requirement here for hooking it up to Espresso is to have some way to be notified when a test scope has added something to the queue and also to know when something that was added to the queue has now finished executing. The way I did that in 1.5.2 was to subclass CoroutineDispatcher to wrap the true dispatcher and wrap the bit of work to execute in a bit of code that incremented and decremented a CountingIdlingResource. That no longer works because you require a TestDispatcher which no one on the outside can implement. That certainly is not the only way this could be done. Another way would simply be the ability to register a listener that is called when work is being added to the queue and when that work completes. Another possibility is to allow registering a function that can transform any bit of work. |
sorry, jumping on the end of this here, @dalewking have you had any luck on this? I've been trying a few different things but without luck |
I have only been able to do it because we have copied the source code for coroutines-test in our project and I modified it to basically create an interface for TestDispatcher then I could create my own implementation of that interface that delegates to a real instance. What we basically need is a way to intercept and replace the "markers" that the dispatcher is sending to the scheduler. The idea being that we increment the CountingIdlingResource when the dispatcher registers an event with the scheduler and the marker is changed to decrement the counter when the block finishes (or in the case of CancellableContinuation it is cancelled). This could be done by extending the dispatcher of the scheduler like I am doing, but that is not possible because they locked them down to only allow the class that they wrote. Alternatively they could provide a mechanism to register an interceptor that allow modification. I'm definitely not done investigating this, but I have something that works for us for now and am in the process of completing upgrade to Kotlin 1.7. After that I need to reinvestigate our whole coroutine strategy.
|
OK, I have figured out how to do it and it is actually pretty straightforward. My problem was that I was trying to create my own dispatcher that controlled a CountingIdlingResource and pass that into the constructor to TestScope. That will not work as TestScope can only be called with a TestCoroutineDispatcher. However the solution is to create a TestScope and then add to it a dispatcher that controls the Idling resource and defers to another CoroutineDispatcher. So here is my wrapper class now:
|
@LUwaisA's solution does seem to work for me with production dispatchers when I use To address this, I can see other's have an implementation of the Is there an alternative way to handle the delay function invocations? Wanted to avoid swapping out production Dispatchers with TestDispatchers just for supporting this use case. |
This is interesting. Should it? Arguably, when there is pending work, the system is still idle. For example, the RxJava |
Your assessment seems correct to me. This IMO is the idling system pointing out very subtle issues in your test logic. Where I have seen it bite us is on an RPC based search where you Initial workaround was a Then later we reworked the test code to loop the test thread watching the view hierarchy until said work triggered a matching view tree update with either an error or success condition. Very important to stop the view actions wait loop on both error and success conditions so you can fail as fast as possible. Looks roughly like this: onView(isRoot())
.perform(
waitForView(
withId(R.id.some_text_id),
withId(R.id.some_error_id),
)
) fun waitForView(vararg matchers: Matcher<View>, timeout: Duration = 5.seconds): ViewAction {
return object : ViewAction {
private val timeoutMillis = timeout.inWholeMilliseconds
override fun getConstraints() = isRoot()
override fun getDescription(): String {
val subDescription = StringDescription()
matchers.forEach { it.describeTo(subDescription) }
return "Wait for a view matching one of: $subDescription; with a timeout of $timeout."
}
override fun perform(uiController: UiController, rootView: View) {
uiController.loopMainThreadUntilIdle()
val startTime = System.currentTimeMillis()
val endTime = startTime + timeoutMillis
do {
for (child in TreeIterables.breadthFirstViewTraversal(rootView)) {
if (matchers.any { matcher -> matcher.matches(child) }) {
return
}
}
uiController.loopMainThreadForAtLeast(100)
} while (System.currentTimeMillis() < endTime)
throw PerformException.Builder()
.withCause(TimeoutException())
.withActionDescription(this.description)
.withViewDescription(HumanReadables.describe(rootView))
.build()
}
}
} Another option would be to put an |
Would be nice to have a wrapper to easily provide support for IdlingResource.
Some examples
EDIT: I'm currently injecting my
CoroutineDispatchers
like so:For espresso testing, which monitors the async task pool and UI thread for idle conditions, I'm injecting the following:
Here's the problem I'm experiencing, which happens about 1% of the time on our automated tests.
ConflatedBroadcastChannel
is updated with the information from the network call.background
dispatcherConflatedBroadcastChannel
in theViewModel
(which has been observing the repository levelConflatedBroadcastChannel
the whole time) is updatedbackground
dispatcherThe text was updated successfully, but these errors were encountered: