Slide 1

Slide 1 text

WHY + HOW OF ANDROID TESTING EXPLORING ANDROID JETPACK TESTING

Slide 2

Slide 2 text

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 ;)

Slide 3

Slide 3 text

I: 
 why we need testing

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

II: Testing on Android

Slide 6

Slide 6 text

TESTING ON ANDROID TEST UI INSTRUMENTATION UNIT ‣ Junit5 ‣ Mockito ‣ Junit4 ‣ Mockito ‣ Expresso

Slide 7

Slide 7 text

TESTING ON ANDROID Unit Testing ‣ Local Computer ‣ Java Virtual Machine (JVM) ‣ Very fast ‣ JUnit5, Mockito

Slide 8

Slide 8 text

TESTING ON ANDROID Instrumentation Test ‣ Similar to local unit tests ‣ Need a real device or emulator ‣ JUnit4, Mockito

Slide 9

Slide 9 text

THE REASON WHY I’M USING JUNIT4 FOR INSTRUMENTATION TEST

Slide 10

Slide 10 text

TESTING ON ANDROID UI Test ‣ Simulate a person using your app ‣ Literally uses widgets ‣ Real device or emulator ‣ Expresso

Slide 11

Slide 11 text

III: Android Testing in Action

Slide 12

Slide 12 text

CRITICAL USER JOURNEY

Slide 13

Slide 13 text

ARCHITECTURE OVERVIEW

Slide 14

Slide 14 text

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 */ }

Slide 15

Slide 15 text

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!") } }

Slide 16

Slide 16 text

TESTING WRITING PROCESS @Test @Throws(Exception::class) fun name() { // Arrange // Act // Assert }

Slide 17

Slide 17 text

JUNIT5 TESTS, PARAMETERIZED & ASSERTIONS ..

Slide 18

Slide 18 text

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; } } } }

Slide 19

Slide 19 text

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"); } }); } }

Slide 20

Slide 20 text

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]) } }

Slide 21

Slide 21 text

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(); } }

Slide 22

Slide 22 text

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 = 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)

Slide 23

Slide 23 text

LIVEDATA TESTING public class LiveDataTestUtil { public T getValue(final LiveData liveData) throws InterruptedException { final List data = new ArrayList<>(); // latch for blocking thread until data is set final CountDownLatch latch = new CountDownLatch(1); Observer observer = new Observer() { @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; } }

Slide 24

Slide 24 text

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>() 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()) } }

Slide 25

Slide 25 text

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) } }

Slide 26

Slide 26 text

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) } }

Slide 27

Slide 27 text

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() val note = liveDataTestUtil.getValue(noteViewModel!!.observeNote()) Assertions.assertNull(note) }

Slide 28

Slide 28 text

VIEWMODEL TEST : PART II @Test @Throws(Exception::class) fun observeNote_whenSet() { val note = Note(TestUtil.TEST_NOTE_1) val liveDataTestUtil = LiveDataTestUtil() noteViewModel!!.setNote(note) val observedNote = liveDataTestUtil.getValue(noteViewModel!!.observeNote()) Assertions.assertEquals(note, observedNote) }

Slide 29

Slide 29 text

TEST COVERAGE

Slide 30

Slide 30 text

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())) } }

Slide 31

Slide 31 text

TESTING ACTIVITIES IN ISOLATION

Slide 32

Slide 32 text

TEST SUITES @RunWith(Suite::class) @Suite.SuiteClasses( ActiviesIsolationTest::class, NotesListActivityTest::class ) class NoteActivitesTestSuite

Slide 33

Slide 33 text

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(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))) } }

Slide 34

Slide 34 text

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(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))) } }

Slide 35

Slide 35 text

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) } }

Slide 36

Slide 36 text

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() } } }

Slide 37

Slide 37 text

FIREBASE TESTLAB

Slide 38

Slide 38 text

- 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

Slide 39

Slide 39 text

thank y u.