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

Unit Testing in a Nutshell - DroidKaigi 2018

Unit Testing in a Nutshell - DroidKaigi 2018

Unit Testing in a Nutshell - DroidKaigi 2018
This is a reference material for this session
https://droidkaigi.jp/2018/timetable?session=16942
Hands-On
https://github.com/srym/DroidKaigi2018UnitTestHandOn

Fumihiko Shiroyama

February 02, 2018
Tweet

More Decks by Fumihiko Shiroyama

Other Decks in Technology

Transcript

  1. Who is this guy? • Senior Android Developer @nikkei •

    Executive Beer Drinker • Chief Baby Sitter at home • Unit Test Enthusiast
  2. Agenda today • Why you should care about Unit Testing

    • JUnit4 and basic assertions • Mock and spy with Mockito • Robolectric for framework's code • Unit Testing SQLite • Unit Testing asynchronous code • Best practices and tips
  3. public class LoginActivity extends AppCompatActivity implements LoaderCallbacks<Cursor> { private static

    final int REQUEST_READ_CONTACTS = 0; private static final String[] DUMMY_CREDENTIALS = new String[]{ "[email protected]:hello", "[email protected]:world" }; private UserLoginTask mAuthTask = null; private AutoCompleteTextView mEmailView; private EditText mPasswordView; private View mProgressView; private View mLoginFormView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); mEmailView = (AutoCompleteTextView) findViewById(R.id.email); populateAutoComplete(); mPasswordView = (EditText) findViewById(R.id.password); mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { attemptLogin(); return true; } return false; } }); Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button); mEmailSignInButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { attemptLogin(); } }); mLoginFormView = findViewById(R.id.login_form); mProgressView = findViewById(R.id.login_progress); } private void populateAutoComplete() { if (!mayRequestContacts()) { return; } getLoaderManager().initLoader(0, null, this); } private boolean mayRequestContacts() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } if (checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { return true; } if (shouldShowRequestPermissionRationale(READ_CONTACTS)) { Snackbar.make(mEmailView, R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE) .setAction(android.R.string.ok, new View.OnClickListener() { @Override @TargetApi(Build.VERSION_CODES.M) public void onClick(View v) { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); } }); } else { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); } return false; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_READ_CONTACTS) { if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { populateAutoComplete(); } } } private void attemptLogin() { if (mAuthTask != null) { return; } mEmailView.setError(null); mPasswordView.setError(null); String email = mEmailView.getText().toString(); String password = mPasswordView.getText().toString(); boolean cancel = false; View focusView = null; if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) { mPasswordView.setError(getString(R.string.error_invalid_password)); focusView = mPasswordView; cancel = true; } if (TextUtils.isEmpty(email)) { mEmailView.setError(getString(R.string.error_field_required)); focusView = mEmailView; cancel = true; } else if (!isEmailValid(email)) { mEmailView.setError(getString(R.string.error_invalid_email)); focusView = mEmailView; cancel = true; } if (cancel) { focusView.requestFocus(); } else { showProgress(true); mAuthTask = new UserLoginTask(email, password); mAuthTask.execute((Void) null); } }
  4. public class LoginActivity extends AppCompatActivity implements LoaderCallbacks<Cursor> { private static

    final int REQUEST_READ_CONTACTS = 0; private static final String[] DUMMY_CREDENTIALS = new String[]{ "[email protected]:hello", "[email protected]:world" }; private UserLoginTask mAuthTask = null; private AutoCompleteTextView mEmailView; private EditText mPasswordView; private View mProgressView; private View mLoginFormView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); mEmailView = (AutoCompleteTextView) findViewById(R.id.email); populateAutoComplete(); mPasswordView = (EditText) findViewById(R.id.password); mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { attemptLogin(); return true; } return false; } }); Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button); mEmailSignInButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { attemptLogin(); } }); mLoginFormView = findViewById(R.id.login_form); mProgressView = findViewById(R.id.login_progress); } private void populateAutoComplete() { if (!mayRequestContacts()) { return; } getLoaderManager().initLoader(0, null, this); } private boolean mayRequestContacts() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } if (checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { return true; } if (shouldShowRequestPermissionRationale(READ_CONTACTS)) { Snackbar.make(mEmailView, R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE) .setAction(android.R.string.ok, new View.OnClickListener() { @Override @TargetApi(Build.VERSION_CODES.M) public void onClick(View v) { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); } }); } else { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); } return false; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_READ_CONTACTS) { if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { populateAutoComplete(); } } } private void attemptLogin() { if (mAuthTask != null) { return; } mEmailView.setError(null); mPasswordView.setError(null); String email = mEmailView.getText().toString(); String password = mPasswordView.getText().toString(); boolean cancel = false; View focusView = null; if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) { mPasswordView.setError(getString(R.string.error_invalid_password)); focusView = mPasswordView; cancel = true; } if (TextUtils.isEmpty(email)) { mEmailView.setError(getString(R.string.error_field_required)); focusView = mEmailView; cancel = true; } else if (!isEmailValid(email)) { mEmailView.setError(getString(R.string.error_invalid_email)); focusView = mEmailView; cancel = true; } if (cancel) { focusView.requestFocus(); } else { showProgress(true); mAuthTask = new UserLoginTask(email, password); mAuthTask.execute((Void) null); } } Faaaaaaaaaaaat Controller!!
  5. Fat Controller is bad because... • Slow iteration • Ambiguous

    responsibilities • Off-by-one errors • Un-reproducible bugs
  6. Unit testing lets you... • Faster execution • Clear division

    of roles • Chance to improve architecture
  7. See also... • EspressoςετίʔυͷಉظॲཧΛڀΊΔ • Day1 Room5 15:40 - •

    UIςετͷ࣮ߦ࣌ؒΛ୹ॖͤ͞Δํ๏ • Day2 Room2 18:30 -
  8. JUnit4 is... • A simple testing framework • Available for

    Android out-of-the-box • Still de-facto standard Moving Forward with JUnit 5 - Day2 Room5 10:30 - Check also
  9. Assertion is... • Assert something is something A rose is

    a rose is a rose is a rose... assertThat(1 + 1).isEqualTo(2); assertThat("Alice") .isNotEmpty() .isNotEqualTo("Bob");
  10. Test target class InputChecker { boolean isValid(String text) { if

    (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } } VERY SIMPLE input text checker
  11. Types of test in Android • Local Unit Tests •

    Runs on JVM • src/test • Instrumented Tests • Runs with Device/Emulators • src/androidTest
  12. • TRUE Android Test • Exactly the same behavior as

    production Instrumented Tests: pros
  13. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { } @Test public void isValid() throws Exception { } }
  14. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { } @Test public void isValid() throws Exception { } } Just naming convention
  15. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { } @Test public void isValid() throws Exception { } } Called at every test case
  16. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { } @Test public void isValid() throws Exception { } } This is a test case Write as many as you want
  17. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { }foo @Test public void isValid() throws Exception { }foo }foo
  18. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); }foo @Test public void isValid() throws Exception { }foo }foo
  19. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { } } Initialization codes here
  20. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); }foo @Test public void isValid() throws Exception { }foo }foo
  21. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); }foo @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); }foo }foo
  22. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } } Write your assertions
  23. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } } what really is "actual"
  24. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } } "expectation" what you expect it to be
  25. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } } From AssertJ Many assertion methods available
  26. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } }
  27. Assertions (AssertJ) • assertThat(all) • isEqualTo / isNotEqualTo • assertThat(String)

    • isEmpty / isNotEmpty • assertThat(Number) • isPositive / isNegative / isZero / isNotZero • isGreaterThan / isLessThan lots and lots and lots more
  28. • assertThat(list) • contains / containsOnly / containsExactly Assertions (AssertJ)

    lots and lots and lots more List<String> strings = Arrays.asList("foo", "bar", "baz"); assertThat(strings).contains("bar"); same collection same collection and order
  29. Remember this... class InputChecker { boolean isValid(String text) { if

    (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } }
  30. Remember this... class InputChecker { boolean isValid(String text) { if

    (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } } What if null... Let's try it anyway
  31. Ouch!! @Test public void isValid() throws Exception { String input

    = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  32. Expected Exception @Test(expected = NullPointerException.class) public void isValid() throws Exception

    { String input = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  33. Expected Exception @Test(expected = NullPointerException.class) public void isValid() throws Exception

    { String input = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  34. Passed!! @Test(expected = NullPointerException.class) public void isValid() throws Exception {

    String input = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  35. Better yet... class InputChecker { boolean isValid(String text) { if

    (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; }foo }foo
  36. Better yet... class InputChecker { boolean isValid(@NonNull String text) {

    if (text == null) throw new IllegalArgumentException("cannot be null"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; }foo }foo
  37. Better yet... class InputChecker { boolean isValid(@NonNull String text) {

    if (text == null) throw new IllegalArgumentException("cannot be null"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } }
  38. Better yet... class InputChecker { boolean isValid(@NonNull String text) {

    if (text == null) throw new IllegalArgumentException("cannot be null"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } } Unit testing helps you refactoring!!
  39. Way better!! @Test(expected = IllegalArgumentException.class) public void isValid() throws Exception

    { String input = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  40. Mock is... • A WHOLE imitation of a real object

    • Used to simulate the behavior of depending objects
  41. Spy is... • A PARTIAL imitation of a real object

    • Used to simulate the behavior of depending objects
  42. TweetRepository public class TweetRepository { private final LocalTweetDataSource localDataSource; public

    TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  43. TweetRepository public class TweetRepository { private final LocalTweetDataSource localDataSource; public

    TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } depending on local data source
  44. Data Source public class LocalTweetDataSource { public List<Tweet> getTimeline() {

    List<Tweet> tweets = new ArrayList<>(); // some local DB interaction // ..... return tweets; } }
  45. Data Source public class LocalTweetDataSource { public List<Tweet> getTimeline() {

    List<Tweet> tweets = new ArrayList<>(); // some local DB interaction // ..... return tweets; } } Because it dose so many interactions with DB You don't wanna deal with all those messes!
  46. TweetRepository public class TweetRepository { private final LocalTweetDataSource localDataSource; public

    TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } You can replace this with simple mock
  47. Installation dependencies { testImplementation 'org.mockito:mockito-inline:2.13.0' } mockito-inline lets you mock

    final class & methods this artifact may be integrated into default in the future but you can't mock static methods & constructors
  48. Stubbing private TweetRepository tweetRepository; @Before public void setUp() throws Exception

    { LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) ); tweetRepository = new TweetRepository(localDataSource); }
  49. Stubbing private TweetRepository tweetRepository; @Before public void setUp() throws Exception

    { LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) ); tweetRepository = new TweetRepository(localDataSource); }
  50. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); }
  51. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); }
  52. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); }
  53. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); }
  54. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); } pass!!
  55. Spying is... • Stub methods can be added on a

    REAL object That means spied objects acts EXACTLY the same as real objects unless stubbed
  56. Spying List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  57. Spying List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); normal ArrayList
  58. Spying List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); wrap with spy()
  59. Spying List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); things you typically to with List
  60. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  61. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  62. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  63. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  64. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  65. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); notice!
  66. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  67. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  68. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); no interaction
  69. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); argument matcher
  70. Argument matchers • eq(T) • anyString() • anyInt() • any()

    • any(Class<T>) • nullable(Class<T>) null or T including null
  71. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else"); if you wanna change return val according to the arguments
  72. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when().thenAnswer(invocation -> { });
  73. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else");
  74. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else");
  75. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else");
  76. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else"); whatever logic you like
  77. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else"); Don't abuse this since it spoils simplicity
  78. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    when(strings.get(3)).thenReturn("foo"); 0 1 2 Because this is a spy, not mock, List#get() is evaluated first that causes an Exception
  79. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo");
  80. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo");
  81. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo");
  82. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo");
  83. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo"); This works well!
  84. InputChecker class InputChecker { boolean isValid(@NonNull String text) { if

    (text == null) throw new IllegalArgumentException("cannot be null"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } }
  85. InputChecker class InputChecker { boolean isValid(@NonNull String text) { if

    (TextUtils.isEmpty(text)) throw new IllegalArgumentException("cannot be blank"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } }
  86. InputChecker class InputChecker { boolean isValid(@NonNull String text) { if

    (TextUtils.isEmpty(text)) throw new IllegalArgumentException("cannot be blank"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } } android.text.TextUtils
  87. InputCheckerTest public class InputCheckerTest { private InputChecker inputChecker; @Before public

    void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); } }
  88. app/build.gradle android { testOptions { unitTests.returnDefaultValues = true } }

    notice: this setting just return the default value @Test isValid() passes successfully
  89. InputCheckerTest public class InputCheckerTest { private InputChecker inputChecker; @Before public

    void setUp() throws Exception { inputChecker = new InputChecker(); } @Test(expected = IllegalArgumentException.class) public void isValid_fails_with_blank() throws Exception { String input = ""; inputChecker.isValid(input); } } Add a test case that checks blank input
  90. InputCheckerTest public class InputCheckerTest { private InputChecker inputChecker; @Before public

    void setUp() throws Exception { inputChecker = new InputChecker(); } @Test(expected = IllegalArgumentException.class) public void isValid_fails_with_blank() throws Exception { String input = ""; inputChecker.isValid(input); } } Expected exception was not thrown! This is because of the "default value" from test options
  91. Robolectric is... • "A unit test framework that de-fangs the

    Android SDK jar so you can test-drive the development of your Android app." • "Tests run inside the JVM on your workstation in seconds." quoted from the official site
  92. InputCheckerTest @RunWith(RobolectricTestRunner.class) public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test(expected = IllegalArgumentException.class) public void isValid_fails_with_blank() throws Exception { String input = ""; inputChecker.isValid(input); } }
  93. InputCheckerTest @RunWith(RobolectricTestRunner.class) public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test(expected = IllegalArgumentException.class) public void isValid_fails_with_blank() throws Exception { String input = ""; inputChecker.isValid(input); } } Just add this!!
  94. Context in Robolectric @RunWith(RobolectricTestRunner.class) public class SQLiteTest { private SQLiteDatabase

    db; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); SQLiteOpenHelper helper = new MySQLiteOpenHelper(context); db = helper.getWritableDatabase(); } @Test public void test_db() throws Exception { // do the stuff } }
  95. Context in Robolectric @RunWith(RobolectricTestRunner.class) public class SQLiteTest { private SQLiteDatabase

    db; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); SQLiteOpenHelper helper = new MySQLiteOpenHelper(context); db = helper.getWritableDatabase(); } @Test public void test_db() throws Exception { // do the stuff } } Really handy to use!!
  96. • http://robolectric.org/ • http://robolectric.org/getting-started/ • http://robolectric.org/extending/ Robolectric One thing you

    have to notice about Robolectric is It just mimics the framework's behavior, not the real one
  97. • Robolectric simulates SQLite out-of-the-box!! • You can use traditional

    SQLiteOpenHelper • You can also use the latest Room Persistence Library Unit Testing SQLite
  98. Room Persistence Library @Entity public class User { @PrimaryKey private

    int uid; @ColumnInfo(name = "first_name") private String firstName; @ColumnInfo(name = "last_name") private String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } // getters & setters } Entity
  99. Room Persistence Library @Dao public interface UserDao { @Query("SELECT *

    FROM user") List<User> getAll(); @Query("SELECT * FROM user WHERE uid IN (:userIds)") List<User> loadAllByIds(int[] userIds); @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + "last_name LIKE :last LIMIT 1") User findByName(String first, String last); @Insert void insertAll(User... users); @Delete void delete(User user); } Dao
  100. Room Persistence Library @Database(entities = {User.class}, version = 1) public

    abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } Database
  101. Room Persistence Library public class UserManager { private final String

    DB_NAME = "sample_app"; private final AppDatabase db; public UserManager(@NonNull Context context) { db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME) .allowMainThreadQueries() .build(); } public void insertUser(@NonNull User user) { db.userDao().insertAll(user); } public List<User> getUsers() { return db.userDao().getAll(); } } Business Logic
  102. Room Persistence Library public class UserManager { private final String

    DB_NAME = "sample_app"; private final AppDatabase db; public UserManager(@NonNull Context context) { db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME) .allowMainThreadQueries() .build(); } public void insertUser(@NonNull User user) { db.userDao().insertAll(user); } public List<User> getUsers() { return db.userDao().getAll(); } } Business Logic just for demo's convenience
  103. Room Persistence Library public class UserManager { private final String

    DB_NAME = "sample_app"; private final AppDatabase db; public UserManager(@NonNull Context context) { db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME) .allowMainThreadQueries() .build(); } public void insertUser(@NonNull User user) { db.userDao().insertAll(user); } public List<User> getUsers() { return db.userDao().getAll(); } } Business Logic delegate
  104. Room Persistence Library public class UserManager { private final String

    DB_NAME = "sample_app"; private final AppDatabase db; public UserManager(@NonNull Context context) { db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME) .allowMainThreadQueries() .build(); } public void insertUser(@NonNull User user) { db.userDao().insertAll(user); } public List<User> getUsers() { return db.userDao().getAll(); } } Business Logic delegate
  105. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  106. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  107. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  108. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  109. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  110. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  111. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } } DB & table's data are reset automatically
  112. Testing async code • Testing asynchronous code is not always

    easy I'd introduce the very basic concept of it!
  113. Synchronous call interface Fetcher<T> { T fetch(); class StringFetcher implements

    Fetcher<String> { @Override public String fetch() { return "OK"; } } } Think of a synchronous API call over internet
  114. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } } Think of an asynchronous API call over internet
  115. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } }
  116. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } }
  117. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } } Executed on a different thread
  118. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } } time intensive task
  119. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } } Callbacks
  120. Asynchronous Test public class AsyncFetcherTest { private Fetcher<String> fetcher; private

    AsyncFetcher<String> asyncFetcher; @Before public void setUp() throws Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); } @Test public void fetch_success() throws Exception { asyncFetcher.fetch(); } }
  121. Asynchronous Test public class AsyncFetcherTest { private Fetcher<String> fetcher; private

    AsyncFetcher<String> asyncFetcher; @Before public void setUp() throws Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); } @Test public void fetch_success() throws Exception { asyncFetcher.fetch(); } }
  122. Asynchronous Test public class AsyncFetcherTest { private Fetcher<String> fetcher; private

    AsyncFetcher<String> asyncFetcher; @Before public void setUp() throws Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); } @Test public void fetch_success() throws Exception { asyncFetcher.fetch(); } } How do I write this test?
  123. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); }, e -> log("async: ng") ); log("fetch_success: end"); }
  124. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); }, e -> log("async: ng") ); log("fetch_success: end"); } success
  125. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); }, e -> log("async: ng") ); log("fetch_success: end"); } failure
  126. Asynchronous Test @Test public void fetch_failure() throws Exception { doThrow(new

    RuntimeException("NG")).when(fetcher).fetch(); asyncFetcher.fetch( result -> log("async: ok"), e -> { assertThat(e.getMessage()).isEqualTo("NG"); log("async: ng"); } ); log("fetch_failure: end"); }
  127. Asynchronous Test @Test public void fetch_failure() throws Exception { doThrow(new

    RuntimeException("NG")).when(fetcher).fetch(); asyncFetcher.fetch( result -> log("async: ok"), e -> { assertThat(e.getMessage()).isEqualTo("NG"); log("async: ng"); } ); log("fetch_failure: end"); } stub failure
  128. Asynchronous Test private CountDownLatch latch; @Before public void setUp() throws

    Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); latch = new CountDownLatch(1); }
  129. Asynchronous Test Acts like an ON/OFF latch private CountDownLatch latch;

    @Before public void setUp() throws Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); latch = new CountDownLatch(1); }
  130. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); latch.countDown(); }, e -> log("async: ng") ); latch.await(); log("fetch_success: end"); }
  131. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); latch.countDown(); }, e -> log("async: ng") ); latch.await(); log("fetch_success: end"); }
  132. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); latch.countDown(); }, e -> log("async: ng") ); latch.await(); log("fetch_success: end"); } Test execution awaits here
  133. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); latch.countDown(); }, e -> log("async: ng") ); latch.await(); log("fetch_success: end"); } Opens the latch and test goes again
  134. Asynchronous Test @Test public void fetch_failure() throws Exception { doThrow(new

    RuntimeException("NG")).when(fetcher).fetch(); asyncFetcher.fetch( result -> log("async: ok"), e -> { assertThat(e.getMessage()).isEqualTo("NG"); log("async: ng"); latch.countDown(); } ); latch.await(); log("fetch_failure: end"); }
  135. Asynchronous Test @Test public void fetch_failure() throws Exception { doThrow(new

    RuntimeException("NG")).when(fetcher).fetch(); asyncFetcher.fetch( result -> log("async: ok"), e -> { assertThat(e.getMessage()).isEqualTo("NG"); log("async: ng"); latch.countDown(); } ); latch.await(); log("fetch_failure: end"); } The same implementation as success
  136. Additional info... • MockWebServer for OkHttp • TestSubscriber for RxJava

    • https://speakerdeck.com/srym/ hayaiyasuiumai-sutatoatupudemoshi-eru- retrofit-plus-rxjava-deshun-jian- apikutukinguresipi?slide=17 Check this slide & sample project
  137. Best practices and tips • Use interface at the structural

    boundary • Use DI pattern for testability • Static is EVIL • Don't be a Mad Tester
  138. Repository pattern public class TweetRepository { private final LocalTweetDataSource localDataSource;

    public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  139. Repository pattern public class LocalTweetDataSource { public List<Tweet> getTimeline() {

    List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } Concrete data source
  140. Repository pattern public interface LocalTweetDataSource { List<Tweet> getTimeline(); class SQLiteTweetDataSource

    implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } class MockTweetDataSource implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { return Arrays.asList(/* some mock data */); } } }
  141. Repository pattern public interface LocalTweetDataSource { List<Tweet> getTimeline(); class SQLiteTweetDataSource

    implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } class MockTweetDataSource implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { return Arrays.asList(/* some mock data */); } } }
  142. Repository pattern public interface LocalTweetDataSource { List<Tweet> getTimeline(); class SQLiteTweetDataSource

    implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } class MockTweetDataSource implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { return Arrays.asList(/* some mock data */); } } } Concrete data source with real DB interactions
  143. Repository pattern public interface LocalTweetDataSource { List<Tweet> getTimeline(); class SQLiteTweetDataSource

    implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } class MockTweetDataSource implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { return Arrays.asList(/* some mock data */); } } } mock data source just for tests
  144. Repository pattern public class TweetRepositoryTest { private TweetRepository tweetRepository; @Before

    public void setUp() throws Exception { LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz"))); tweetRepository = new TweetRepository(localDataSource); } }
  145. Repository pattern public class TweetRepositoryTest { private TweetRepository tweetRepository; @Before

    public void setUp() throws Exception { LocalTweetDataSource localDataSource = new LocalTweetDataSource.MockTweetDataSource(); tweetRepository = new TweetRepository(localDataSource); } } You can even get rid of Mockito dependency
  146. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository() { this.localDataSource = new LocalTweetDataSource.SQLiteTweetDataSource(); } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  147. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository() { this.localDataSource = new LocalTweetDataSource.SQLiteTweetDataSource(); } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } mocking field is painful!!
  148. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository() { this.localDataSource = new LocalTweetDataSource.SQLiteTweetDataSource(); } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  149. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  150. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } Putting dependency from outer
  151. Use DI pattern • This is so-called DI pattern •

    DI pattern does NOT have to use DI library • DI library (e.g. Dagger2) is useful though e.g. Factory Pattern
  152. Static is EVIL because... • Static has global scope •

    Mockito cannot mock static • Static is practically unnecessary This may cause order dependent bugs
  153. Static is practically unnecessary @Singleton public class TweetRepository { private

    final LocalTweetDataSource localDataSource; @Inject public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  154. Static is practically unnecessary @Singleton public class TweetRepository { private

    final LocalTweetDataSource localDataSource; @Inject public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } Use Dagger's annotations for handy singleton
  155. Don't be a Mad Tester • Avoid to test EVERYTHING

    • Unit tests are NOT perfect • Gain maximum with minimum investment! Enjoy your test life!!!
  156. Thank you for listening! • All illustrations by www.irasutoya.com •

    Special Thanks to @sumio_tym, @numa08 • Get the slides: https://goo.gl/ju66No • Get the materials: https://goo.gl/R5kBeN