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.

Avatar for Su Myat

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