Add Espresso and UiAutomator tests to your Surface Duo app

Espresso and UiAutomator are both testing frameworks provided by AndroidX libraries. Espresso is used to find and interact with UI elements within an app, while UiAutomator provides cross-app testing functionality and access to device sensors.

To learn more about these testing frameworks, check out these resources:

In your Android Studio Project, you'll be writing these UI tests in the androidTest section, where the default test file is named ExampleInstrumentedTest. And remember, disable animations on your device before running your UI tests.

Dependencies

Add the following dependencies to your app's build.gradle file:

android {
    defaultConfig {
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

dependencies {
    testImplementation "junit:junit:4.13"
    androidTestImplementation "androidx.test.ext:junit:1.1.1"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0"
    androidTestImplementation "androidx.test:runner:1.2.0"
    androidTestImplementation "androidx.test:rules:1.2.0"
    androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"
}

If testing with a WebView, androidTestImplementation "androidx.test.espresso:espresso-web:3.2.0" should also be added to the dependencies section.

Note

If using a separate dependencies.gradle file, see the app samples repo for examples of how the add the necessary dependencies.

Create rules for tests

JUnit rules can be added to your tests to specify additional information about subsequent tests. One of the most common rules used is to define a main activity for the tests to be applied to, as shown below:

@RunWith(AndroidJUnit4::class)
class PhotoEditorUITest {
    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java)
}

Write dual-screen tests

Basics

Use the UiDevice class provided by UiAutomator to find your device and change its spanning/rotation configurations, and then use Espresso to check that UI elements behave as expected.

As shown in the example below, the swipe method can be used to simulate spanning and unspanning. To make your tests are as reliable as possible, try to group tests together that need the same configuration so you don’t have to make multiple spanning and unspanning requests.

@Test
fun testSpan() {
    val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

    // Span the app (from the left side)
    device.swipe(675, 1780, 1350, 900, 400)

    // Test that UI element appears
    onView(withId(R.id.yourElement)).check(matches(isDisplayed()))

    // Unspan the app (to the left side)
    // device.swipe(2109, 1780, 675, 900, 200)

    // Test that UI element has disappeared
    // onView(withId(R.id.yourElement)).check(matches(not(isDisplayed())))
}

Other useful UiAutomator functions include setOrientationNatural, unfreezeRotation, and pressHome. If you want to test cross-app functionality, you can also use UiAutomator to launch other apps.

Helper functions

These basic helper functions can be used for simulating spanning behavior:

companion object {
    // testing device
    val device: UiDevice = UiDevice.getInstance(getInstrumentation())

    // Swipe constants
    const val leftX: Int = 675          // middle of left screen
    const val rightX: Int = 2109        // middle of right screen
    const val middleX: Int = 1350       // hinge area
    const val bottomY: Int = 1780       // bottom of screen
    const val middleY: Int = 900        // middle of screen
    const val spanSteps: Int = 400      // spanning swipe
    const val unspanSteps: Int = 200    // unspanning swipe
    const val switchSteps: Int = 100    // swipe to switch from one screen to the other
    const val closeSteps: Int = 50      // swipe to close app
}

private fun spanFromLeft() {
    device.swipe(leftX, bottomY, middleX, middleY, spanSteps)
}

private fun unspanToLeft() {
    device.swipe(rightX, bottomY, leftX, middleY, unspanSteps)
}

private fun spanFromRight() {
    device.swipe(rightX, bottomY, middleX, middleY, spanSteps)
}

private fun unspanToRight() {
    device.swipe(leftX, bottomY, rightX, middleY, unspanSteps)
}

private fun switchToLeft() {
    device.swipe(rightX, bottomY, leftX, middleY, switchSteps)
}

private fun switchToRight() {
    device.swipe(leftX, bottomY, rightX, middleY, switchSteps)
}

private fun closeLeft() {
    device.swipe(leftX, bottomY, leftX, middleY, closeSteps)
}

private fun closeRight() {
    device.swipe(rightX, bottomY, rightX, middleY, closeSteps)
}

To test if an app is spanned, define a rule that connects to the main activity and use it to replace activityRule in this function:

private fun isSpanned(): Boolean {
    onIdle() // wait until layout changes have been fully processed before checking
    return ScreenHelper.isDualMode(activityRule.activity)
}

To make sure all of the helper functions work as expected, add import org.hamcrest.CoreMatchers.`is` as iz to your import statements and then run the following test:

/**
 * Runs helper functions and checks that they work as expected
 *
 * @precondition device in portrait mode, no other applications are open
 * (so app by default opens on left screen)
 */
@Test
fun testSpanningHelperFunctions() {
    spanFromLeft()
    assertThat(isSpanned(), iz(true))

    unspanToRight()
    assertThat(isSpanned(), iz(false))

    spanFromRight()
    assertThat(isSpanned(), iz(true))

    unspanToLeft()
    assertThat(isSpanned(), iz(false))

    switchToRight()
    switchToLeft()
    closeLeft()
}

Troubleshooting tips

If the testSpanningHelperFunctions test fails or you encounter other testing issues:

  • Adjust the step constants - certain gestures need to be slow enough to ensure they are processed correctly. 100 steps take approximately 0.5 seconds.
  • Change the x and y constants, which represent pixel values on the screen. Each screen is 1350 x 1800 px, and the hinge gap is 84 px wide (Surface Duo dimensions).
  • Add IdlingResources to tests that load data or perform other asynchronous operations that may affect UI tests.

Summary

UI testing for Surface Duo apps can be accomplished by using a combination of the Espresso and UiAutomator testing frameworks. In most cases, the swipe method from UiAutomator is all you need to simulate dual-screen events, and then Espresso can be used normally to interact with UI elements in your Surface Duo app.

For examples of UI testing in Surface Duo apps, check out these resources: