I have a composable with a button that launches one of the native activities (Google Settings).
To test this before compose (using Robolectric) I would do something like this:
My test:
#Test
fun `MyFragment - when button clicked - starts activity`() {
// ...
val shadowActivity: ShadowActivity = Shadow.extract(activity)
val nextStartedActivity = shadowActivity.nextStartedActivity
assertNotNull(nextStartedActivity)
assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, nextStartedActivity.action)
}
With compose tests (not using activity scenario) it's different. There is no activity handle, only a composeTestRule:
My test:
// ...
private val buttonNode get() = composeTestRule.onNodeWithContentDescription("Button")
#Test
fun `MyComposableToTest - when button clicked - starts activity`() {
composeTestRule.setContent {
MyComposableToTest()
}
buttonNode.assertExists().assertHasClickAction().assertIsEnabled().performClick()
// No possibility to get current activity
}
How can I assert that a new activity is started when testing a Composable?
Some context:
Android Gradle Plugin 7.0.3
Robolectric 4.7.3
Compose 1.1.0-beta04
You are able to fetch the context from the ComposeContentTestRule like this:
lateinit var context : Context
composeTestRule.setContent {
context = LocalContext.current
MyComposableToTest()
}
and then to assert the next started activity
val shadowActivity: ShadowActivity = Shadow.extract(context as ComponentActivity)
val nextStartedActivity = shadowActivity.nextStartedActivity
assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, nextStartedActivity.action)
This is how I did it for my instrumented test (NOT using Robolectric).
build.gradle[.kts]:
androidTestImplementation("androidx.test.espresso:espresso-intents:3.4.0")
The test class (in src/androidTest/... directory):
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
// ...
#RunWith(AndroidJUnit4::class)
class MainActivityInstrumentedTest {
#get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>()
#Test fun testTheIntent() {
Intents.init() // IMPORTANT (to be able to use "intended" function)
composeTestRule.setContent {
MyAppTheme {
MainScreen()
}
}
composeTestRule.onNodeWithText("My Button").performClick()
intended(hasComponent(MySecondActivity::class.java.name))
Intents.release()
}
}
Related
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))
I want to test the following very common usecase as an Instrumented Test in Android:
When clicking a button a fetch() function is called in my ViewModel
This function tells the view to show a loading-overlay
It executes a fetch in a coroutine
After the result is fetched it lets the view know to display the result
Here is the function in my Viewmodel:
fun fetch() {
_loading.value = true //loading is shown
viewModelScope.launch {
val results = fetchUseCase() //suspend function
_result.postValue(results)
_loading.postValue(false) //loading is not displayed
}
}
Here is the test which works according to this CodeLab https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#4:
#HiltAndroidTest
#UninstallModules(CoroutinesDispatcherModule::class)
#RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTestJunit4Deprecated {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#ExperimentalCoroutinesApi
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
#Before
fun setup() {
ActivityScenario.launch(HomeScreenActivity::class.java)
}
#ExperimentalCoroutinesApi
#Test
fun fetchTest() {
//pausing the long running tasks
mainCoroutineRule.pauseDispatcher()
//When clicking the button
onView(withId(R.id.load_measurement_button)).perform(click())
//loading is shown
onView(withId(R.id.loading_overlay))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
//continue fetch
mainCoroutineRule.resumeDispatcher()
// loading is not shown anymore and the result is there
onView(withId(R.id.loading_overlay))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
onView(withId(R.id.message))
.check(matches(withText("0")))
}
}
Unfortunately "pauseDispatcher()" and "resumeDispatcher" are Deprecated. I tried to use the "StandardTestDispatcher" and "advanceUntilIdle()" but it does not work as expected. The coroutine is never resumed. How can this test be rewritten, such that it works:
Without deprecated function calls
Without changing the productive code
?
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()
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()))
}
I am developing an Android application using Kotlin programming language. I am adding instrumentation tests into my application. Now I am trying to test if an activity is started after some delay.
This is my activity code.
class MainActivity : AppCompatActivity() {
companion object {
val LAUNCH_DELAY: Long = 2000
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Handler().postDelayed({
this.startLoginActivity()
}, LAUNCH_DELAY)
}
protected fun startLoginActivity()
{
startActivity(Intent(this, LoginActivity::class.java))
}
}
I know how to write a simple test like this
#Test
fun itRendersCompanyName() {
onView(withId(R.id.main_tv_company_name)).check(matches(withText("App Name")))
}
But what I am trying to test here is if the LoginActivity is launched after some delay. How can I do it using Espresso framework?
You can get the visible Activity using ActivityManager:
inline fun <reified T : Activity> isVisible(): Boolean {
val am = ApplicationProvider.getApplicationContext<Context>().getSystemService(ACTIVITY_SERVICE)
as ActivityManager
val visibleActivityName = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
am.appTasks[0].taskInfo.topActivity.className
} else {
am.getRunningTasks(1)[0].topActivity.className
}
return visibleActivityName == T::class.java.name
}
Calling isVisible<LoginActivity>() will tell you that LoginActivity is visible or not.
Also, to wait until your LoginActivity visible, you can wait for this method to gets true. For example:
inline fun <reified T : Activity> waitUntilActivityVisible() {
val startTime = System.currentTimeMillis()
while (!isVisible<T>()) {
Thread.sleep(200)
if (System.currentTimeMillis() - startTime >= TIMEOUT) {
throw AssertionError("Condition unsatisfied after $TIMEOUT milliseconds")
}
}
}
You can use Intents.intended() for that.
Add following to your build.gradle file:
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
In your test function, you can try following code:
Intents.init()
Intents.intended(hasComponent(LoginActivity::class.java!!.getName()))
You can read more about Espresso-Intents here.
It’s better to test this state with unit tests. Use architecture pattern (for example MVP/MVVM), mock presenter/view model and check what method which is responsible for activity start is triggered
I got it working using the following:
val expectedUrl = "https://yoururlhere"
Intents.init();
Matcher<Intent> expectedIntent = allOf(hasAction(Intent.ACTION_VIEW), hasData(expectedUrl));
intending(expectedIntent).respondWith(new Instrumentation.ActivityResult(0, null));
onView(withId(R.id.someViewId)).perform(click());
intended(expectedIntent);
Intents.release();
And remember to add "androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'" to your gradle dependency