Best approach for unit-testing scoped viewmodels
When dealing with coroutines inside a viewModel is best to have said viewModel implement CoroutineScope
so all coroutines are cancelled when the viewModel is cleared. Usually I see coroutineContext
defined as Dispatchers.Main + _job
so that coroutines are executed in the main UI thread by default. Usually this is done on a open class so that all your viewModels can extend it and get the scope without boilerplate code.
The issue arises when trying to unit test said viewModels as Dispatchers.Main
is not available and trying to use it throws an exception. I am tryin to find a good solution that doesn't involve external libraries or too much boiler plate on the child viewModels.
My current solution is to add the maincontext as a contructor paramenter with the Dispatchers.Main
as the default value. Then in the unit test, before testing the viewModel I set it to Dispatchers.Default
. I don't quiet like this solution as it exposes the coroutineContext implementation details for everyone to see and change:
open class ScopedViewModel(var maincontext = Dispatchers.Main) : ViewModel(), CoroutineScope {
private val _job = Job()
override val coroutineContext: CoroutineContext
get() = maincontext + _job
override fun onCleared() {
super.onCleared()
_job.cancel()
}
}
class MyViewModel : ScopedViewModel() {}
In the tests:
fun setup(){
viewModel = MyViewModel()
viewModel.maincontext = Dispacther.Default
}
android unit-testing kotlin kotlinx.coroutines android-viewmodel
add a comment |
When dealing with coroutines inside a viewModel is best to have said viewModel implement CoroutineScope
so all coroutines are cancelled when the viewModel is cleared. Usually I see coroutineContext
defined as Dispatchers.Main + _job
so that coroutines are executed in the main UI thread by default. Usually this is done on a open class so that all your viewModels can extend it and get the scope without boilerplate code.
The issue arises when trying to unit test said viewModels as Dispatchers.Main
is not available and trying to use it throws an exception. I am tryin to find a good solution that doesn't involve external libraries or too much boiler plate on the child viewModels.
My current solution is to add the maincontext as a contructor paramenter with the Dispatchers.Main
as the default value. Then in the unit test, before testing the viewModel I set it to Dispatchers.Default
. I don't quiet like this solution as it exposes the coroutineContext implementation details for everyone to see and change:
open class ScopedViewModel(var maincontext = Dispatchers.Main) : ViewModel(), CoroutineScope {
private val _job = Job()
override val coroutineContext: CoroutineContext
get() = maincontext + _job
override fun onCleared() {
super.onCleared()
_job.cancel()
}
}
class MyViewModel : ScopedViewModel() {}
In the tests:
fun setup(){
viewModel = MyViewModel()
viewModel.maincontext = Dispacther.Default
}
android unit-testing kotlin kotlinx.coroutines android-viewmodel
add a comment |
When dealing with coroutines inside a viewModel is best to have said viewModel implement CoroutineScope
so all coroutines are cancelled when the viewModel is cleared. Usually I see coroutineContext
defined as Dispatchers.Main + _job
so that coroutines are executed in the main UI thread by default. Usually this is done on a open class so that all your viewModels can extend it and get the scope without boilerplate code.
The issue arises when trying to unit test said viewModels as Dispatchers.Main
is not available and trying to use it throws an exception. I am tryin to find a good solution that doesn't involve external libraries or too much boiler plate on the child viewModels.
My current solution is to add the maincontext as a contructor paramenter with the Dispatchers.Main
as the default value. Then in the unit test, before testing the viewModel I set it to Dispatchers.Default
. I don't quiet like this solution as it exposes the coroutineContext implementation details for everyone to see and change:
open class ScopedViewModel(var maincontext = Dispatchers.Main) : ViewModel(), CoroutineScope {
private val _job = Job()
override val coroutineContext: CoroutineContext
get() = maincontext + _job
override fun onCleared() {
super.onCleared()
_job.cancel()
}
}
class MyViewModel : ScopedViewModel() {}
In the tests:
fun setup(){
viewModel = MyViewModel()
viewModel.maincontext = Dispacther.Default
}
android unit-testing kotlin kotlinx.coroutines android-viewmodel
When dealing with coroutines inside a viewModel is best to have said viewModel implement CoroutineScope
so all coroutines are cancelled when the viewModel is cleared. Usually I see coroutineContext
defined as Dispatchers.Main + _job
so that coroutines are executed in the main UI thread by default. Usually this is done on a open class so that all your viewModels can extend it and get the scope without boilerplate code.
The issue arises when trying to unit test said viewModels as Dispatchers.Main
is not available and trying to use it throws an exception. I am tryin to find a good solution that doesn't involve external libraries or too much boiler plate on the child viewModels.
My current solution is to add the maincontext as a contructor paramenter with the Dispatchers.Main
as the default value. Then in the unit test, before testing the viewModel I set it to Dispatchers.Default
. I don't quiet like this solution as it exposes the coroutineContext implementation details for everyone to see and change:
open class ScopedViewModel(var maincontext = Dispatchers.Main) : ViewModel(), CoroutineScope {
private val _job = Job()
override val coroutineContext: CoroutineContext
get() = maincontext + _job
override fun onCleared() {
super.onCleared()
_job.cancel()
}
}
class MyViewModel : ScopedViewModel() {}
In the tests:
fun setup(){
viewModel = MyViewModel()
viewModel.maincontext = Dispacther.Default
}
android unit-testing kotlin kotlinx.coroutines android-viewmodel
android unit-testing kotlin kotlinx.coroutines android-viewmodel
asked Nov 26 '18 at 10:28
Eric MartoriEric Martori
879411
879411
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
Personally I copied a solution from RxJava2: if your test runs against RxJava2 flow which includes two or more different schedulers, you want, sure, all of them to actually run in a single thread.
Here is how it is done with RxJava2 testing:
@BeforeClass
public static void prepare() {
RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setSingleSchedulerHandler(scheduler -> Schedulers.trampoline());
RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
}
I did the same for coroutines. Just have created a class which collects dispatchers, but these dispatchers can be changed.
object ConfigurableDispatchers {
@JvmStatic
@Volatile
var Default: CoroutineDispatcher = Dispatchers.Default
@JvmStatic
@Volatile
var Main: MainCoroutineDispatcher = Dispatchers.Main
...
}
And, inside @BeforeClass
method I call
@ExperimentalCoroutinesApi
fun setInstantMainDispatcher() {
Main = object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = this
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
}
That will guarantee that the block will be executed in the calling thread.
It is the only alternative I found to constructor injection.
Given yourdispatch
implementation, you could returnthis
fromimmediate
.
– Marko Topolnik
Nov 26 '18 at 10:55
You're right. I'll edit the post. Thank you
– Andrey Ilyunin
Nov 26 '18 at 10:56
Would justUnconfined
work without a custom implementation?
– Marko Topolnik
Nov 26 '18 at 10:56
If you meanMainCoroutineDispatcher
, then worth mentioning thatDispatchers.Unconfined
has a type ofCoroutineDispatcher
, whereasMainCoroutineDispatcher
is a subtype ofCoroutineDispatcher
– Andrey Ilyunin
Nov 26 '18 at 10:58
1
The type is there for a reason, that's for sure. It's an optimization measure. The only question is whether you gain anything by statically exposing it. Another point:immediate
will never be used if you always returntrue
fromisDispatchNeeded
.
– Marko Topolnik
Nov 26 '18 at 11:17
|
show 6 more comments
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53479143%2fbest-approach-for-unit-testing-scoped-viewmodels%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
Personally I copied a solution from RxJava2: if your test runs against RxJava2 flow which includes two or more different schedulers, you want, sure, all of them to actually run in a single thread.
Here is how it is done with RxJava2 testing:
@BeforeClass
public static void prepare() {
RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setSingleSchedulerHandler(scheduler -> Schedulers.trampoline());
RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
}
I did the same for coroutines. Just have created a class which collects dispatchers, but these dispatchers can be changed.
object ConfigurableDispatchers {
@JvmStatic
@Volatile
var Default: CoroutineDispatcher = Dispatchers.Default
@JvmStatic
@Volatile
var Main: MainCoroutineDispatcher = Dispatchers.Main
...
}
And, inside @BeforeClass
method I call
@ExperimentalCoroutinesApi
fun setInstantMainDispatcher() {
Main = object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = this
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
}
That will guarantee that the block will be executed in the calling thread.
It is the only alternative I found to constructor injection.
Given yourdispatch
implementation, you could returnthis
fromimmediate
.
– Marko Topolnik
Nov 26 '18 at 10:55
You're right. I'll edit the post. Thank you
– Andrey Ilyunin
Nov 26 '18 at 10:56
Would justUnconfined
work without a custom implementation?
– Marko Topolnik
Nov 26 '18 at 10:56
If you meanMainCoroutineDispatcher
, then worth mentioning thatDispatchers.Unconfined
has a type ofCoroutineDispatcher
, whereasMainCoroutineDispatcher
is a subtype ofCoroutineDispatcher
– Andrey Ilyunin
Nov 26 '18 at 10:58
1
The type is there for a reason, that's for sure. It's an optimization measure. The only question is whether you gain anything by statically exposing it. Another point:immediate
will never be used if you always returntrue
fromisDispatchNeeded
.
– Marko Topolnik
Nov 26 '18 at 11:17
|
show 6 more comments
Personally I copied a solution from RxJava2: if your test runs against RxJava2 flow which includes two or more different schedulers, you want, sure, all of them to actually run in a single thread.
Here is how it is done with RxJava2 testing:
@BeforeClass
public static void prepare() {
RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setSingleSchedulerHandler(scheduler -> Schedulers.trampoline());
RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
}
I did the same for coroutines. Just have created a class which collects dispatchers, but these dispatchers can be changed.
object ConfigurableDispatchers {
@JvmStatic
@Volatile
var Default: CoroutineDispatcher = Dispatchers.Default
@JvmStatic
@Volatile
var Main: MainCoroutineDispatcher = Dispatchers.Main
...
}
And, inside @BeforeClass
method I call
@ExperimentalCoroutinesApi
fun setInstantMainDispatcher() {
Main = object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = this
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
}
That will guarantee that the block will be executed in the calling thread.
It is the only alternative I found to constructor injection.
Given yourdispatch
implementation, you could returnthis
fromimmediate
.
– Marko Topolnik
Nov 26 '18 at 10:55
You're right. I'll edit the post. Thank you
– Andrey Ilyunin
Nov 26 '18 at 10:56
Would justUnconfined
work without a custom implementation?
– Marko Topolnik
Nov 26 '18 at 10:56
If you meanMainCoroutineDispatcher
, then worth mentioning thatDispatchers.Unconfined
has a type ofCoroutineDispatcher
, whereasMainCoroutineDispatcher
is a subtype ofCoroutineDispatcher
– Andrey Ilyunin
Nov 26 '18 at 10:58
1
The type is there for a reason, that's for sure. It's an optimization measure. The only question is whether you gain anything by statically exposing it. Another point:immediate
will never be used if you always returntrue
fromisDispatchNeeded
.
– Marko Topolnik
Nov 26 '18 at 11:17
|
show 6 more comments
Personally I copied a solution from RxJava2: if your test runs against RxJava2 flow which includes two or more different schedulers, you want, sure, all of them to actually run in a single thread.
Here is how it is done with RxJava2 testing:
@BeforeClass
public static void prepare() {
RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setSingleSchedulerHandler(scheduler -> Schedulers.trampoline());
RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
}
I did the same for coroutines. Just have created a class which collects dispatchers, but these dispatchers can be changed.
object ConfigurableDispatchers {
@JvmStatic
@Volatile
var Default: CoroutineDispatcher = Dispatchers.Default
@JvmStatic
@Volatile
var Main: MainCoroutineDispatcher = Dispatchers.Main
...
}
And, inside @BeforeClass
method I call
@ExperimentalCoroutinesApi
fun setInstantMainDispatcher() {
Main = object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = this
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
}
That will guarantee that the block will be executed in the calling thread.
It is the only alternative I found to constructor injection.
Personally I copied a solution from RxJava2: if your test runs against RxJava2 flow which includes two or more different schedulers, you want, sure, all of them to actually run in a single thread.
Here is how it is done with RxJava2 testing:
@BeforeClass
public static void prepare() {
RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setSingleSchedulerHandler(scheduler -> Schedulers.trampoline());
RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
}
I did the same for coroutines. Just have created a class which collects dispatchers, but these dispatchers can be changed.
object ConfigurableDispatchers {
@JvmStatic
@Volatile
var Default: CoroutineDispatcher = Dispatchers.Default
@JvmStatic
@Volatile
var Main: MainCoroutineDispatcher = Dispatchers.Main
...
}
And, inside @BeforeClass
method I call
@ExperimentalCoroutinesApi
fun setInstantMainDispatcher() {
Main = object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = this
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
}
That will guarantee that the block will be executed in the calling thread.
It is the only alternative I found to constructor injection.
edited Nov 26 '18 at 10:56
answered Nov 26 '18 at 10:49
Andrey IlyuninAndrey Ilyunin
1,234223
1,234223
Given yourdispatch
implementation, you could returnthis
fromimmediate
.
– Marko Topolnik
Nov 26 '18 at 10:55
You're right. I'll edit the post. Thank you
– Andrey Ilyunin
Nov 26 '18 at 10:56
Would justUnconfined
work without a custom implementation?
– Marko Topolnik
Nov 26 '18 at 10:56
If you meanMainCoroutineDispatcher
, then worth mentioning thatDispatchers.Unconfined
has a type ofCoroutineDispatcher
, whereasMainCoroutineDispatcher
is a subtype ofCoroutineDispatcher
– Andrey Ilyunin
Nov 26 '18 at 10:58
1
The type is there for a reason, that's for sure. It's an optimization measure. The only question is whether you gain anything by statically exposing it. Another point:immediate
will never be used if you always returntrue
fromisDispatchNeeded
.
– Marko Topolnik
Nov 26 '18 at 11:17
|
show 6 more comments
Given yourdispatch
implementation, you could returnthis
fromimmediate
.
– Marko Topolnik
Nov 26 '18 at 10:55
You're right. I'll edit the post. Thank you
– Andrey Ilyunin
Nov 26 '18 at 10:56
Would justUnconfined
work without a custom implementation?
– Marko Topolnik
Nov 26 '18 at 10:56
If you meanMainCoroutineDispatcher
, then worth mentioning thatDispatchers.Unconfined
has a type ofCoroutineDispatcher
, whereasMainCoroutineDispatcher
is a subtype ofCoroutineDispatcher
– Andrey Ilyunin
Nov 26 '18 at 10:58
1
The type is there for a reason, that's for sure. It's an optimization measure. The only question is whether you gain anything by statically exposing it. Another point:immediate
will never be used if you always returntrue
fromisDispatchNeeded
.
– Marko Topolnik
Nov 26 '18 at 11:17
Given your
dispatch
implementation, you could return this
from immediate
.– Marko Topolnik
Nov 26 '18 at 10:55
Given your
dispatch
implementation, you could return this
from immediate
.– Marko Topolnik
Nov 26 '18 at 10:55
You're right. I'll edit the post. Thank you
– Andrey Ilyunin
Nov 26 '18 at 10:56
You're right. I'll edit the post. Thank you
– Andrey Ilyunin
Nov 26 '18 at 10:56
Would just
Unconfined
work without a custom implementation?– Marko Topolnik
Nov 26 '18 at 10:56
Would just
Unconfined
work without a custom implementation?– Marko Topolnik
Nov 26 '18 at 10:56
If you mean
MainCoroutineDispatcher
, then worth mentioning that Dispatchers.Unconfined
has a type of CoroutineDispatcher
, whereas MainCoroutineDispatcher
is a subtype of CoroutineDispatcher
– Andrey Ilyunin
Nov 26 '18 at 10:58
If you mean
MainCoroutineDispatcher
, then worth mentioning that Dispatchers.Unconfined
has a type of CoroutineDispatcher
, whereas MainCoroutineDispatcher
is a subtype of CoroutineDispatcher
– Andrey Ilyunin
Nov 26 '18 at 10:58
1
1
The type is there for a reason, that's for sure. It's an optimization measure. The only question is whether you gain anything by statically exposing it. Another point:
immediate
will never be used if you always return true
from isDispatchNeeded
.– Marko Topolnik
Nov 26 '18 at 11:17
The type is there for a reason, that's for sure. It's an optimization measure. The only question is whether you gain anything by statically exposing it. Another point:
immediate
will never be used if you always return true
from isDispatchNeeded
.– Marko Topolnik
Nov 26 '18 at 11:17
|
show 6 more comments
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53479143%2fbest-approach-for-unit-testing-scoped-viewmodels%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown