Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Exploring Android Jetpack Testing

Exploring Android Jetpack Testing

It's all about android testing with hands-on things.

Su Myat

April 16, 2020
Tweet

More Decks by Su Myat

Other Decks in Programming

Transcript

  1. OVERVIEW What we’ll talk about today 1.Why we need testing:

    the problem with the way we build apps and why your current approach will tire you out 2.Types of Testing: Unit, Instrumentation & UI 3.Android Testing in Action: let’s get our hands dirty ;)
  2. WHY ANDROID TESTING WILL MAKE YOUR LIFE EASIER The problem

    with how we do things now ‣ We are building application that are unnecessarily complex. ‣ It become developers are scared to refactor code and not confident about their changes. ‣ We have to test that previous features working properly. ‣ They are not sure about regression testing. ‣ Doing this manually every time is tiring. It makes you prone to mistakes and does not scale
  3. TESTING ON ANDROID TEST UI INSTRUMENTATION UNIT ‣ Junit5 ‣

    Mockito ‣ Junit4 ‣ Mockito ‣ Expresso
  4. TESTING ON ANDROID Unit Testing ‣ Local Computer ‣ Java

    Virtual Machine (JVM) ‣ Very fast ‣ JUnit5, Mockito
  5. TESTING ON ANDROID Instrumentation Test ‣ Similar to local unit

    tests ‣ Need a real device or emulator ‣ JUnit4, Mockito
  6. TESTING ON ANDROID UI Test ‣ Simulate a person using

    your app ‣ Literally uses widgets ‣ Real device or emulator ‣ Expresso
  7. TESTING WRITING PROCESS : TIPS & TRICKS public class NoteTest

    { /* Compare two equal Notes */ /* Compare notes with 2 different ids */ /* Compare two notes with different timestamps */ /* Compare two notes with different titles */ }
  8. MODEL TEST WITH JUNITS class NoteTest { companion object {

    const val TITLE_1 = "Note #1" const val CONTENT_1 = "This is note #1" const val TIMESTAMP_1 = "05-2019" } @Throws(Exception::class) @Test fun isNotesEqual_identicalProperties_returnTrue() { // Arrange val note1 = Note(TITLE_1, CONTENT_1, TIMESTAMP_1) note1.id = 1 val note2 = Note(TITLE_1, CONTENT_1, TIMESTAMP_1) note2.id = 1 assertEquals(note1, note2) println("The notes are equal!") } @Throws(Exception::class) @Test fun isNotesEqual_differentIds_returnFalse() { // Arrange val note1 = Note("Note #1", "This is note #1", TIMESTAMP_1) note1.id = 1 val note2 = Note("Note #1", "This is note #1", TIMESTAMP_1) note2.id = 2 assertNotEquals(note1, note2) println("The notes are not equal!") } }
  9. DATEUTIL IN TEST PROJECT public class DateUtil { public static

    final String[] monthNumbers = {"01","02","03","04","05","06","07","08","09","10","11","12"}; public static final String[] months = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}; public static final String GET_MONTH_ERROR = "Error. Invalid month number."; public static final String DATE_FORMAT = "MM-yyyy"; public static String getCurrentTimeStamp() throws Exception{ try { SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); return dateFormat.format(new Date()); // Find todays date } catch (Exception e) { e.printStackTrace(); throw new Exception("Couldn't format the date into MM-yyyy"); } } public static String getMonthFromNumber(String monthNumber){ switch(monthNumber){ case "01":{ return months[0]; }.. default:{ return GET_MONTH_ERROR; } } } }
  10. DATEUTIL TEST IN ASSERTION public class DateUtilTest { companion object

    { private const val today = “04-2020” } @Test public void testGetCurrentTimestamp_returnedTimestamp(){ assertDoesNotThrow(new Executable() { @Override public void execute() throws Throwable { assertEquals(today, DateUtil.getCurrentTimeStamp()); System.out.println("Timestamp is generated correctly"); } }); } }
  11. DATEUTIL TEST IN PARAMETERIZED class DateUtilTest { @ParameterizedTest @ValueSource(ints =

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) fun getMonthFromNumber_returnSuccess(monthNumber: Int, testInfo: TestInfo?, testReporter: TestReporter?) { assertEquals(DateUtil.months[monthNumber], DateUtil.getMonthFromNumber(DateUtil.monthNumbers[monthNumber])) println(DateUtil.monthNumbers[monthNumber].toString() + " : " + DateUtil.months[monthNumber]) } }
  12. DB INSTRUMENTATION TEST public abstract class NoteDatabaseTest { private NoteDatabase

    noteDatabase; public NoteDao getNoteDao(){ return noteDatabase.getNoteDao(); } @Before public void init(){ noteDatabase = Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), NoteDatabase.class ).build(); } @After public void finish(){ noteDatabase.close(); } }
  13. SHARING RESOURCES BETWEEN TESTS DIRECTORIES object TestUtil { const val

    TIMESTAMP_1 = "05-2019" @JvmField val TEST_NOTE_1 = Note("Take out the trash", "It's garbage day tomorrow.", TIMESTAMP_1) const val TIMESTAMP_2 = "06-2019" val TEST_NOTE_2 = Note("Anniversary gift", "Buy an anniversary gift.", TIMESTAMP_2) @JvmField val TEST_NOTES_LIST: List<Note> = mutableListOf(Note(1, "Take out the trash", "It's garbage day tomorrow.", TIMESTAMP_1), Note(2, "Anniversary gift", "Buy an anniversary gift.", TIMESTAMP_2)) } /* Will use like blow in android tests rectories */ val note = Note(TestUtil.TEST_NOTE_1)
  14. LIVEDATA TESTING public class LiveDataTestUtil<T> { public T getValue(final LiveData<T>

    liveData) throws InterruptedException { final List<T> data = new ArrayList<>(); // latch for blocking thread until data is set final CountDownLatch latch = new CountDownLatch(1); Observer<T> observer = new Observer<T>() { @Override public void onChanged(T t) { data.add(t); latch.countDown(); // release the latch liveData.removeObserver(this); } }; liveData.observeForever(observer); try { latch.await(2, TimeUnit.SECONDS); // wait for onChanged to fire and set d } catch (InterruptedException e) { throw new InterruptedException("Latch failure"); } if(data.size() > 0){ return data.get(0); } return null; } }
  15. INSERT, READ & DELETE class NoteDaoTest : NoteDatabaseTest() { @Rule

    var rule = InstantTaskExecutorRule() @Test @Throws(Exception::class) fun insertReadDelete() { val note = Note(TestUtil.TEST_NOTE_1) // insert noteDao.insertNote(note).blockingGet() // wait until inserted // read val liveDataTestUtil = LiveDataTestUtil<List<Note>>() var insertedNotes = liveDataTestUtil.getValue(noteDao.notes) Assert.assertNotNull(insertedNotes) Assert.assertEquals(note.content, insertedNotes[0].content) Assert.assertEquals(note.timestamp, insertedNotes[0].timestamp) Assert.assertEquals(note.title, insertedNotes[0].title) note.id = insertedNotes[0].id Assert.assertEquals(note, insertedNotes[0]) // delete noteDao.deleteNote(note).blockingGet() // confirm the database is empty insertedNotes = liveDataTestUtil.getValue(noteDao.notes) Assert.assertEquals(0, insertedNotes.size.toLong()) } }
  16. SHARING RESOURCES VIA LIVEDATA TEST @ExtendWith(InstantExecutorExtension::class) class NoteRepositoryTest { private

    var noteRepository: NoteRepository? = null private lateinit var noteDao: NoteDao @BeforeEach fun initEach() { noteDao = Mockito.mock(NoteDao::class.java) noteRepository = NoteRepository(noteDao) } @Test @Throws(Exception::class) fun insertNote_returnRow() { // Arrange val insertedRow = 1L val returnedData = Single.just(insertedRow) Mockito.`when`(noteDao!!.insertNote(ArgumentMatchers.any(Note::class.java))).thenReturn(re turnedData) // Act val returnedValue = noteRepository!!.insertNote(NOTE1).blockingSingle() // Assert Mockito.verify(noteDao)!!.insertNote(ArgumentMatchers.any(Note::class.java)) Mockito.verifyNoMoreInteractions(noteDao) println("Returned value: " + returnedValue.data) Assertions.assertEquals(Resource.success(1, NoteRepository.INSERT_SUCCESS), returnedValue) } }
  17. RESPOSITORY TEST @ExtendWith(InstantExecutorExtension::class) class NoteRepositoryTest { private var noteRepository: NoteRepository?

    = null private lateinit var noteDao: NoteDao @BeforeEach fun initEach() { noteDao = Mockito.mock(NoteDao::class.java) noteRepository = NoteRepository(noteDao) } @Test @Throws(Exception::class) fun insertNote_returnRow() { // Arrange val insertedRow = 1L val returnedData = Single.just(insertedRow) `when`(noteDao!!.insertNote(ArgumentMatchers.any(Note::class.java))).thenReturn(returnedData) // Act val returnedValue = noteRepository!!.insertNote(NOTE1).blockingSingle() // Assert verify(noteDao)!!.insertNote(ArgumentMatchers.any(Note::class.java)) verifyNoMoreInteractions(noteDao) println("Returned value: " + returnedValue.data) assertEquals(Resource.success(1, NoteRepository.INSERT_SUCCESS), returnedValue) } }
  18. VIEWMODEL TEST : PART I @ExtendWith(InstantExecutorExtension::class) class NoteViewModelTest { private

    var noteViewModel: NoteViewModel? = null @Mock private val noteRepository: NoteRepository? = null @BeforeEach fun init() { MockitoAnnotations.initMocks(this) noteViewModel = NoteViewModel(noteRepository) } @Test @Throws(Exception::class) fun observeEmptyNoteWhenNoteSet() { val liveDataTestUtil = LiveDataTestUtil<Note>() val note = liveDataTestUtil.getValue(noteViewModel!!.observeNote()) Assertions.assertNull(note) }
  19. VIEWMODEL TEST : PART II @Test @Throws(Exception::class) fun observeNote_whenSet() {

    val note = Note(TestUtil.TEST_NOTE_1) val liveDataTestUtil = LiveDataTestUtil<Note>() noteViewModel!!.setNote(note) val observedNote = liveDataTestUtil.getValue(noteViewModel!!.observeNote()) Assertions.assertEquals(note, observedNote) }
  20. TESTING ACTIVITIES IN ISOLATION : UI TEST @RunWith(AndroidJUnit4ClassRunner::class) class ActiviesIsolationTest

    { /** * Test both ways to navigate from NotesListActivity to NoteActivity */ @Test fun test_navNotesListActivity() { val activityScenario = ActivityScenario.launch(NotesListActivity::class.java) onView(withId(R.id.fab)).perform(click()) onView(withId(R.id.note_text)).check(matches(isDisplayed())) pressBack() onView(withId(R.id.parent)).check(matches(isDisplayed())) } }
  21. ESPRESSOIDLINGRESOURCE @FixMethodOrder(MethodSorters.NAME_ASCENDING) @RunWith(AndroidJUnit4ClassRunner::class) class NotesListActivityTest{ val LIST_ITEM_IN_TEST = 0 val

    TITLE_IN_TEST = "Android Night" @get:Rule val activityRule = ActivityScenarioRule(NotesListActivity::class.java) @Before fun registerIdlingResource() { IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) } @After fun unregisterIdlingResource() { IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) } @Test fun a_test_isNotesListActivityVisible_onAppLaunch() { onView(withId(R.id.recyclerview)).check(matches(isDisplayed())) } @Test fun test_selectListItem_isNoteActivityVisible() { // Click list item #LIST_ITEM_IN_TEST onView(withId(R.id.recyclerview)) .perform(actionOnItemAtPosition<NotesRecyclerAdapter.ViewHolder>(LIST_ITEM_IN_TEST, click())) // Confirm nav to NoteActivity and display title onView(withId(R.id.note_text_title)).check(matches(withText(TITLE_IN_TEST))) } }
  22. CUSTOM TEST RULE @FixMethodOrder(MethodSorters.NAME_ASCENDING) @RunWith(AndroidJUnit4ClassRunner::class) class NotesListActivityTest{ val LIST_ITEM_IN_TEST =

    0 val TITLE_IN_TEST = "Android Night" @get:Rule val activityRule = ActivityScenarioRule(NotesListActivity::class.java) @get: Rule val espressoIdlingResoureRule = EspressoIdlingResourceRule() @Test fun a_test_isNotesListActivityVisible_onAppLaunch() { onView(withId(R.id.recyclerview)).check(matches(isDisplayed())) } @Test fun test_selectListItem_isNoteActivityVisible() { // Click list item #LIST_ITEM_IN_TEST onView(withId(R.id.recyclerview)) .perform(actionOnItemAtPosition<NotesRecyclerAdapter.ViewHolder>(LIST_ITEM_IN_TEST, click())) // Confirm nav to NoteActivity and display title onView(withId(R.id.note_text_title)).check(matches(withText(TITLE_IN_TEST))) } }
  23. COMMON ESPRESSOIDLINGRESOURCE class EspressoIdlingResourceRule : TestWatcher(){ private val idlingResource =

    EspressoIdlingResource.countingIdlingResource override fun finished(description: Description?) { IdlingRegistry.getInstance().unregister(idlingResource) super.finished(description) } override fun starting(description: Description?) { IdlingRegistry.getInstance().register(idlingResource) super.starting(description) } }
  24. DEBUG $ RELEASE OF EXPRESSO IDLING RESOURCE object EspressoIdlingResource {

    fun increment() { } fun decrement() { } } object EspressoIdlingResource { private const val RESOURCE = "GLOBAL" @JvmField val countingIdlingResource = CountingIdlingResource(RESOURCE) fun increment() { countingIdlingResource.increment() } fun decrement() { if (!countingIdlingResource.isIdleNow) { countingIdlingResource.decrement() } } }
  25. - Testing on Android may seem intimidating, but in the

    long run it will save you from many headaches. - Before you jump into android testing, ask yourself: is my architecture simplified and ‘testable’? Have I defined the scope, speed and fidelity of the tests I want to run? - Hands on Custom Test Template, Common Resources for Tests, Test Coverage, Test Suites, Firebase Test Lab, Custom Test Rules - Keep learning: https://junit.org/junit5/docs/current/user-guide/ #writing-tests, https://github.com/mannodermaus/android-junit5
 Key Takeaways