Elena Canorea
Communications Lead
Looking at “Advanced Android in Kotlin” code labs series from google you can find one about Testing and Dependency Injection. It is not only a testing practice but an architecture guide for android too.
When you reach the third section you will start to write User Interface tests with Espresso. It is awesome to run them and see the app running alone and doing things. Although it is filed as an advanced code lab, the code for the test can be refactored using the Page Object Pattern
The sense of a page object is to hide the “odd” code needed to replicate the steps needed to navigate through our app, perform actions as a user and provide a clean interface to create meaning and useful tests with.
To see this in action we will use the code from the code lab repo final solution you can find here:
https://github.com/googlecodelabs/android-testing/tree/end_codelab_3
We will work from the “end_codelab_3” branch.
We will start by adding the base Kotlin class Page that will start the magic. Add this class to the “androidTest” source set:
open class Page {
companion object {
inline fun <reified T : Page> on(): T {
return Page().on()
}
}
inline fun <reified T : Page> on(): T {
val page = T::class.constructors.first().call()
page.verify()
//Thread.sleep(500) //To give time when asynchronous calls are invoked
return page
}
open fun verify(): Page {
// Each subpage should have its default assurances here
return this
}
fun back(): Page {
Espresso.pressBack()
return this
}
}
If you want to go deep on this, please take a look to Page Object Pattern in Kotlin for UI Test Automation On Android from Kate Savelova
In the end, this class is used to be able to define a fluent API that will help us to organize the code needed to perform navigation, actions, and checks throughout our app pages, views, etc. Pay special attention to the verify function. This function is used to check if the page we want has been loaded.
Let’s start by adding our first test with this page. Go to AppNavigationTest.kt file and add a new test that will add a new task to the app.
@Test
fun createNewTask() {
// Start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// When using ActivityScenario.launch, always call close()
activityScenario.close()
}
This is the starter code for the test that just launch the TasksActivity, for a deep understanding of this take a look at the code lab: https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#0
The full test code is:
@Test
fun createNewTask() {
// Start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
val testTask= Task(“title”, “description”)
Page.on<TasksPage>()
.tapOnAddButton()
.on<AddTaskPage>()
.addTask(testTask)
.on<TasksPage>()
.checkAddedTask(testTask)
// When using ActivityScenario.launch, always call close()
activityScenario.close()
}
See the beauty of the test:
It is very meaningful, and simple. But it does not compile yet, but do not worry. Let’s fix all:
Add the gradle dependency to the app build.gradle:
Add the TasksPage and AddTaskPage classes in the same package of PageObject:
class TasksPage : Page() {
override fun verify(): TasksPage {
onView(withId(R.id.tasks_container_layout))
.check(matches(isDisplayed()))
return this
}
fun tapOnAddButton(): TasksPage {
onView(withId(R.id.add_task_fab)).perform(ViewActions.click())
return this
}
fun tapOnEditTask(): TasksPage {
onView(withId(R.id.edit_task_fab)).perform(ViewActions.click())
return this
}
fun checkAddedTask(testTask: Task): TasksPage {
onView(withText(testTask.title))
return this
}
}
class AddTaskPage: Page() {
override fun verify(): AddTaskPage {
Espresso.onView(withId(R.id.add_task_title_edit_text))
.check(ViewAssertions.matches(isDisplayed()))
return this
}
fun addTask(task: Task):AddTaskPage{
onView(withId(R.id.add_task_title_edit_text))
.perform(clearText(), typeText(task.title))
onView(withId(R.id.add_task_description_edit_text))
.perform(clearText(), typeText(task.description))
onView(withId(R.id.save_task_fab)).perform(click())
return this
}
}
It contains all the Espresso code needed to do all the interactions. If we do not use this pattern, we will end up with a very long test with all those Espresso methods in only one test, which make it very hard to read and maintain.
You will end with this running app on your mobile:
The equivalent test without using Page Object pattern is:
@Test
fun createNewTaskWithoutPageObject(){
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
val task= Task(“title”, “description”)
//check tasks page open
onView(withId(R.id.tasks_container_layout))
.check(matches(isDisplayed()))
//tap on add button
onView(withId(R.id.add_task_fab)).perform(ViewActions.click())
//check task page is open
onView(withId(R.id.add_task_title_edit_text))
.check(ViewAssertions.matches(isDisplayed()))
//add task
onView(withId(R.id.add_task_title_edit_text))
.perform(ViewActions.clearText(), ViewActions.typeText(task.title))
onView(withId(R.id.add_task_description_edit_text))
.perform(ViewActions.clearText(), ViewActions.typeText(task.description))
onView(withId(R.id.save_task_fab)).perform(click())
//check task page is open
onView(withId(R.id.tasks_container_layout))
.check(matches(isDisplayed()))
//check added task
onView(withText(task.title))
// When using ActivityScenario.launch, always call close()
activityScenario.close()
}
Which test code will you prefer?
The benefits of using this pattern to build UI tests are:
Page Object is a great widely pattern used to perform UI tests in other platforms like java, JavaScript, c#, etc. And as you can see it is very easy once understood. We hope you find this post useful.
You can see a full version of this code on my google code lab fork.
Elena Canorea
Communications Lead
Cookie | Duration | Description |
---|---|---|
__cfduid | 1 year | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
__cfduid | 29 days 23 hours 59 minutes | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
__cfduid | 1 year | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
__cfduid | 29 days 23 hours 59 minutes | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_gat_UA-326213-2 | 1 year | No description |
_gat_UA-326213-2 | 1 year | No description |
_gat_UA-326213-2 | 1 year | No description |
_gat_UA-326213-2 | 1 year | No description |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
attributionCookie | session | No description |
cookielawinfo-checkbox-analytics | 1 year | Set by the GDPR Cookie Consent plugin, this cookie is used to record the user consent for the cookies in the "Analytics" category . |
cookielawinfo-checkbox-necessary | 1 year | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-necessary | 1 year | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-non-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-non-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-non-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-non-necessary | 1 year | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-performance | 1 year | Set by the GDPR Cookie Consent plugin, this cookie is used to store the user consent for cookies in the category "Performance". |
cppro-ft | 1 year | No description |
cppro-ft | 7 years 1 months 12 days 23 hours 59 minutes | No description |
cppro-ft | 7 years 1 months 12 days 23 hours 59 minutes | No description |
cppro-ft | 1 year | No description |
cppro-ft-style | 1 year | No description |
cppro-ft-style | 1 year | No description |
cppro-ft-style | session | No description |
cppro-ft-style | session | No description |
cppro-ft-style-temp | 23 hours 59 minutes | No description |
cppro-ft-style-temp | 23 hours 59 minutes | No description |
cppro-ft-style-temp | 23 hours 59 minutes | No description |
cppro-ft-style-temp | 1 year | No description |
i18n | 10 years | No description available. |
IE-jwt | 62 years 6 months 9 days 9 hours | No description |
IE-LANG_CODE | 62 years 6 months 9 days 9 hours | No description |
IE-set_country | 62 years 6 months 9 days 9 hours | No description |
JSESSIONID | session | The JSESSIONID cookie is used by New Relic to store a session identifier so that New Relic can monitor session counts for an application. |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
viewed_cookie_policy | 1 year | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
viewed_cookie_policy | 1 year | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
VISITOR_INFO1_LIVE | 5 months 27 days | A cookie set by YouTube to measure bandwidth that determines whether the user gets the new or old player interface. |
wmc | 9 years 11 months 30 days 11 hours 59 minutes | No description |
Cookie | Duration | Description |
---|---|---|
__cf_bm | 30 minutes | This cookie, set by Cloudflare, is used to support Cloudflare Bot Management. |
sp_landing | 1 day | The sp_landing is set by Spotify to implement audio content from Spotify on the website and also registers information on user interaction related to the audio content. |
sp_t | 1 year | The sp_t cookie is set by Spotify to implement audio content from Spotify on the website and also registers information on user interaction related to the audio content. |
Cookie | Duration | Description |
---|---|---|
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjFirstSeen | 29 minutes | No description |
_hjFirstSeen | 29 minutes | No description |
_hjFirstSeen | 29 minutes | No description |
_hjFirstSeen | 1 year | No description |
_hjid | 11 months 29 days 23 hours 59 minutes | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjid | 11 months 29 days 23 hours 59 minutes | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjid | 1 year | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjid | 1 year | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjIncludedInPageviewSample | 1 year | No description |
_hjIncludedInPageviewSample | 1 year | No description |
_hjIncludedInPageviewSample | 1 year | No description |
_hjIncludedInPageviewSample | 1 year | No description |
_hjSession_1776154 | session | No description |
_hjSessionUser_1776154 | session | No description |
_hjTLDTest | 1 year | No description |
_hjTLDTest | 1 year | No description |
_hjTLDTest | session | No description |
_hjTLDTest | session | No description |
_lfa_test_cookie_stored | past | No description |
Cookie | Duration | Description |
---|---|---|
loglevel | never | No description available. |
prism_90878714 | 1 month | No description |
redirectFacebook | 2 minutes | No description |
YSC | session | YSC cookie is set by Youtube and is used to track the views of embedded videos on Youtube pages. |
yt-remote-connected-devices | never | YouTube sets this cookie to store the video preferences of the user using embedded YouTube video. |
yt-remote-device-id | never | YouTube sets this cookie to store the video preferences of the user using embedded YouTube video. |
yt.innertube::nextId | never | This cookie, set by YouTube, registers a unique ID to store data on what videos from YouTube the user has seen. |
yt.innertube::requests | never | This cookie, set by YouTube, registers a unique ID to store data on what videos from YouTube the user has seen. |