Efficient UI testing series
- Introduction
- Local and Instrumentation tests in Android
- Android Studio and Android SDK tools
- UI Testing Frameworks
- Exploring "Espresso Test Recorder"
- First test case with Kakao framework
- Testing Android Fragment in isolation
- Exploring test application and first test cases
- A domain-specific language for testing
- Introducing UI tests with mocking
- Flaky tests
- Achieving efficiency in tests
Introduction
In the previous article, we compared available frameworks which can help to create Android tests and Android Tools (Layout Inspector, UiAutomator Viewer and Espresso Test Recorder). I recommend using Espresso because of execution time and the possibility of creating End-To-End (E2E) and UI tests with mocking data.
The Espresso is a native framework for creating UI tests for Android applications. It means that we have many libraries with additional Mathers or frameworks based on Espresso. Sometimes it's only wrappers; however, from time to time, we can find solutions with additional features.
Choose an Espresso based framework
One of the most popular Espresso based framework is Kakao. It's not the only wrapper over Espresso, but this framework also provides additional features, like Intercepting. An interceptor allows you to inject other logic between Kakao → Espresso call chain. This feature allows you to reduce the amount or avoid flaky tests. (Flaky tests mean that test cases without any modification of the codebase can have different results from time to time. As an example, a test case passes, but sometimes it can fail without modification test case and code of the application. We will talk about flaky tests in one of the future articles).
Let's try to create the same tests with Espresso and Kakao. Let's test Sign In the screen of the application. Before comparing test cases, let's visualize the test scenario.
Espresso:
@RunWith(AndroidJUnit4::class)
class EspressoSignInTest {
@Rule @JvmField
val activityRule = ActivityTestRule<SignInActivity>(SignInActivity::class.java)
@Test
fun shouldDisplaySinInErrorWhenEmailIsIncorrect() {
val incorrectEmail = "test"
onView(withId(R.id.email))
.perform(replaceText(incorrectEmail), closeSoftKeyboard())
onView(withId(R.id.signIn))
.perform(click())
onView(withText(R.string.error_email_should_be_valid))
.check(matches(isDisplayed()))
}
}
Kakao:
@RunWith(AndroidJUnit4::class)
class EspressoSignInTest {
@Rule @JvmField
val activityRule = ActivityTestRule(SignInActivity::class.java)
@Test
fun shouldDisplaySinInErrorWhenEmailIsIncorrect() {
onScreen<SignInScreen> {
emailEditText {
replaceText("test")
ViewActions.closeSoftKeyboard()
}
signInButton {
click()
}
snackbar {
text {
hasText(R.string.error_email_should_be_valid)
}
}
}
}
}
As you can see from the comparison, the Kakao framework provides more declarative ways of working with Views and allows us to write less boilerplate in comparison with Espresso framework.
However, previous test cases with the Kakao framework required creating a SignInScreen
class.
class SignInScreen : Screen<SignInScreen>() {
val emailEditText = KEditText { withId(R.id.email) }
val signInButton = KButton { withId(R.id.signIn) }
val snackbar = KSnackbar()
}
It helps us to test each screen with a DSL. However, there can be a lot of code if you want to create a test case which covers few screens.
class E2ETest {
private val correctEmail = "test@test.com"
private val correctPassword = "test123"
private val incorrectPassword = "test-password"
@Rule @JvmField
val activityRule = ActivityTestRule(SplashActivity::class.java)
@Test
fun shouldVerifySuccessfulLogin() {
onScreen<LoginScreen> {
signInButton {
click()
}
}
onScreen<SignInScreen> {
emailEditText {
replaceText(correctEmail)
}
passwordEditText {
replaceText(correctPassword)
ViewActions.closeSoftKeyboard()
}
signInButton {
click()
}
}
onScreen<HomeScreen> {
waitForMap() // custom function in HomeScreen class
}
...
}
}
As you can see, this test is long, and the reader should follow the code and keep in my UI of the application to understand the test case. It means that for E2E test cases, it will be better to create a DSL. I want to propose a DSL for creating readable End to End test cases for the same scenario. You can create DSL with Kakao framework on top or hide Kakao framework inside DSL.
@RunWith(AndroidJUnit4::class)
class E2ETests {
@Rule @JvmField
val activityRule = ActivityTestRule(SplashActivity::class.java)
@Test
fun shouldVerifySuccessfulLogin() {
loginScreen {
openSignIn()
}
signInScreen {
signIn(correctEmail, correctPassword)
}
homeScreen {
isMapDisplayed()
}
...
}
}
Intercepting
One of the benefits of Kakao in comparison with Espresso, is that it's an interceptor mechanism which allows us to inject code during Kakao → Espresso call chain. Let's take a look at a few examples.
Logging action
This approach can help us to create a description of our test. Let's create an interceptor which logs all actions.
@Test
fun shouldDisplaySinInErrorWhenEmailIsIncorrect() {
onScreen<SignInScreen> {
intercept {
onViewInteraction {
onPerform { interaction, action ->
Log.d("ACTION", "$interaction is performing ${action.description}")
}
}
}
emailEditText {
replaceText("test")
ViewActions.closeSoftKeyboard()
}
signInButton {
click()
}
snackbar {
text {
hasText(R.string.error_email_should_be_valid)
}
}
}
}
Output:
androidx.test.espresso.ViewInteraction@9a070c4 is performing replace text
androidx.test.espresso.ViewInteraction@e83e365 is performing single click
Repeat assertion when failing
Cases can occur when the test case tries to check an assertion, but the view is not rendered yet. One solution is using IdlingResources or we can use intercepter and repeat an assertion after a small delay. Let's try to implement it.
KakaoE2EScenarioTest {
...
@Test
fun shouldVerifySuccessfulLogin() {
onScreen<LoginScreen> {
signInButton {
click()
}
}
onScreen<SignInScreen> {
emailEditText {
replaceText(correctEmail)
}
passwordEditText {
replaceText(correctPassword)
ViewActions.closeSoftKeyboard()
}
signInButton {
click()
}
}
onScreen<HomeScreen> {
intercept {
onViewInteraction {
onCheck(true) { interaction, assertion ->
try {
interaction.check(assertion)
} catch (ex: Throwable) {
idle(250)
interaction.check(assertion)
}
}
}
}
mapView {
isDisplayed()
}
}
}
}
Summary
Espresso is a powerful tool which allows us to create efficient UI tests. However, the API of this framework can be improved in the case of Kakao framework. As a result, we can develop tests without boilerplate code.
In addition Kakao framework allows us to inject code between Kakao → Espresso call chain. It can help us to create a description of a test case, improve logging, repeat failed action, etc.
Sometimes even the Kakao framework is not enough for creating short and declarative E2E test cases. However, we can use the Kakao framework without additional DSL in many cases. Next time we will talk about mocking data with the Koin library, and after it, we will discuss creating DSL for UI testing. Stay tuned.