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:registerUseris not asuspend fun, wrapping it inasync-awaithas no effect. The coroutine still runs on its own andawaitcompletes immediately. Not that you should be usingasync-awaitlike that in the first place, it's an established anti-idiom. Then, apparenty youlaunchand 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 yourregisterUsershould be asuspend funthat starts withwithContext(ConfigurableDispatchers.IO) { ... body ... }and then you should pull thelaunchup 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: sayinglaunchI mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pureIOthis is not the case, isn't it?
– Andrey Ilyunin
Nov 26 '18 at 10:34
IOis 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:registerUseris not asuspend fun, wrapping it inasync-awaithas no effect. The coroutine still runs on its own andawaitcompletes immediately. Not that you should be usingasync-awaitlike that in the first place, it's an established anti-idiom. Then, apparenty youlaunchand 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 yourregisterUsershould be asuspend funthat starts withwithContext(ConfigurableDispatchers.IO) { ... body ... }and then you should pull thelaunchup 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: sayinglaunchI mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pureIOthis is not the case, isn't it?
– Andrey Ilyunin
Nov 26 '18 at 10:34
IOis 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:registerUseris not asuspend fun, wrapping it inasync-awaithas no effect. The coroutine still runs on its own andawaitcompletes immediately. Not that you should be usingasync-awaitlike that in the first place, it's an established anti-idiom. Then, apparenty youlaunchand 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 yourregisterUsershould be asuspend funthat starts withwithContext(ConfigurableDispatchers.IO) { ... body ... }and then you should pull thelaunchup 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: sayinglaunchI mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pureIOthis is not the case, isn't it?
– Andrey Ilyunin
Nov 26 '18 at 10:34
IOis 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:
registerUseris not asuspend fun, wrapping it inasync-awaithas no effect. The coroutine still runs on its own andawaitcompletes immediately. Not that you should be usingasync-awaitlike that in the first place, it's an established anti-idiom. Then, apparenty youlaunchand 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
registerUsershould be asuspend funthat starts withwithContext(ConfigurableDispatchers.IO) { ... body ... }and then you should pull thelaunchup 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
launchI mean that if this context will be cancelled, then cancel the whole outer coroutine, but with pureIOthis is not the case, isn't it?– Andrey Ilyunin
Nov 26 '18 at 10:34
IOis 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