How to handle Espresso idling resource in Compose Android - android

Pretty new to Compose and I am having an Espresso test to become idle timed out when I run all my tests in the test folder. When I run test alone it does pass, I have tried following instruction from this medium post on the waitUntil but I am still facing the same issue. Here is my code snippet.
// https://medium.com/androiddevelopers/alternatives-to-idling-resources-in-compose-tests-8ae71f9fc473
My question is what am I missing ?
#RunWith(AndroidJUnit4::class)
class PCardTest {
#get:Rule
val composeTestRule = createComposeRule()
private val pd = PData(
"home",
"Dec 1, 2021 - May 31, 2022",
)
#Before
fun setUp() {
composeTestRule.setContent {
PCard(pd = pd)
}
}
#Test
fun should_match_home() {
val t = "T ${pd.home}"
composeTestRule.waitUntilDoesNotExist(hasTestTag("T"))
composeTestRule.onNodeWithText(t).assertExists()
}
#Test
fun start_end_date_should_match() {
val startEndDate = pd.datesFrom
composeTestRule.waitUntilDoesNotExist(hasTestTag("Start"))
composeTestRule.onNodeWithText(startEndDate).assertExists()
}

Related

composeTestRule not work in Scenario() in kaspresso

I'm trying to write Scenario() with composeTestRule in Kaspresso, but I get an error:
Test not setup properly. Use a ComposeTestRule in your test to be able to interact with composables
My scenario for example:
class FillOtp(
) : Scenario() {
override val steps: TestContext<Unit>.() -> Unit = {
val baseTest = BaseTest()
step("Write 0000") {
baseTest.composeTestRule
.onNodeWithTag("test")
.performClick()
MyScreen {
pinEdit {
typeText("0000")
}
continueBtn.click()
}
}
}
}
}
But I have added composeTestRule in BaseTest() class. And composeTestRule works successfully in tests without Scenario()
could you help me solve the problem ?
On another site I was offered the answer:
You can pass it to the constructor from the test:
class MyScenario(semanticsProvider: SemanticsNodeInteractionsProvider) : Scenario() {
override val steps: TestContext<Unit>.() -> Unit = {
semanticsProvider.onNode(...)
}
}
And in test:
scenario(MyScenario(composeTestRule))

Waiting for livedata to complete in UI Espresso test

I'm new to testing and Espresso, so bear with me please.
I have an app with some simple image editing and I have decided to cover it with UI tests.
For starters I have decided to test the initial image uploading, processing and moving to the next screen.
here is the test I came up with so far:
#LargeTest
#RunWith(AndroidJUnit4::class)
class VerifyLoadImage {
lateinit var testContext: Context
#Rule
#JvmField
var mActivityTestRule = ActivityScenarioRule(MainActivity::class.java)
#Before
fun loadContext() {
testContext = InstrumentationRegistry.getInstrumentation().context
}
#Test
fun loadImageToCrop() {
mActivityTestRule.scenario.onActivity { mainActivity ->
// get the activity
val navigationFragment = mainActivity.supportFragmentManager.findFragmentById(R.id.fragmentContainer)
//verify that current fragment displayed is ImagePickerFragment
val currentFragment = navigationFragment?.getDisplayedChildFragment()?.let { it as? ImagePickerFragment }
?: throw AssertionError("currentFragment is not instance of ImagePickerFragment")
//call the method to upload the image from input stream, process it and then navigate to the crop screen
currentFragment.loadBitmapAndOpenCropScreen(AssetInputStreamProvider(testContext, "sample_image.jpg"))
//verify that crop screen is currently displayed
assert(navigationFragment.getDisplayedChildFragment() is CropFragment)
}
}
}
private fun Fragment.getDisplayedChildFragment() = childFragmentManager.fragments.first()
this is the code in currentFragment.loadBitmapAndOpenCropScreen
internal fun loadBitmapAndOpenCropScreen(inputStreamProvider: InputStreamProvider) {
activityViewModel.loadBitmap(inputStreamProvider).observe(viewLifecycleOwner) {
when (it) {
Loading -> showLoading()
is Success -> {
hideLoading()
findNavController().navigate(ImagePickerFragmentDirections.toCrop())
}
is Error -> hideLoading()
}
}
}
the problem is that when testing, the LiveData never changes updates at all [works normally when launching the app].
I would appreciate any help here.
Try InstantTaskExecutorRule
#Rule
#JvmField
var mInstantTaskExecutorRule = InstantTaskExecutorRule()

Android - Testing Fragments With Espresso by Using launchFragmentInContainer Never Completes

My test is never running to completion and I have absolutely no idea why. I can see the toast displayed on my phone's screen. There is absolutely nothing in the logs.
#RunWith(AndroidJUnit4::class)
#SmallTest
class BaseDataFragmentUITest
{
#Test
fun isDisplayingToastWhenFAILED_TO_UPDATE()
{
val fragmentScenario = launchFragmentInContainer<TestBaseDataFragmentImp>()
val toastString: String = context.resources.getString(com.developerkurt.gamedatabase.R.string.data_update_fail)
fragmentScenario.onFragment {
it.handleDataStateChange(BaseRepository.DataState.FAILED_TO_UPDATE)
onView(withText(toastString)).inRoot(withDecorView(not(it.requireActivity().getWindow().getDecorView()))).check(matches(isDisplayed()))
}
}
}
Apparently, Espresso assertions shouldn't be made inside of the onFragment block. So when I wrote the test like this it worked:
#Test
fun isDisplayingToastWhenFAILED_TO_UPDATE()
{
val fragmentScenario = launchFragmentInContainer<TestBaseDataFragmentImp>()
val toastString: String = context.resources.getString(com.developerkurt.gamedatabase.R.string.data_update_fail)
var decorView: View? = null
fragmentScenario.onFragment {
it.handleDataStateChange(BaseRepository.DataState.FAILED_TO_UPDATE)
decorView = it.requireActivity().getWindow().getDecorView()
}
onView(withText(toastString)).inRoot(withDecorView(not(decorView!!))).check(matches(isDisplayed()))
}

Unit testing coroutines runBlockingTest: This job has not completed yet

Please find below a function using a coroutine to replace callback :
override suspend fun signUp(authentication: Authentication): AuthenticationError {
return suspendCancellableCoroutine {
auth.createUserWithEmailAndPassword(authentication.email, authentication.password)
.addOnCompleteListener(activityLifeCycleService.getActivity()) { task ->
if (task.isSuccessful) {
it.resume(AuthenticationError.SignUpSuccess)
} else {
Log.w(this.javaClass.name, "createUserWithEmail:failure", task.exception)
it.resume(AuthenticationError.SignUpFail)
}
}
}
}
Now I would like to unit testing this function. I am using Mockk :
#Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() = runBlockingTest {
val listener = slot<OnCompleteListener<AuthResult>>()
val authentication = mockk<Authentication> {
every { email } returns "email"
every { password } returns "pswd"
}
val task = mockk<Task<AuthResult>> {
every { isSuccessful } returns true
}
every { auth.createUserWithEmailAndPassword("email", "pswd") } returns
mockk {
every { addOnCompleteListener(activity, capture(listener)) } returns mockk()
}
service.signUp(authentication)
listener.captured.onComplete(task)
}
Unfortunately this test failed due to the following exception : java.lang.IllegalStateException: This job has not completed yet
I tried to replace runBlockingTest with runBlocking but the test seems to wait in an infinite loop.
Can someone help me with this UT please?
Thanks in advance
As can be seen in this post:
This exception usually means that some coroutines from your tests were scheduled outside the test scope (more specifically the test dispatcher).
Instead of performing this:
private val networkContext: CoroutineContext = TestCoroutineDispatcher()
private val sut = Foo(
networkContext,
someInteractor
)
fun `some test`() = runBlockingTest() {
// given
...
// when
sut.foo()
// then
...
}
Create a test scope passing test dispatcher:
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val networkContext: CoroutineContext = testDispatcher
private val sut = Foo(
networkContext,
someInteractor
)
Then in test perform testScope.runBlockingTest
fun `some test`() = testScope.runBlockingTest {
...
}
See also Craig Russell's "Unit Testing Coroutine Suspend Functions using TestCoroutineDispatcher"
In case of Flow testing:
Don't use flow.collect directly inside runBlockingTest. It should be wrapped in launch
Don't forget to cancel TestCoroutineScope in the end of a test. It will stop a Flow collecting.
Example:
class CoroutinesPlayground {
private val job = Job()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(job + testDispatcher)
#Test
fun `play with coroutines here`() = testScope.runBlockingTest {
val flow = MutableSharedFlow<Int>()
launch {
flow.collect { value ->
println("Value: $value")
}
}
launch {
repeat(10) { value ->
flow.emit(value)
delay(1000)
}
job.cancel()
}
}
}
This is not an official solution, so use it at your own risk.
This is similar to what #azizbekian posted, but instead of calling runBlocking, you call launch.
As this is using TestCoroutineDispatcher, any tasks scheduled to be run without delay are immediately executed. This might not be suitable if you have several tasks running asynchronously.
It might not be suitable for every case but I hope that it helps for simple cases.
You can also follow up on this issue here:
https://github.com/Kotlin/kotlinx.coroutines/issues/1204
If you know how to solve this using the already existing runBlockingTest and runBlocking, please be so kind and share with the community.
class MyTest {
private val dispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(dispatcher)
#Test
fun myTest {
val apiService = mockk<ApiService>()
val repository = MyRepository(apiService)
testScope.launch {
repository.someSuspendedFunction()
}
verify { apiService.expectedFunctionToBeCalled() }
}
}
According to my understanding, this exception occurs when you are using a different dispatcher in your code inside the runBlockingTest { } block with the one that started runBlockingTest { }.
So in order to avoid this, you first have to make sure you inject Dispatchers in your code, instead of hardcoding it throughout your app. If you haven't done it, there's nowhere to begin because you cannot assign a test dispatcher to your test codes.
Then, in your BaseUnitTest, you should have something like this:
#get:Rule
val coroutineRule = CoroutineTestRule()
#ExperimentalCoroutinesApi
class CoroutineTestRule(
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.setMain(testDispatcher)
}
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
Next step really depends on how you do Depedency Injection. The main point is to make sure your test codes are using coroutineRule.testDispatcher after the injection.
Finally, call runBlockingTest { } from this testDispatcher:
#Test
fun `This should pass`() = coroutineRule.testDispatcher.runBlockingTest {
//Your test code where dispatcher is injected
}
There is an open issue for this problem: https://github.com/Kotlin/kotlinx.coroutines/issues/1204
The solution is to use the CoroutineScope intead of the TestCoroutinScope until the issue is resolved, you can do by replacing
#Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() =
runBlockingTest {
with
#Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() =
runBlocking {
None of these answers quite worked for my setup due to frequent changes in the coroutines API.
This specifically works using version 1.6.0 of kotlin-coroutines-test, added as a testImplementation dependency.
#Test
fun `test my function causes flow emission`() = runTest {
// calling this function will result in my flow emitting a value
viewModel.myPublicFunction("1234")
val job = launch {
// Force my flow to update via collect invocation
viewModel.myMemberFlow.collect()
}
// immediately cancel job
job.cancel()
assertEquals("1234", viewModel.myMemberFlow.value)
}
If you have any
Channel
inside the launch, you must call to
Channel.close()
Example code:
val channel = Channel<Success<Any>>()
val flow = channel.consumeAsFlow()
launch {
channel.send(Success(Any()))
channel.close()
}
runBlockingTest deprecated since 1.6.0 and replaced with runTest.
You need to swap arch background executor with one that execute tasks synchronously. eg. For room suspend functions, live data etc.
You need the following dependency for core testing
androidTestImplementation 'androidx.arch.core:core-testing:2.1.0'
Then add the following at the top of test class
#get:Rule
val instantExecutor = InstantTaskExecutorRule()
Explanations
InstantTaskExecutorRule A JUnit Test Rule that swaps the background executor used by the
Architecture Components with a different one which executes each task
synchronously.
You can use this rule for your host side tests that use Architecture
Components
As I mentioned here about fixing runBlockingTest, maybe it could help you too.
Add this dependency if you don't have it
testImplementation "androidx.arch.core:core-testing:$versions.testCoreTesting" (2.1.0)
Then in your test class declare InstantTaskExecutorRule rule:
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()

JUnit 5 Annotations - Not Working with LiveData / Parameterized Tests

Overview
Expected Behavior
Replace mock object initialization using the mockObject function implementation with annotation syntax initialization for JUnit 5 as outlined in the documentation and Medium post by #oleksiyp.
Current Behavior
The test in question is a parameterized test as outlined by #phauer for JUnit 5 which seems to conflict with #ExtendWith(MockKExtension::class). In order to implement tests with LiveData the test must run synchronously in the local unit test using this InstantExecutorExtension designed by #JeroenMols.
Mock object initialization works as expected with the mockObject function, but fails using the annotation #MockK.
Error
Warning message/Build fail:
Repeatable annotations with non-SOURCE retention are not yet supported.
Implementation
mockObject function implementation (Working as expected)
#ExtendWith(InstantExecutorExtension::class)
class NavigateContentTests {
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
private val contentViewModel = ContentViewModel()
// This is the stream of tests to run in the Parameterized test below.
private fun NavigateContent() = Stream.of(
NavigateContentTest(
isRealtime = false,
feedType = MAIN,
timeframe = DAY,
mockFeedList = mockDbContentListForDay,
mockContent = mockArticleContent),
...)
#BeforeAll
fun beforeAll() { mockkObject(ContentRepository) }
#AfterAll
fun afterAll() { unmockkAll() // Re-assigns transformation of object to original state prior to mock. }
#BeforeEach
fun beforeEach() { Dispatchers.setMain(mainThreadSurrogate) }
#AfterEach
fun afterEach() {
Dispatchers.resetMain() // Reset main dispatcher to the original Main dispatcher.
mainThreadSurrogate.close()
}
#ParameterizedTest
#MethodSource("NavigateContent")
fun `Navigate Content`(test: NavigateContentTest) = runBlocking {
every { ContentRepository.getMainFeedList(test.isRealtime, any()) } returns mockGetMainFeedList(
test.mockFeedList, CONTENT)
every {
ContentRepository.queryLabeledContentList(test.feedType)
} returns mockQueryMainContentList(test.mockFeedList)
every { ContentRepository.getContent(test.mockContent.id) } returns mockGetContent(test)
// Tests here...
// Verification here...
}
}
Annotation syntax initialization (Not working due to two extensions #ExtendWith)
#ExtendWith(InstantExecutorExtension::class)
#ExtendWith(MockKExtension::class)
class NavigateContentTests {
// This object should be mocked.
#MockK
lateinit var contentRepository: ContentRepository
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
private val contentViewModel = ContentViewModel()
// This is the stream of tests to run in the Parameterized test below.
private fun NavigateContent() = Stream.of(
NavigateContentTest(
isRealtime = false,
feedType = MAIN,
timeframe = DAY,
mockFeedList = mockDbContentListForDay,
mockContent = mockArticleContent),
...)
#BeforeAll
fun beforeAll() { MockKAnnotations.init(this, relaxUnitFun = true) // turn relaxUnitFun on for }
#AfterAll
fun afterAll() { unmockkAll() // Re-assigns transformation of object to original state prior to mock. }
#BeforeEach
fun beforeEach() { Dispatchers.setMain(mainThreadSurrogate) }
#AfterEach
fun afterEach() {
Dispatchers.resetMain() // Reset main dispatcher to the original Main dispatcher.
mainThreadSurrogate.close()
}
#ParameterizedTest
#MethodSource("NavigateContent")
fun `Navigate Content`(test: NavigateContentTest) = runBlocking {
every { contentRepository.getMainFeedList(test.isRealtime, any()) } returns mockGetMainFeedList(
test.mockFeedList, CONTENT)
every {
contentRepository.queryLabeledContentList(test.feedType)
} returns mockQueryMainContentList(test.mockFeedList)
every { contentRepository.getContent(test.mockContent.id) } returns mockGetContent(test)
// Tests here...
// Verification here...
}
}
Environment
MockK version: 1.9.3
OS: Mac 10.14.6
Kotlin version: 1.3.50
JDK version: 12.0.1
JUnit version: 5.5.1
Type of test: Unit test
This is a bug according to this GitHub issue, as documented by the MockK creator, #oleksiy.
I will update this post once I see the bug is resolved.

Categories

Resources