Slide 1

Slide 1 text

Unit Testing in a Nutshell Fumihiko Shiroyama DroidKaigi 2018

Slide 2

Slide 2 text

Who is this guy? • Senior Android Developer @nikkei • Executive Beer Drinker • Chief Baby Sitter at home • Unit Test Enthusiast

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Why you should care about Unit Testing?

Slide 5

Slide 5 text

What if you DON'T?

Slide 6

Slide 6 text

public class LoginActivity extends AppCompatActivity implements LoaderCallbacks { 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); } }

Slide 7

Slide 7 text

public class LoginActivity extends AppCompatActivity implements LoaderCallbacks { 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!!

Slide 8

Slide 8 text

Fat Controller is bad because... • Slow iteration • Ambiguous responsibilities • Off-by-one errors • Un-reproducible bugs

Slide 9

Slide 9 text

Unit testing lets you... • Faster execution • Clear division of roles • Chance to improve architecture

Slide 10

Slide 10 text

Is unit testing just enough?

Slide 11

Slide 11 text

No!! It just ensures it partially worked

Slide 12

Slide 12 text

See also... • EspressoςετίʔυͷಉظॲཧΛڀΊΔ • Day1 Room5 15:40 - • UIςετͷ࣮ߦ࣌ؒΛ୹ॖͤ͞Δํ๏ • Day2 Room2 18:30 -

Slide 13

Slide 13 text

anyway

Slide 14

Slide 14 text

JUnit4 and basic assertions

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Assertions

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Installation dependencies { testImplementation 'junit:junit:4.12' testImplementation 'com.squareup.assertj:assertj-android:1.2.0' } assertj-android lets you assert Android framework's code as well

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Create New Test Command + SHIFT + T

Slide 21

Slide 21 text

Create New Test

Slide 22

Slide 22 text

Create New Test

Slide 23

Slide 23 text

Types of test in Android • Local Unit Tests • Runs on JVM • src/test • Instrumented Tests • Runs with Device/Emulators • src/androidTest

Slide 24

Slide 24 text

Local Unit Tests: pros • Runs fast! • No device, emulator, android sdk

Slide 25

Slide 25 text

Local Unit Tests: cons • No REAL Android framework's code

Slide 26

Slide 26 text

• TRUE Android Test • Exactly the same behavior as production Instrumented Tests: pros

Slide 27

Slide 27 text

Instrumented Tests: cons • Runs slow! • Need device, emulator, android sdk

Slide 28

Slide 28 text

This session only cares Local Unit Tests

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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"

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Run tests Run single Run all or Passed!!!!

Slide 44

Slide 44 text

If it fails... reason why it failed failure... Fix and try again!!!

Slide 45

Slide 45 text

Congrats!!!

Slide 46

Slide 46 text

Assertions (AssertJ) • assertThat(all) • isEqualTo / isNotEqualTo • assertThat(String) • isEmpty / isNotEmpty • assertThat(Number) • isPositive / isNegative / isZero / isNotZero • isGreaterThan / isLessThan lots and lots and lots more

Slide 47

Slide 47 text

• assertThat(list) • contains / containsOnly / containsExactly Assertions (AssertJ) lots and lots and lots more List strings = Arrays.asList("foo", "bar", "baz"); assertThat(strings).contains("bar"); same collection same collection and order

Slide 48

Slide 48 text

• http://joel-costigliola.github.io/assertj/ • https://github.com/square/assertj-android • https://qiita.com/opengl-8080/items/ b07307ab0d33422be9c5 AssertJ

Slide 49

Slide 49 text

Exceptions

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Ouch!! @Test public void isValid() throws Exception { String input = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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!!

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Mock and spy with Mockito

Slide 62

Slide 62 text

Mock is... • A WHOLE imitation of a real object • Used to simulate the behavior of depending objects

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Test Doubles They are

Slide 65

Slide 65 text

Why you need mocks?

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

TweetRepository public class TweetRepository { private final LocalTweetDataSource localDataSource; public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List getTimeline() { return localDataSource.getTimeline(); } } depending on local data source

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Data Source public class LocalTweetDataSource { public List getTimeline() { List 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!

Slide 70

Slide 70 text

TweetRepository public class TweetRepository { private final LocalTweetDataSource localDataSource; public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List getTimeline() { return localDataSource.getTimeline(); } } You can replace this with simple mock

Slide 71

Slide 71 text

Mockito

Slide 72

Slide 72 text

Mockito is... • A simple mocking library • Most used one in Java and Android community

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

Stubbing

Slide 75

Slide 75 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );

Slide 76

Slide 76 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );

Slide 77

Slide 77 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );

Slide 78

Slide 78 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) ); mock instance

Slide 79

Slide 79 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn(); stub method

Slide 80

Slide 80 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn();

Slide 81

Slide 81 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn();

Slide 82

Slide 82 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn();

Slide 83

Slide 83 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( );zz

Slide 84

Slide 84 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );zz

Slide 85

Slide 85 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );zz any item you wanna return

Slide 86

Slide 86 text

Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

Spying

Slide 95

Slide 95 text

Spying is... • Stub methods can be added on a REAL object That means spied objects acts EXACTLY the same as real objects unless stubbed

Slide 96

Slide 96 text

Spying List strings = spy(new ArrayList()); 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());

Slide 97

Slide 97 text

Spying List strings = spy(new ArrayList()); 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

Slide 98

Slide 98 text

Spying List strings = spy(new ArrayList()); 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()

Slide 99

Slide 99 text

Spying List strings = spy(new ArrayList()); 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

Slide 100

Slide 100 text

Verify • Check the interactions with mock/spy mocks/spies will remember all interactions

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

Verify List strings = spy(new ArrayList()); 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());

Slide 103

Slide 103 text

Verify List strings = spy(new ArrayList()); 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());

Slide 104

Slide 104 text

Verify List strings = spy(new ArrayList()); 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());

Slide 105

Slide 105 text

Verify List strings = spy(new ArrayList()); 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());

Slide 106

Slide 106 text

Verify List strings = spy(new ArrayList()); 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());

Slide 107

Slide 107 text

Verify List strings = spy(new ArrayList()); 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!

Slide 108

Slide 108 text

Verify List strings = spy(new ArrayList()); 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());

Slide 109

Slide 109 text

Verify List strings = spy(new ArrayList()); 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());

Slide 110

Slide 110 text

Verify List strings = spy(new ArrayList()); 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

Slide 111

Slide 111 text

Verify List strings = spy(new ArrayList()); 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

Slide 112

Slide 112 text

Argument matchers • eq(T) • anyString() • anyInt() • any() • any(Class) • nullable(Class) null or T including null

Slide 113

Slide 113 text

Stubbing with callbacks List strings = spy(new ArrayList()); 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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

Stubbing with callbacks List strings = spy(new ArrayList()); 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");

Slide 116

Slide 116 text

Stubbing with callbacks List strings = spy(new ArrayList()); 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");

Slide 117

Slide 117 text

Stubbing with callbacks List strings = spy(new ArrayList()); 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");

Slide 118

Slide 118 text

Stubbing with callbacks List strings = spy(new ArrayList()); 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

Slide 119

Slide 119 text

Stubbing with callbacks List strings = spy(new ArrayList()); 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

Slide 120

Slide 120 text

Stubbing spy method

Slide 121

Slide 121 text

doReturn List strings = spy(new ArrayList()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println); when(strings.get(3)).thenReturn("foo"); when().thenReturn() is almost always useful

Slide 122

Slide 122 text

doReturn List strings = spy(new ArrayList()); 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

Slide 123

Slide 123 text

doReturn List strings = spy(new ArrayList()); 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");

Slide 124

Slide 124 text

doReturn List strings = spy(new ArrayList()); 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");

Slide 125

Slide 125 text

doReturn List strings = spy(new ArrayList()); 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");

Slide 126

Slide 126 text

doReturn List strings = spy(new ArrayList()); 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");

Slide 127

Slide 127 text

doReturn List strings = spy(new ArrayList()); 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!

Slide 128

Slide 128 text

Stubbing void method

Slide 129

Slide 129 text

doThrow / doNothing List strings = spy(new ArrayList()); doThrow(new IllegalStateException()).when(strings).clear(); strings.clear();

Slide 130

Slide 130 text

doThrow / doNothing List strings = spy(new ArrayList()); doThrow(new IllegalStateException()).when(strings).clear(); strings.clear();

Slide 131

Slide 131 text

doThrow / doNothing List strings = spy(new ArrayList()); doThrow(new IllegalStateException()).when(strings).clear(); strings.clear();

Slide 132

Slide 132 text

doThrow / doNothing List strings = spy(new ArrayList()); doThrow(new IllegalStateException()).when(strings).clear(); strings.clear();

Slide 133

Slide 133 text

doThrow / doNothing List strings = spy(new ArrayList()); doThrow(new IllegalStateException()).when(strings).clear(); strings.clear();

Slide 134

Slide 134 text

doThrow / doNothing List strings = spy(new ArrayList()); doThrow(new IllegalStateException()).when(strings).clear(); strings.clear();

Slide 135

Slide 135 text

doThrow / doNothing List strings = spy(new ArrayList()); doThrow(new IllegalStateException()).when(strings).clear(); doNothing().when(strings).clear(); strings.clear();

Slide 136

Slide 136 text

doThrow / doNothing List strings = spy(new ArrayList()); doThrow(new IllegalStateException()).when(strings).clear(); doNothing().when(strings).clear(); strings.clear();

Slide 137

Slide 137 text

doThrow / doNothing List strings = spy(new ArrayList()); doThrow(new IllegalStateException()).when(strings).clear(); doNothing().when(strings).clear(); strings.clear(); pass!!

Slide 138

Slide 138 text

• http://site.mockito.org/ • http://static.javadoc.io/org.mockito/mockito-core/ 2.13.0/org/mockito/Mockito.html#0 Mockito REALLY HELPFUL!!

Slide 139

Slide 139 text

Robolectric for framework's code

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

InputCheckerTest Basically, Android framework's code cannot be executed on JVM tests

Slide 145

Slide 145 text

app/build.gradle android { testOptions { unitTests.returnDefaultValues = true } } notice: this setting just return the default value @Test isValid() passes successfully

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

Robolectric

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

testImplementation "org.robolectric:robolectric:3.6.1" android { testOptions { unitTests { includeAndroidResources = true } } } Installation remove: unitTests.returnDefaultValues = true

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

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!!

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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!!

Slide 155

Slide 155 text

• 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

Slide 156

Slide 156 text

Unit Testing SQLite

Slide 157

Slide 157 text

• Robolectric simulates SQLite out-of-the-box!! • You can use traditional SQLiteOpenHelper • You can also use the latest Room Persistence Library Unit Testing SQLite

Slide 158

Slide 158 text

Room Persistence Library dependencies { implementation "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "android.arch.persistence.room:compiler:1.0.0" }

Slide 159

Slide 159 text

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

Slide 160

Slide 160 text

Room Persistence Library @Dao public interface UserDao { @Query("SELECT * FROM user") List getAll(); @Query("SELECT * FROM user WHERE uid IN (:userIds)") List 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

Slide 161

Slide 161 text

Room Persistence Library @Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } Database

Slide 162

Slide 162 text

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 getUsers() { return db.userDao().getAll(); } } Business Logic

Slide 163

Slide 163 text

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 getUsers() { return db.userDao().getAll(); } } Business Logic just for demo's convenience

Slide 164

Slide 164 text

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 getUsers() { return db.userDao().getAll(); } } Business Logic delegate

Slide 165

Slide 165 text

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 getUsers() { return db.userDao().getAll(); } } Business Logic delegate

Slide 166

Slide 166 text

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 users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }

Slide 167

Slide 167 text

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 users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }

Slide 168

Slide 168 text

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 users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }

Slide 169

Slide 169 text

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 users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }

Slide 170

Slide 170 text

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 users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }

Slide 171

Slide 171 text

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 users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }

Slide 172

Slide 172 text

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

Slide 173

Slide 173 text

Room Persistence Library • https://developer.android.com/topic/ libraries/architecture/room.html • https://developer.android.com/topic/ libraries/architecture/adding- components.html • https://developer.android.com/training/ data-storage/room/index.html

Slide 174

Slide 174 text

Unit Testing asynchronous code

Slide 175

Slide 175 text

Testing async code • Testing asynchronous code is not always easy I'd introduce the very basic concept of it!

Slide 176

Slide 176 text

Synchronous call interface Fetcher { T fetch(); class StringFetcher implements Fetcher { @Override public String fetch() { return "OK"; } } } Think of a synchronous API call over internet

Slide 177

Slide 177 text

Simple callback Callback interfaces interface OnSuccess { void onSuccess(T result); } interface OnFailure { void onFailure(Exception e); }

Slide 178

Slide 178 text

Asynchronous call class AsyncFetcher { private final Fetcher fetcher; AsyncFetcher(Fetcher fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess 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

Slide 179

Slide 179 text

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

Slide 180

Slide 180 text

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

Slide 181

Slide 181 text

Asynchronous call class AsyncFetcher { private final Fetcher fetcher; AsyncFetcher(Fetcher fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess 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

Slide 182

Slide 182 text

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

Slide 183

Slide 183 text

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

Slide 184

Slide 184 text

Asynchronous Test public class AsyncFetcherTest { private Fetcher fetcher; private AsyncFetcher 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(); } }

Slide 185

Slide 185 text

Asynchronous Test public class AsyncFetcherTest { private Fetcher fetcher; private AsyncFetcher 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(); } }

Slide 186

Slide 186 text

Asynchronous Test public class AsyncFetcherTest { private Fetcher fetcher; private AsyncFetcher 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?

Slide 187

Slide 187 text

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

Slide 188

Slide 188 text

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

Slide 189

Slide 189 text

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

Slide 190

Slide 190 text

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

Slide 191

Slide 191 text

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

Slide 192

Slide 192 text

Asynchronous Test Callback is NOT actually called!!! Execution seems successful...but wait?

Slide 193

Slide 193 text

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

Slide 194

Slide 194 text

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

Slide 195

Slide 195 text

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

Slide 196

Slide 196 text

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

Slide 197

Slide 197 text

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

Slide 198

Slide 198 text

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

Slide 199

Slide 199 text

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

Slide 200

Slide 200 text

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

Slide 201

Slide 201 text

Asynchronous Test Callback is properly called!!! Log message seems correct!!

Slide 202

Slide 202 text

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

Slide 203

Slide 203 text

Best practices and tips

Slide 204

Slide 204 text

Best practices and tips • Use interface at the structural boundary • Use DI pattern for testability • Static is EVIL • Don't be a Mad Tester

Slide 205

Slide 205 text

Use interface at the structural boundary

Slide 206

Slide 206 text

Repository pattern public class TweetRepository { private final LocalTweetDataSource localDataSource; public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List getTimeline() { return localDataSource.getTimeline(); } }

Slide 207

Slide 207 text

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

Slide 208

Slide 208 text

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

Slide 209

Slide 209 text

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

Slide 210

Slide 210 text

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

Slide 211

Slide 211 text

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

Slide 212

Slide 212 text

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

Slide 213

Slide 213 text

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

Slide 214

Slide 214 text

Use DI pattern for testability

Slide 215

Slide 215 text

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

Slide 216

Slide 216 text

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

Slide 217

Slide 217 text

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

Slide 218

Slide 218 text

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

Slide 219

Slide 219 text

Use DI pattern public class TweetRepository { private final LocalTweetDataSource localDataSource; public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List getTimeline() { return localDataSource.getTimeline(); } } Putting dependency from outer

Slide 220

Slide 220 text

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

Slide 221

Slide 221 text

Static is EVIL

Slide 222

Slide 222 text

Static is EVIL because... • Static has global scope • Mockito cannot mock static • Static is practically unnecessary This may cause order dependent bugs

Slide 223

Slide 223 text

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

Slide 224

Slide 224 text

Static is practically unnecessary @Singleton public class TweetRepository { private final LocalTweetDataSource localDataSource; @Inject public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List getTimeline() { return localDataSource.getTimeline(); } } Use Dagger's annotations for handy singleton

Slide 225

Slide 225 text

Don't be a Mad Tester

Slide 226

Slide 226 text

Don't be a Mad Tester • Avoid to test EVERYTHING • Unit tests are NOT perfect • Gain maximum with minimum investment! Enjoy your test life!!!

Slide 227

Slide 227 text

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