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;
}







1















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?










share|improve this question

























  • 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













  • 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











  • 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


















1















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?










share|improve this question

























  • 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













  • 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











  • 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














1












1








1








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?










share|improve this question
















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






share|improve this question















share|improve this question













share|improve this question




share|improve this question








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 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













  • 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











  • 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











  • @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











  • @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

















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












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
});


}
});














draft saved

draft discarded


















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
















draft saved

draft discarded




















































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.




draft saved


draft discarded














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





















































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







Popular posts from this blog

"Incorrect syntax near the keyword 'ON'. (on update cascade, on delete cascade,)

Alcedinidae

Origin of the phrase “under your belt”?