[00:30.000 --> 00:37.040] last talk of the day, I am glad that there are still people in this room, like usually [00:37.040 --> 00:44.760] people tend to go like dinner early or so, but well, let's close this tab room and please [00:44.760 --> 00:55.360] welcome Istvan on stage, he's going to talk about how to test your compose UI. [00:55.360 --> 01:02.200] So thank you very much, hello, I'm Istvan from here and thank you all for still being here, [01:02.200 --> 01:04.320] that's really encouraging. [01:04.320 --> 01:10.880] So let's get started, I'll be talking about testing Jetpack Compose UI on Android. [01:10.880 --> 01:14.960] So first of all, just a little extra, there is a sample project, which I took all the [01:14.960 --> 01:20.640] samples in this talk from, so feel free to check it out on my GitHub page. [01:21.080 --> 01:28.160] Okay, so a quick recap on Android testing without compose, so the regular view system. [01:28.160 --> 01:32.400] With Android, with the regular view system, we have views or view groups, which are objects [01:32.400 --> 01:39.000] created from inflating XML, or of course from code, and they have rendering and behavior [01:39.000 --> 01:47.040] attached to them, and because of this, or well, because of their actual object, we have [01:47.120 --> 01:53.520] a grasp on them, we can get their reference from code or reference for them from the view [01:53.520 --> 01:58.320] hierarchy by find view by ID or so. [01:58.320 --> 02:00.640] Okay, let's see how compose compares to that. [02:00.640 --> 02:07.080] Of course, we have a declaration of the UI and not the UI objects themselves, so we [02:07.080 --> 02:11.800] don't have a grasp on what compose actually does, what the framework actually translates [02:11.800 --> 02:18.200] our description of the UI to, and of course not every composable actually emits UI elements [02:18.200 --> 02:24.720] into the composition, so that might make our work harder as well. [02:24.720 --> 02:33.760] Okay, so let's see what composables we will be testing in the next few minutes. [02:33.760 --> 02:38.360] First of all, there is an entry list, which is just a simple screen of a list, which will [02:38.400 --> 02:45.880] display hydration entry items, and the entry list is just a wrapper around the laser column [02:45.880 --> 02:52.880] that does just that, that translates hydration entries into hydration item composables, and [02:52.880 --> 02:58.080] the items themselves are just a simple row with two texts to display the data on the [02:58.080 --> 02:59.080] screen. [02:59.080 --> 03:07.000] Okay, let's see how we can solve tests in our Android project for these composables. [03:07.560 --> 03:13.040] We have to just add a few dependencies, of course now we have a nice bomb to do that, [03:13.040 --> 03:18.320] so after we do that we add those to our grader files, we can already start writing compose [03:18.320 --> 03:20.320] UI tests. [03:20.320 --> 03:26.080] And how a compose UI test looks, like it's just a regular test class, so nothing special [03:26.080 --> 03:30.400] there, if you were writing Android UI tests, this is pretty much the same. [03:30.400 --> 03:37.600] The first thing that you can spot is that there's just another rule that we have to [03:37.600 --> 03:45.360] use inside the instrumented test, and actually we can create that rule with a built-in create [03:45.360 --> 03:50.840] Android compose rule function, which has a type parameter, which has to be an activity, [03:50.840 --> 03:55.760] so for that type parameter we can set main activity or any other activity inside the [03:55.800 --> 04:01.800] replication that we want to start, so this rule created by this function will start the [04:01.800 --> 04:04.400] activity provided in that parameter. [04:04.400 --> 04:10.320] Of course, if we want to test composables in isolation without any specific activity, we [04:10.320 --> 04:14.680] can do that as well, then we can just pass component activity to that type parameter, [04:14.680 --> 04:21.680] and yeah, component activity is just a base class for many Android X activities, it's [04:22.600 --> 04:29.360] just a foundation of all the other classes, and it can host composables, so that's that. [04:29.360 --> 04:33.360] And if we want to do this, there's actually a shortcut to do that, which is called create [04:33.360 --> 04:38.280] compose rule, which does just the same as you've seen before, but in a multi-platform [04:38.280 --> 04:43.600] way, so if you want to prepare for a multi-platform project, then you can call this create compose [04:43.600 --> 04:47.280] rule, and on Android it will translate to what you've seen before. [04:48.000 --> 04:53.400] Okay, with that out of the way, we can start writing URI tests. The tests themselves are [04:53.400 --> 04:58.960] just regular test functions, nothing special there. The specialty comes when we actually [04:58.960 --> 05:04.840] start to write the test, because all of the calls, all of the test calls, has to be made [05:04.840 --> 05:11.840] on this test rule, so we can actually scope to that, scope to, say scope to that, we apply [05:11.840 --> 05:16.480] because this is just regular Kotlin code, so we can do whatever we want and whatever [05:16.520 --> 05:22.520] we know of, so we can just scope to this compose rules, scope to this compose rule and call [05:22.520 --> 05:29.520] anything on that. And the compose test rule has a set content method, which you could [05:30.640 --> 05:36.640] have already met, as an extension on the activity or as a function of compose view, if you do [05:38.600 --> 05:45.600] interoperability, so yeah, this is the entrance to the compose world inside our tests. And [05:46.360 --> 05:52.360] in that content, we can actually call anything, any composable at this point, but for this [05:52.360 --> 05:58.000] example, we'll call our applications theme, and then just call the entry list composable, [05:58.000 --> 06:02.320] which we've seen before, with just a fake list of entries, and if you wonder where those [06:02.320 --> 06:05.960] entries can come from, they can come from basically anywhere, we're still just writing [06:05.960 --> 06:10.520] Kotlin code, so these entries could be just a fake list of entries that you provide to [06:10.600 --> 06:17.600] the test suite from anywhere. Okay, so now we know how to set up our tests, but we still [06:19.280 --> 06:25.200] have no grasp on that entry list composable or anything that entry list composable actually [06:25.200 --> 06:31.320] emits into the composition, so let's see how we can fix that problem. Enter the Semantix [06:31.320 --> 06:38.320] tree. So the Semantix tree is actually another tree that is built in parallel with the composition, [06:39.080 --> 06:46.080] consists of node that will be rendered after some processing, of course, and the composables [06:47.520 --> 06:52.680] that we write emit nodes into the composition, but they also emit nodes into the Semantix [06:52.680 --> 06:59.280] tree, which is used by the accessibility frameworks and also the testing framework. And just as [06:59.280 --> 07:05.040] with the composition, composables that we write can, but may not contribute to the Semantix [07:05.040 --> 07:12.040] tree in any way, but also that behavior can be modified, and we will see how we can do [07:12.680 --> 07:19.680] that as well. Okay, so let's simplify our example a bit. Now we just call a simple text [07:19.680 --> 07:25.240] composable inside the set content of our test through, and by doing that, the Semantix tree [07:25.240 --> 07:30.920] will look like that on the right. So there will always be a root node for the Semantix [07:31.000 --> 07:38.000] tree, and the text composable by default emits its text content into a new Semantix node, [07:39.160 --> 07:46.160] which you can see as a green node. And yeah, that's it. So let's change quickly to the [07:46.600 --> 07:52.000] canonical representation that you will see when you will be writing tests and observing [07:52.000 --> 07:59.000] this Semantix tree. Now is the root node, of course, and the text contributes, text attributes [08:00.000 --> 08:07.000] of 100 milliliters text inside that Semantix node. Okay, let's add the row, because as [08:08.600 --> 08:13.400] you can see, we are building the items of the list. So let's add row, and yeah, that [08:13.400 --> 08:19.400] doesn't change the Semantix tree in any way, because the row composable is actually a layout [08:19.400 --> 08:24.880] composable, and it doesn't emit anything into the composition. And by default, it doesn't [08:24.960 --> 08:31.480] emit anything into the Semantix tree either. Let's add another text, and of course, that [08:31.480 --> 08:36.200] will create a new Semantix node, which will be a sibling of the previous one, and the [08:36.200 --> 08:43.200] child of the root node in this example. Okay, so this is a really easy example, but of course, [08:43.400 --> 08:49.600] when you're writing an application, you will be facing complex screens and complex subcomposables [08:49.680 --> 08:56.680] and whatnot. So you will be looking at more and more complex Semantix trees as well, which [08:57.440 --> 09:04.440] you have to assert on. So of course, there must be a way to visualize this. One thing [09:04.560 --> 09:09.240] that we can do in our tests is call the own root finder method. We will be talking about [09:09.240 --> 09:16.240] those later. And call print to log on it with a test tag, sorry, a log tag. So what this [09:17.200 --> 09:22.920] will do when you run such a UI test that has this line on it, is that it will print this [09:22.920 --> 09:28.440] structured log, which you can find by the log tag, and you will see the root node here [09:28.440 --> 09:35.440] and all the other nodes structured inside this log entry. The other way might be a [09:36.160 --> 09:42.400] bit easier. You can configure the layout inspector, if you have a fully composed screen, of course, [09:42.480 --> 09:48.400] to highlight the nodes that are contributing into the Semantix tree and to inspect the [09:48.400 --> 09:55.400] attributes of those nodes. Okay, let's get back to this easy example and let's see how [09:55.600 --> 10:00.840] we can modify the behavior of how composables emit things into the Semantix tree. There [10:00.840 --> 10:07.840] is a modifier called Semantix because of course, there are modifiers for everything. So yeah, [10:08.360 --> 10:14.360] the Semantix modifier by itself does nothing, as you can see on the Semantix tree representation. [10:14.360 --> 10:19.720] So by default, adding the Semantix tree modifier into the modifier cascade won't do anything [10:19.720 --> 10:26.720] special. As long as we start adding some attribute values inside the Semantix modifier. [10:27.480 --> 10:33.360] So in this example, we add a content description to the row. And as you can see, this will [10:33.400 --> 10:39.040] actually modify the behavior of row. And with this row will actually contribute to the [10:39.040 --> 10:44.000] Semantix tree with the new Semantix node. That will be the parent of the two text nodes [10:44.000 --> 10:49.120] that are the children of the row, of course. And as you can see, the content description [10:49.120 --> 10:53.760] Semantix setting adds a content description attribute into that new node with the text [10:53.760 --> 10:58.880] of a list item. That will be picked up by the accessibility and testing frameworks, [10:58.920 --> 11:05.200] but also we can define a test tag which will only be picked up by the testing framework [11:05.200 --> 11:10.480] and not the accessibility framework. And of course, we will be able to assert on this [11:10.480 --> 11:17.480] as well. As you can see, the test tag attribute actually contributes with a tag attribute [11:18.040 --> 11:24.040] inside the accessibility node, and it will be a text as well. [11:25.000 --> 11:30.600] Okay, so with this knowledge, we can already start asserting and exercising our UI's with [11:30.600 --> 11:37.600] the Compose UI testing framework. Let's see what APIs we can use to do so. We've already [11:37.840 --> 11:43.840] seen this first one, the onRoot, which selects the, so we already seen this first finder [11:43.840 --> 11:50.840] which selects the root node of the whole Semantix tree. And yeah, there are a few functions [11:51.840 --> 11:57.320] that you can call on them, call on the root node. One you already seen, this is the print [11:57.320 --> 12:04.320] to log, which can be useful when you start writing your UI tests. There is another family [12:04.920 --> 12:11.440] of functions which is called onNodeWith, and there are a few variants on this which can [12:11.440 --> 12:17.520] find our nodes based on multiple predefined tags or multiple predefined attributes that [12:17.600 --> 12:23.800] can be present in our Semantix nodes. In this example, onNodeWith tag selects the node with [12:23.800 --> 12:29.800] the tag that matches there. In the next example, it matches for a content description, and [12:29.800 --> 12:36.800] yeah, in the next one, it matches on the text. It can be a test tag and so on and so forth. [12:37.280 --> 12:44.280] Of course, these finders try to find exactly one matching node. So if you don't have a [12:45.240 --> 12:50.920] matching node, or if you have multiple nodes that would match the criteria, this will fail [12:50.920 --> 12:57.920] your UI test. There are another family of functions that you can use to find nodes they [12:58.440 --> 13:05.200] are called onAllNodesWith, and they also have the same variants predefined, and these will [13:05.200 --> 13:12.200] try to match one or more of the nodes of the criteria. If they don't find any nodes, of [13:12.720 --> 13:19.720] course, they will also fail our tests. Just as in Espresso, if you found a node in Espresso, [13:22.760 --> 13:29.760] you can find a view. Here you can find the Semantix node. Just as in Espresso, you can [13:30.000 --> 13:37.000] perform some actions on your found nodes which will translate to actions in the actual composables [13:37.000 --> 13:43.160] that you are referring to by Semantix nodes. Let's see this example of a button. It's just [13:43.160 --> 13:49.840] a simple button that has a text inside of it. You would expect that it can be clickable, [13:49.840 --> 13:56.840] and there is a perform click on the node class that the onNodeWith functions return. If you [13:57.280 --> 14:02.840] do this perform click, then the button will be actually clicked in our UI test. This is [14:02.960 --> 14:09.960] because the button, besides many, many attributes that it contributes to the Semantix tree with, [14:10.080 --> 14:15.160] it defines an action which is called onClick, and perform click checks for this action, [14:15.160 --> 14:20.560] and if it's there on the button's Semantix node, then it deems it clickable and it will [14:20.560 --> 14:27.320] be clicked. For the rest of the APIs, because we don't have much time here, you can check [14:27.320 --> 14:32.320] the official compost testing cheat sheet which you can find on that link. [14:32.320 --> 14:39.320] Okay. Let's dive into the last topic for today, pretty much. It's hybrid UI testing. So what's [14:40.320 --> 14:47.320] hybrid UI in this context? Hybrid UI is when you want to include composable content inside [14:47.800 --> 14:52.680] the view hierarchy or the other way around, you want to include your existing custom views [14:52.680 --> 14:59.680] for whatever reason inside a composition, so like a full screen made with Compose. And [15:00.160 --> 15:05.640] luckily, we have support for this. Espresso and the compost testing framework can work [15:05.640 --> 15:12.640] together to test such cases. Let's see our first example here. So in this example, we [15:13.880 --> 15:20.880] will go with the Compose UVay, meaning that the container here, the toolbar, and the rest [15:21.360 --> 15:28.360] of the screen, except the button, will be in XML, and we will be trying to include the [15:28.720 --> 15:35.560] button which is written in Compose. So yeah, this is the context that we will be using. [15:35.560 --> 15:40.720] There's a constraint layout with the toolbar, with its regular attributes, and there is [15:40.720 --> 15:47.720] a Compose view which will be our entry point for Compose inside our layout. Okay. Yeah, [15:47.720 --> 15:54.720] here is the fluff that we need to set up this layout. We have an activity that uses view [15:55.640 --> 16:02.240] binding to set up its views. And of course, with the binding, we will be setting up the [16:02.240 --> 16:06.640] toolbar title, but the most, but the more important thing is that we set up our Compose [16:06.640 --> 16:13.640] view where we can call the setContent method that you can see here. Sorry. And yeah, there [16:14.640 --> 16:19.640] is a custom button Composable which is written somewhere else. It doesn't really matter. [16:19.640 --> 16:25.640] We can set a text on it, and it won't be, we won't be clicking it, so we just set its [16:25.640 --> 16:32.640] clickListener to nothing. All right. So how would we test this scenario? First of all, [16:33.120 --> 16:39.320] we declare our Compose test rule with starting that Compose view demo activity that we declared [16:39.400 --> 16:46.320] previously, and then we scope to the Compose test rule and call our tests. First of all, [16:46.320 --> 16:51.880] this is just a regular espresso call. Of course, we are acting on a regular view hierarchy, [16:51.880 --> 16:58.080] so this is displayed check on the toolbar. It will work. There's nothing special there. [16:58.080 --> 17:03.000] But the next thing that will also work is just calling the Compose testing API on this [17:03.080 --> 17:09.480] same layout in this same activity, and that assertion will actually pass as well because [17:09.480 --> 17:16.480] of the interoperability between the Jetpack Compose testing APIs and espresso. Okay. That's [17:17.360 --> 17:22.920] really nice. Let's see the other way around. So in this example, the whole screen you see [17:22.920 --> 17:29.920] in here except the button will be in Compose, and we'll be including this custom button [17:30.040 --> 17:37.040] here, which is written in the plain old view system. It will be a custom view. Okay. This [17:38.000 --> 17:43.640] is the custom button view. Nothing special is here. The layout is inflated from a layout [17:43.640 --> 17:50.240] XML. There might be some fluff on it, and there is a subtext method to set the text, [17:50.240 --> 17:53.800] and there is an onClickListener method to set the clickListener. Of course, nothing [17:53.840 --> 18:00.840] special here. And this is a constraint layout, so this is like a deep custom button. Of course, [18:00.880 --> 18:07.880] there might be better examples than this, like an external SDK's custom view that's [18:08.800 --> 18:15.800] still not implemented in Compose. But for this example, we'll stick to this custom button [18:16.800 --> 18:23.800] view. Okay. And this is the Composable that we will be including that button in. It's called [18:25.120 --> 18:31.520] Android View Demo, and it has an onButtonClick parameter to lift up the action handling [18:31.520 --> 18:37.800] of the button. Okay. Of course, we are using a scaffold. We are adding a top up bar there [18:37.800 --> 18:44.800] that's the fluff here, and there is the interoperability API for including a view inside the Compose [18:45.800 --> 18:52.800] position, and that's called Android View. You can read on it in the documentation. The [18:54.200 --> 18:59.640] important part here is that there we call the constructor of the custom button view and [18:59.640 --> 19:06.640] set it up like you would with a regular view in code. Okay. So how do we test this scenario? [19:08.160 --> 19:12.640] We will just, sorry, we will just call the createCompose rule because we don't need an [19:12.680 --> 19:19.680] exact activity to test this Composable in isolation. And then we set up our test, and [19:19.880 --> 19:26.880] we do a kind of behavioral test pattern there for our Android View Demo Composable. As you [19:27.520 --> 19:34.520] can see, the button click handler is just setting up an external value outside. Okay. [19:34.840 --> 19:40.840] So if the button will be clicked, then we would expect that button clicked variable to be [19:40.840 --> 19:46.960] set to true, and we will be asserting on that. So let's start testing. First things first. [19:46.960 --> 19:53.960] Android View, we are testing if the toolbar in the Composable is visible, and that will [19:55.840 --> 20:02.840] pass. Then we do the espresso testing for the button to check if it's visible, and then [20:04.520 --> 20:09.800] it's displayed, and that will pass as well. Again, this is the power of interoperability [20:09.800 --> 20:16.800] between espresso and Compos. Okay. So let's go forward and try to click that button that [20:18.360 --> 20:23.280] we have this as a view, that we have here as a view. And we would expect that that assert [20:23.280 --> 20:30.280] equals on the button will be passing, but unfortunately that's not the case as of now. [20:30.400 --> 20:35.600] So yeah, as of now with the latest Compos bomb and the latest Compos version, this will [20:35.600 --> 20:41.000] not pass. This will not happen, actually. That perform click won't be clicking the button [20:41.000 --> 20:47.840] because of a bug in espresso. The thing that we can do is to call perform click on the [20:47.840 --> 20:54.480] view that's provided by us in espresso, but the problem with this is that perform click [20:54.480 --> 20:59.680] won't be happening inside the context of espresso. So there might be timing issues, and when [20:59.760 --> 21:04.760] you want to run a check after doing this, that might fail because the click is not performed [21:04.760 --> 21:11.760] or the side effects of the click won't be performed in time. So with this, yeah, we [21:12.000 --> 21:17.280] now have a kind of flaky test, which we could circumvent by doing some more fluff around [21:17.280 --> 21:24.280] it with espresso, but by default this is the case now. Hopefully it will be fixed soon. [21:24.720 --> 21:31.720] So we're almost done. There are more topics that you can check out on Compos testing. [21:33.160 --> 21:38.720] The best part of it is the libraries that do screenshot testing, of course, but yeah, [21:38.720 --> 21:44.400] this topic is pretty deep, so yeah, feel free to check out these. Finally, here are some [21:44.400 --> 21:49.480] resources that I use to create this talk, and also there is a 40-minute version on my [21:49.520 --> 21:55.000] website that you can watch, and multiple instances actually. So yeah, if you're interested in [21:55.000 --> 22:02.000] this topic and some more tools to use and some more examples, check out those as well. [22:04.080 --> 22:08.600] And yeah, finally, thank you for your attention. If you have any questions, I guess we have [22:08.600 --> 22:12.600] some time, and yeah, that's it. Thank you. [22:12.900 --> 22:16.060] Thank you. [22:16.060 --> 22:19.060] Thank you.