• 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
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
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
lots and lots and lots more List<String> strings = Arrays.asList("foo", "bar", "baz"); assertThat(strings).contains("bar"); same collection same collection and order
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
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; } }
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!!
TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } depending on local data source
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!
TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } You can replace this with simple mock
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
= {"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
(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; } }
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
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
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
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
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?
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
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
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 */); } } }
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 */); } } }
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
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
public void setUp() throws Exception { LocalTweetDataSource localDataSource = new LocalTweetDataSource.MockTweetDataSource(); tweetRepository = new TweetRepository(localDataSource); } } You can even get rid of Mockito dependency
localDataSource; public TweetRepository() { this.localDataSource = new LocalTweetDataSource.SQLiteTweetDataSource(); } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } mocking field is painful!!