Coroutines unit testing: exceptions are being swallowed and test passes
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{ height:90px;width:728px;box-sizing:border-box;
}
I have the next test function:
@Test
fun `registerUser verify that loading was emitted`() {
runBlocking {
var emission = 0
val viewModel = createSubject()
viewModel.loading.observeForever {
if (emission == 0) {
assertNotNull(it)
assertFalse(it!!) //MARK #1
emission++
}
}
async { viewModel.registerCommand.registerUser("asd") }.await()
assertNotNull(viewModel.loading.value)
}
}
I've wrapped registerUser
invocation with async
and await
according to this.
Inside registerUser
:
fun registerUser(username: String) {
launch {...
withContext(ConfigurableDispatchers.IO) {...}
...
}
}
Whereas the class which has this function as a member inherits CoroutineScope
and overrides coroutineContext
with ConfigurableDispatchers.Main + job
, whereas durint testing (in @BeforeClass
-annotated method) ConfigurableDispatchers.Main
is stubbed with
object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = throw UnsupportedOperationException()
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
Also, ConfigurableDispatchers.IO
is stubbed with Dispatchers.Unconfined
.
The problem is that assertion exception thrown from MARK #1
is just being printed in the console and than just swallowed up. Test passes...
As I can see and as I've debugged - all this method body is being executed in a single thread, so if exception is being caught at the default UncaughtExceptionHandler
, then, as I understand, **test should fail **.
What is also interesting, when I wrap MARK #1
(the failing line) with try catch
block, whereas I am catching an Exception
- nothing drops in the handler...
Why?
unit-testing kotlin kotlinx.coroutines
|
show 1 more comment
I have the next test function:
@Test
fun `registerUser verify that loading was emitted`() {
runBlocking {
var emission = 0
val viewModel = createSubject()
viewModel.loading.observeForever {
if (emission == 0) {
assertNotNull(it)
assertFalse(it!!) //MARK #1
emission++
}
}
async { viewModel.registerCommand.registerUser("asd") }.await()
assertNotNull(viewModel.loading.value)
}
}
I've wrapped registerUser
invocation with async
and await
according to this.
Inside registerUser
:
fun registerUser(username: String) {
launch {...
withContext(ConfigurableDispatchers.IO) {...}
...
}
}
Whereas the class which has this function as a member inherits CoroutineScope
and overrides coroutineContext
with ConfigurableDispatchers.Main + job
, whereas durint testing (in @BeforeClass
-annotated method) ConfigurableDispatchers.Main
is stubbed with
object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = throw UnsupportedOperationException()
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
Also, ConfigurableDispatchers.IO
is stubbed with Dispatchers.Unconfined
.
The problem is that assertion exception thrown from MARK #1
is just being printed in the console and than just swallowed up. Test passes...
As I can see and as I've debugged - all this method body is being executed in a single thread, so if exception is being caught at the default UncaughtExceptionHandler
, then, as I understand, **test should fail **.
What is also interesting, when I wrap MARK #1
(the failing line) with try catch
block, whereas I am catching an Exception
- nothing drops in the handler...
Why?
unit-testing kotlin kotlinx.coroutines
There are so many layers here that you probably don't need. One example:registerUser
is not asuspend fun
, wrapping it inasync-await
has no effect. The coroutine still runs on its own andawait
completes immediately. Not that you should be usingasync-await
like that in the first place, it's an established anti-idiom. Then, apparenty youlaunch
and then immedietalywithContext(ConfigurableDispatchers.IO)
. This is the same aslaunch(ConfigurableDispatechers.IO)
– Marko Topolnik
Nov 23 '18 at 20:16
@MarkoTopolnik, thanks, will try that on monday
– Andrey Ilyunin
Nov 24 '18 at 7:22
I think yourregisterUser
should be asuspend fun
that starts withwithContext(ConfigurableDispatchers.IO) { ... body ... }
and then you should pull thelaunch
up into the callers.
– Marko Topolnik
Nov 24 '18 at 7:44
@MarkoTopolnik, "This is the same as launch(ConfigurableDispatechers.IO)" - actually here is an interesting moment: sayinglaunch
I mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pureIO
this is not the case, isn't it?
– Andrey Ilyunin
Nov 26 '18 at 10:34
IO
is just a dispatcher, not a coroutine scope. The choice of dispatcher doesn't affect cancellation behavior. Also, i don't think cancelling a job propagates to its parent. It is only the failure of a job that propagates.
– Marko Topolnik
Nov 26 '18 at 10:38
|
show 1 more comment
I have the next test function:
@Test
fun `registerUser verify that loading was emitted`() {
runBlocking {
var emission = 0
val viewModel = createSubject()
viewModel.loading.observeForever {
if (emission == 0) {
assertNotNull(it)
assertFalse(it!!) //MARK #1
emission++
}
}
async { viewModel.registerCommand.registerUser("asd") }.await()
assertNotNull(viewModel.loading.value)
}
}
I've wrapped registerUser
invocation with async
and await
according to this.
Inside registerUser
:
fun registerUser(username: String) {
launch {...
withContext(ConfigurableDispatchers.IO) {...}
...
}
}
Whereas the class which has this function as a member inherits CoroutineScope
and overrides coroutineContext
with ConfigurableDispatchers.Main + job
, whereas durint testing (in @BeforeClass
-annotated method) ConfigurableDispatchers.Main
is stubbed with
object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = throw UnsupportedOperationException()
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
Also, ConfigurableDispatchers.IO
is stubbed with Dispatchers.Unconfined
.
The problem is that assertion exception thrown from MARK #1
is just being printed in the console and than just swallowed up. Test passes...
As I can see and as I've debugged - all this method body is being executed in a single thread, so if exception is being caught at the default UncaughtExceptionHandler
, then, as I understand, **test should fail **.
What is also interesting, when I wrap MARK #1
(the failing line) with try catch
block, whereas I am catching an Exception
- nothing drops in the handler...
Why?
unit-testing kotlin kotlinx.coroutines
I have the next test function:
@Test
fun `registerUser verify that loading was emitted`() {
runBlocking {
var emission = 0
val viewModel = createSubject()
viewModel.loading.observeForever {
if (emission == 0) {
assertNotNull(it)
assertFalse(it!!) //MARK #1
emission++
}
}
async { viewModel.registerCommand.registerUser("asd") }.await()
assertNotNull(viewModel.loading.value)
}
}
I've wrapped registerUser
invocation with async
and await
according to this.
Inside registerUser
:
fun registerUser(username: String) {
launch {...
withContext(ConfigurableDispatchers.IO) {...}
...
}
}
Whereas the class which has this function as a member inherits CoroutineScope
and overrides coroutineContext
with ConfigurableDispatchers.Main + job
, whereas durint testing (in @BeforeClass
-annotated method) ConfigurableDispatchers.Main
is stubbed with
object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = throw UnsupportedOperationException()
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
Also, ConfigurableDispatchers.IO
is stubbed with Dispatchers.Unconfined
.
The problem is that assertion exception thrown from MARK #1
is just being printed in the console and than just swallowed up. Test passes...
As I can see and as I've debugged - all this method body is being executed in a single thread, so if exception is being caught at the default UncaughtExceptionHandler
, then, as I understand, **test should fail **.
What is also interesting, when I wrap MARK #1
(the failing line) with try catch
block, whereas I am catching an Exception
- nothing drops in the handler...
Why?
unit-testing kotlin kotlinx.coroutines
unit-testing kotlin kotlinx.coroutines
edited Nov 23 '18 at 16:43
Andrey Ilyunin
asked Nov 23 '18 at 16:34
Andrey IlyuninAndrey Ilyunin
1,317224
1,317224
There are so many layers here that you probably don't need. One example:registerUser
is not asuspend fun
, wrapping it inasync-await
has no effect. The coroutine still runs on its own andawait
completes immediately. Not that you should be usingasync-await
like that in the first place, it's an established anti-idiom. Then, apparenty youlaunch
and then immedietalywithContext(ConfigurableDispatchers.IO)
. This is the same aslaunch(ConfigurableDispatechers.IO)
– Marko Topolnik
Nov 23 '18 at 20:16
@MarkoTopolnik, thanks, will try that on monday
– Andrey Ilyunin
Nov 24 '18 at 7:22
I think yourregisterUser
should be asuspend fun
that starts withwithContext(ConfigurableDispatchers.IO) { ... body ... }
and then you should pull thelaunch
up into the callers.
– Marko Topolnik
Nov 24 '18 at 7:44
@MarkoTopolnik, "This is the same as launch(ConfigurableDispatechers.IO)" - actually here is an interesting moment: sayinglaunch
I mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pureIO
this is not the case, isn't it?
– Andrey Ilyunin
Nov 26 '18 at 10:34
IO
is just a dispatcher, not a coroutine scope. The choice of dispatcher doesn't affect cancellation behavior. Also, i don't think cancelling a job propagates to its parent. It is only the failure of a job that propagates.
– Marko Topolnik
Nov 26 '18 at 10:38
|
show 1 more comment
There are so many layers here that you probably don't need. One example:registerUser
is not asuspend fun
, wrapping it inasync-await
has no effect. The coroutine still runs on its own andawait
completes immediately. Not that you should be usingasync-await
like that in the first place, it's an established anti-idiom. Then, apparenty youlaunch
and then immedietalywithContext(ConfigurableDispatchers.IO)
. This is the same aslaunch(ConfigurableDispatechers.IO)
– Marko Topolnik
Nov 23 '18 at 20:16
@MarkoTopolnik, thanks, will try that on monday
– Andrey Ilyunin
Nov 24 '18 at 7:22
I think yourregisterUser
should be asuspend fun
that starts withwithContext(ConfigurableDispatchers.IO) { ... body ... }
and then you should pull thelaunch
up into the callers.
– Marko Topolnik
Nov 24 '18 at 7:44
@MarkoTopolnik, "This is the same as launch(ConfigurableDispatechers.IO)" - actually here is an interesting moment: sayinglaunch
I mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pureIO
this is not the case, isn't it?
– Andrey Ilyunin
Nov 26 '18 at 10:34
IO
is just a dispatcher, not a coroutine scope. The choice of dispatcher doesn't affect cancellation behavior. Also, i don't think cancelling a job propagates to its parent. It is only the failure of a job that propagates.
– Marko Topolnik
Nov 26 '18 at 10:38
There are so many layers here that you probably don't need. One example:
registerUser
is not a suspend fun
, wrapping it in async-await
has no effect. The coroutine still runs on its own and await
completes immediately. Not that you should be using async-await
like that in the first place, it's an established anti-idiom. Then, apparenty you launch
and then immedietaly withContext(ConfigurableDispatchers.IO)
. This is the same as launch(ConfigurableDispatechers.IO)
– Marko Topolnik
Nov 23 '18 at 20:16
There are so many layers here that you probably don't need. One example:
registerUser
is not a suspend fun
, wrapping it in async-await
has no effect. The coroutine still runs on its own and await
completes immediately. Not that you should be using async-await
like that in the first place, it's an established anti-idiom. Then, apparenty you launch
and then immedietaly withContext(ConfigurableDispatchers.IO)
. This is the same as launch(ConfigurableDispatechers.IO)
– Marko Topolnik
Nov 23 '18 at 20:16
@MarkoTopolnik, thanks, will try that on monday
– Andrey Ilyunin
Nov 24 '18 at 7:22
@MarkoTopolnik, thanks, will try that on monday
– Andrey Ilyunin
Nov 24 '18 at 7:22
I think your
registerUser
should be a suspend fun
that starts with withContext(ConfigurableDispatchers.IO) { ... body ... }
and then you should pull the launch
up into the callers.– Marko Topolnik
Nov 24 '18 at 7:44
I think your
registerUser
should be a suspend fun
that starts with withContext(ConfigurableDispatchers.IO) { ... body ... }
and then you should pull the launch
up into the callers.– Marko Topolnik
Nov 24 '18 at 7:44
@MarkoTopolnik, "This is the same as launch(ConfigurableDispatechers.IO)" - actually here is an interesting moment: saying
launch
I mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pure IO
this is not the case, isn't it?– Andrey Ilyunin
Nov 26 '18 at 10:34
@MarkoTopolnik, "This is the same as launch(ConfigurableDispatechers.IO)" - actually here is an interesting moment: saying
launch
I mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pure IO
this is not the case, isn't it?– Andrey Ilyunin
Nov 26 '18 at 10:34
IO
is just a dispatcher, not a coroutine scope. The choice of dispatcher doesn't affect cancellation behavior. Also, i don't think cancelling a job propagates to its parent. It is only the failure of a job that propagates.– Marko Topolnik
Nov 26 '18 at 10:38
IO
is just a dispatcher, not a coroutine scope. The choice of dispatcher doesn't affect cancellation behavior. Also, i don't think cancelling a job propagates to its parent. It is only the failure of a job that propagates.– Marko Topolnik
Nov 26 '18 at 10:38
|
show 1 more comment
0
active
oldest
votes
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%2f53450216%2fcoroutines-unit-testing-exceptions-are-being-swallowed-and-test-passes%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
0
active
oldest
votes
0
active
oldest
votes
active
oldest
votes
active
oldest
votes
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%2f53450216%2fcoroutines-unit-testing-exceptions-are-being-swallowed-and-test-passes%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
There are so many layers here that you probably don't need. One example:
registerUser
is not asuspend fun
, wrapping it inasync-await
has no effect. The coroutine still runs on its own andawait
completes immediately. Not that you should be usingasync-await
like that in the first place, it's an established anti-idiom. Then, apparenty youlaunch
and then immedietalywithContext(ConfigurableDispatchers.IO)
. This is the same aslaunch(ConfigurableDispatechers.IO)
– Marko Topolnik
Nov 23 '18 at 20:16
@MarkoTopolnik, thanks, will try that on monday
– Andrey Ilyunin
Nov 24 '18 at 7:22
I think your
registerUser
should be asuspend fun
that starts withwithContext(ConfigurableDispatchers.IO) { ... body ... }
and then you should pull thelaunch
up into the callers.– Marko Topolnik
Nov 24 '18 at 7:44
@MarkoTopolnik, "This is the same as launch(ConfigurableDispatechers.IO)" - actually here is an interesting moment: saying
launch
I mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pureIO
this is not the case, isn't it?– Andrey Ilyunin
Nov 26 '18 at 10:34
IO
is just a dispatcher, not a coroutine scope. The choice of dispatcher doesn't affect cancellation behavior. Also, i don't think cancelling a job propagates to its parent. It is only the failure of a job that propagates.– Marko Topolnik
Nov 26 '18 at 10:38