16. Testing Apps

16.1. Unit Testing

Unit tests are referred to as “local unit tests”. The tests run without a device or an emulator attached. However, unit tests cannot test the UI for your app without mocking objects such as an Activity. In your Android Studio project, source files for local unit tests are stored at module-name/src/test/java/.

Instrumented unit tests are tests that run on physical devices and emulators, they provide more fidelity than local unit tests, but they run much more slowly. Source files for instrumented tests are stored at module-name/src/androidTest/java/.

16.1.1. Unit Testing Example

This example shows a function that verifies the format of email address and a unit test for the function.

  1. First, create a new project, write a program that verifies the correctness of email addresses’ format, returns true if the email address has the correct address, otherwise return false.

    class EmailValidation {
       val emailPatternString ="""[a-zA-Z0-9\+\.\_\%\-]{1,256}\@[a-zA-Z0-9][a-zA-Z0-9\-]{0,64}(\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,25})+"""
    
       fun invalidateEmail(email: String): Boolean = email.matches(Regex(emailPatternString))
    }
    
  2. Then create a local unit test class, it should be written as a JUnit 4 test class. To create a basic JUnit 4 test class, create a class that contains one or more test methods. A test method begins with the @Test annotation and contains the code to exercise and verify a single functionality in the component that you want to test. In our example, there are several cases that should be considered:

    import org.junit.Assert.*
    import org.junit.Test
    
    class EmailValidationTest{
    
        val emailValidator:EmailValidation = EmailValidation()
    
        @Test
        fun correctEmail() {
            assertTrue(emailValidator.invalidateEmail("name@email.com"))
        }
    
        @Test
        fun correctEmailSubDomain() {
            assertTrue(emailValidator.invalidateEmail("name@email.co.uk"))
        }
    
        @Test
        fun invalidEmailNoTld() {
            assertFalse(emailValidator.invalidateEmail("name@email"))
        }
    
        @Test
        fun invalidEmailDoubleDot() {
            assertFalse(emailValidator.invalidateEmail("name@email..com"))
        }
    
        @Test
        fun invalidEmailNoUsername() {
            assertFalse(emailValidator.invalidateEmail("@email.com"))
        }
    
        @Test
        fun emptyString() {
            assertFalse(emailValidator.invalidateEmail(""))
        }
    }
    
  3. Now run the test to check if all tests are passed. If a test is passed, the test will be labeled with a green tick, otherwise, it will be labeled red.

    ../_images/unit_test.jpg
  4. Here are some methods for verifying results:

    assertEquals(expected, actual), verifies whether the value of expected is the same as actual.

    assertArrayEquals(expected, actual), verifies if the actual array is the same as expected.

    assertFalse(actual), verify that the returned value is false.

    assertNotNull(actual), verify that the returned value is not null.

    assertNull(actual), verify that the returned value is null.

    assertTrue(actual), verify that the returned value is true.

    assertThat(actual, matcher), verify that returned value meets the conditions in matcher.

16.2. Espresso Tests

Espresso is a framework that allows us to write tests on the user interface. Espresso Testing works basically in three blocks:

  • ViewMatchers – allows to find an item in the view

  • ViewActions – allows to execute actions on the elements

  • ViewAssertions – validate a view state

This example creates a simple app that a user can type message in a TextBox, click a button, then the message entered will be displayed.

  1. Add the following dependencies to build.gradle file.

    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    androidTestImplementation 'androidx.test:runner:1.1.0'
    androidTestImplementation 'androidx.test:rules:1.1.0'
    
  2. Create a simple app where it contains a EditText for user to type in, a button to click, and once the button is clicked, the message will be shown on the screen in a TextView.

    Example code:

    <EditText android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:hint="enter your message"
            android:id="@+id/message"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_marginTop="20dp" app:layout_constraintTop_toTopOf="parent"/>
    <Button
          android:text="Button"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="8dp" android:id="@+id/button"
          app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp" android:layout_marginTop="10dp"
          app:layout_constraintTop_toBottomOf="@+id/message"/>
    <TextView
          android:text="TextView"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
          android:id="@+id/textView" android:layout_marginTop="10dp"
          app:layout_constraintTop_toBottomOf="@+id/button"/>
    
    button.setOnClickListener {
       textView.text = message.text
    }
    
  3. Then create a test class under module-name/src/androidTest/java/, and create a class as following:

    package com.example.testing
    
    @RunWith(AndroidJUnit4::class)
    class MainActivityTest {
        @get:Rule
        val activityRule = ActivityTestRule(MainActivity::class.java)
    }
    
  4. First, we need to test whether the user can enter message:

    @Test
    fun user_can_type(){
        Espresso.onView(ViewMatchers.withId(R.id.message)).perform(ViewActions.typeText("Hello World"))
    }
    
  5. Then, whether the button can be clicked:

    @Test
    fun user_can_click(){
         Espresso.onView(ViewMatchers.withId(R.id.button)).perform(ViewActions.click())
    }
    
  6. Next, whether the entered message matches displayed message:

    @Test
    fun result_match(){
        Espresso.onView(ViewMatchers.withId(R.id.message)).perform(ViewActions.typeText("Hello World"))
        Espresso.onView(ViewMatchers.withId(R.id.button)).perform(ViewActions.click())
        Espresso.onView(ViewMatchers.withId(R.id.textView)).check(ViewAssertions.matches(ViewMatchers.withText("Hello World")))
    }
    
  7. Now run the test, all tests will be performed on emulator/device.