Status Update
Comments
ma...@google.com <ma...@google.com>
je...@google.com <je...@google.com> #2
You can use createAndroidComposeRule<MyActivity>()
to launch an activity of your choice. In that case, the content is set by your Activity rather than by composeTestRule.setContent {}
, see for example our
If you want to set Compose content from your test, but inside a custom template (as opposed to as the root), you can create a custom test activity that has it's own setContent
method, something like this:
class CustomActivity : ComponentActivity() {
private lateinit var composeView: ComposeView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.my_test_activity)
composeView = findViewById(R.id.my_composeview)
}
fun setContentInTemplate(content: @Composable () -> Unit) {
runOnUiThread {
composeView.setContent(content)
}
}
}
@RunWith(AndroidJUnit4::class)
class CustomActivityTest {
@get:Rule
val rule = createAndroidComposeRule<CustomActivity>()
@Test
fun launchCustomContent() {
rule.activity.setContentInTemplate {
MaterialTheme {
Box {
Button(onClick = {}) {
Text("Hello")
}
}
}
}
rule.onNodeWithText("Hello").assertExists()
}
}
This should give you precise control over when the content is set (which should address (2)), and where it is set (addresses (1) and (3)). Please let me know this works for your case.
je...@google.com <je...@google.com> #3
I'm preemptively closing this bug, but feel free to reopen when the comment above didn't work for you
me...@thomaskeller.biz <me...@thomaskeller.biz> #4
I'm basically doing what you propose here, and this indeed allows me to test the compose code and my Android viewmodel in conjunction.
Where this procedure falls short however is when I want to test the interfacing between controller components (like activities or fragments) and my viewmodel, e.g. to check whether intent data or fragment arguments are properly passed to my viewmodel or to evaluate AndroidX navigation library events, aso.
In general, all this controller glue code can no longer be tested with compose UI, because the original controller component is not even part of the test execution. Yes, it's good practice to keep the controller component on Android light and thin, eg. by moving most of the logic into the viewmodel, but some things have to stay there and can't be eliminated, and it would just be cool to still have a way to run and check this code as well during testing.
je...@google.com <je...@google.com> #5
Could you provide a small sample to illustrate the scenario you would like to test, and the thing that's not possible to test now?
me...@thomaskeller.biz <me...@thomaskeller.biz> #6
Yes, for example I have a Fragment with a Compose UI, set up like this:
class FooFragment : Fragment() {
private lateinit var binding: FragmentFooBinding
private val viewModel: FooViewModel by viewModels()
private val args: FooFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.composeView.setContent {
MdcTheme {
FooScreen(viewModel)
}
}
viewModel.onAction(FooActions.OnViewCreated(args))
}
override fun onPause() {
viewModel.onAction(FooActions.OnPause)
super.onPause()
}
}
What I'm doing right now in my Compose test is that I manually trigger the specific actions to either feed the arguments to my ViewModel or to manually trigger the OnPause
lifecycle action. This however doesn't test the real implementation! Since the fragment code isn't run, I cannot verify that I trigger the specific actions in the right lifecycle methods (or, that anybody moved things around).
Similar issues arise once I want to trigger and test for jetpack navigation events via a TestNavController
, i.e. testing regular nav graphs in conjunction with the VM / Fragment calling code.
me...@thomaskeller.biz <me...@thomaskeller.biz> #7
Hi, is this still a Won't Fix for you? This issue is still present and needs awkward workarounds.
je...@google.com <je...@google.com> #8
Apologies, this slipped off my radar.
If you add the FooFragment to a test activity and then use createAndroidComposeRule<YourTestActivity>()
, it should execute everything necessary to get the activity in the resumed state, which includes onViewCreated
.
For onPause
, however, you need a little more control. What you can do there is create an "empty" compose test rule, one that does not start an activity for you at all, and move your fragment through all states manually (like you would do for View-based UIs). For example:
@get:Rule
val composeRule = createEmptyComposeRule()
@Test
fun test() {
with(launchFragmentInContainer<FooFragment>()) {
// composeRule.onNode(..).assert(..), etc
moveToState(State.STARTED)
// assertions, etc.
}
}
me...@thomaskeller.biz <me...@thomaskeller.biz> #9
The problem is really interaction. The OnViewCreated
action would kick-off a change that would result in a change of the downstream state that FooScreen
would subscribe to. I assume that even though I'd add FooFragment
to the Activity-under-test, I'd still have to call composeRule.setContent {}
, because the Fragment's setContent
call on the ComposeView
still knows nothing of the Recomposer
from the test rule, so these two things would still be detached from one another.
That's what I was referring to - if the current implementation of AndroidComposeTestRule
would be a bit more open and would also allow the wrapping of ComposeView
intances (from Activity
s, Fragment
s or else), it would be of much help, especially for cases where developers weren't able to convert a complete screen to Compose yet, but start bottom up with small building blocks, like single views or fragments.
My go-to-solution is now to have a separate Fragment
test that mocks out the ViewModel and tests the interactions in isolation, and then have another Compose+ViewModel test which does the other part.
je...@google.com <je...@google.com> #10
Ah, I think I see the misunderstanding now.
You don't need to call AndroidComposeTestRule.setContent
if your Activity or Fragment already sets the content. If you look closely to the return type of createEmptyComposeRule()
, you'll find that it doesn't even have a setContent
method.
The test rule takes care of wiring everything together. If you're interested to see how, take a look at
@get:Rule(order = 0)
val activityScenarioRule = ActivityScenarioRule<CustomActivity>()
@get:Rule(order = 1)
val composeTestRule = createEmptyComposeRule()
Please take a look at
ap...@google.com <ap...@google.com> #11
Branch: androidx-main
commit ff4469e78695de2436bdbe8822722f7c5e22de29
Author: Jelle Fresen <jellefresen@google.com>
Date: Tue Feb 15 14:22:51 2022
Test Compose when content set by a Fragment
This shows we can test Compose content if it is set by a Fragment
Fix: 199631334
Test: ./gradlew compose:ui:ui-test-junit4:cC
Change-Id: I9c79a1e240734be481c54d24c14111750c916f45
M compose/ui/ui-test-junit4/build.gradle
A compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeInFragmentTest.kt
je...@google.com <je...@google.com>
me...@thomaskeller.biz <me...@thomaskeller.biz> #12
Many thanks for the example, will try that out!
me...@thomaskeller.biz <me...@thomaskeller.biz> #13
A small addition - when my Fragment is in Lifecycle.State.STARTED
, compose will tell me this:
java.lang.IllegalStateException: No compose views found in the app. Is your Activity resumed?
at androidx.compose.ui.test.TestContext.getAllSemanticsNodes$ui_test_release(TestOwner.kt:96)
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchSemanticsNodes$ui_test_release(SemanticsNodeInteraction.kt:82)
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrDie(SemanticsNodeInteraction.kt:155)
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchSemanticsNode(SemanticsNodeInteraction.kt:106)
at androidx.compose.ui.test.AndroidAssertions_androidKt.checkIsDisplayed(AndroidAssertions.android.kt:29)
at androidx.compose.ui.test.AssertionsKt.assertIsDisplayed(Assertions.kt:33)
If I move my Fragment to Lifecycle.State.RESUMED
, this error is gone, which is fine for me; I don't need to test things before onResume
.
I do call setContent
on the ComposeView
in onViewCreated
, as per your example.
je...@google.com <je...@google.com> #14
when my Fragment is in Lifecycle.State.STARTED, compose will tell me this
That is by design. If you open multiple activities we only want to find Compose elements in the RESUMED activity as that is the one the user is looking at.
If there is a good reason why the test harness should look in STARTED activities (/fragments) as well, then please open a new bug and I'll take a look.
me...@thomaskeller.biz <me...@thomaskeller.biz> #15
What I also figured is that each test that does anything with composeRule
runs quite slow, i.e. each test method needs about two extra seconds to run. I looked at the profile, but cannot say for sure whats causing this. What I can see however is that a test with the Activity-based rule takes about 150 to 250ms to execute with Robolectric, when I use the empty test rule the test takes about 2100 to 2300ms. Unfortunately I couldn't reproduce it yet with a smaller example. Will further have to look into this.
me...@thomaskeller.biz <me...@thomaskeller.biz> #16
That is by design. If you open multiple activities we only want to find Compose elements in the RESUMED activity as that is the one the user is looking at.
No, this is totally fine. The error message could only a tad bit better by saying "Is your Activity or Fragment resumed?", other than that this is fine.
me...@thomaskeller.biz <me...@thomaskeller.biz> #17
I created a follow-up DialogFragment
-related crash.
ap...@google.com <ap...@google.com> #18
Branch: androidx-main
commit 0fa0145d802d8effe0e833f0fe996dbeec3ab23e
Author: Jelle Fresen <jellefresen@google.com>
Date: Thu Feb 24 14:40:50 2022
Disallow overwriting content with test content
This adds a check in AndroidComposeTest.setContent that makes sure that
the Activity on which the content is set doesn't have content yet. If
there is content already, it is a signal that the tester is probably
unintentionally doing something wrong, so help them by throwing.
Bug: 199631334
Test: ./gradlew bOS
Relnote: "`ComposeContentTestRule.setContent` will now throw an
IllegalStateException if you try to set content when there already is
content."
Change-Id: I888a5054c27d7884110415a812d0ac748be3f869
M compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRemoveTest.kt
M compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt
M compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
M compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/CustomActivityTest.kt
M compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/accessibility/CollectionInfoTest.kt
M compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ClickTestRuleTest.kt
M compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/IsDisplayedTest.kt
M compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ActivityWithActionBar.kt
M compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeTest.android.kt
Description
Previously with plain XML UIs, one was able to test ViewModel and UI (Fragment/Activity + XML) in combination. With Compose and the
AndroidComposeTestRule
this is no longer possible:AndroidComposeTestRule
needs a test rule that provides anActivity
to the underlying compose infrastructure, while one can give it aFragmentScenarioRule
the fragment part of this cannot actually be re-used and the content of the whole Activity is replaced by the Compose UI.ComposeView.setContent()
of theFragment
(orActivity
, for that matter) isn't run when theAndroidComposeTestRule
'ssetContent
is used instead. Even worse, depending on the lifecycle state in which anActivity
is in (in which state it is started), a previous rendering could or could not have happened, leading to possible side effects with an actualViewModel
interfacing implementation.ComposeView
is not the root view of the controller component.I understand that a special
Recomposer
setup and a lot of other custom tooling is needed to make Jetpack Compose testable in an Robolectric / Android Instrumentation test, however I wish the test architecture would first and foremost provide an API to hook in / replace the existing functionality, e.g. by making it easy to inject a custom test composer in a running fragment / activity instance or by at least making this composer and possibly other utilities available for certain testing purposes.