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

The real beginner's guide to Android testing

Avatar for Eric N Eric N
April 07, 2018

The real beginner's guide to Android testing

Easy to follow, contains only what's absolutely necessary to get started. Starts with MVP but expands to MVVM. Follow up Medium article on UI testing also available

Avatar for Eric N

Eric N

April 07, 2018

Other Decks in Technology

Transcript

  1. About myself • Software Engineer at Grab • DragonBall fan

    (Vegeta fan) • @ericntd (LinkedIn, Medium, Github, StackoverFlow, Twitter) • Send me your resume eric.nguyen at grab.com - https://grab.careers/ #droidconvn
  2. Agenda 1. Why write tests 2. Types of tests 3.

    Introduction to unit testing 4. Challenge to unit testing with Android 5. The solution 6. Steps to your first Android unit tests 7. MVP architecture limitations 8. Q & A @ericntd
  3. 1) Why write tests? • Improve and maintain product quality

    with less QA’s manual effort • Increase confidence when shipping • Perform routine exhaustive checks humans can’t perform, fast • Help you write more modular code @ericntd
  4. 2) Different of type of tests Unit tests Component/ Integration

    Tests End-to- end tests 20-30 mins to run 100++ component UI tests <10 mins to run 4000 tests Fast to run, easy to write, run and to measure coverage Slow to run, difficult to write, run and to measure coverage #droidconvn @ericntd
  5. 3) Unit testing crash course public class Calculator { /**

    * @param input an integer in the range of [Integer.MIN_VALUE/2, Integer.MAX_VALUE/2] * @return -1 if the input is too big or too small, otherwise the input times 2 */ public int timesTwo(int input) { if (input > Integer.MAX_VALUE / 2 || input < Integer.MIN_VALUE / 2) { return -1; } return input * 2; } } • • @ericntd
  6. 3) Unit testing crash course dependencies { //... testImplementation 'junit:junit:4.12'

    testImplementation "org.mockito:mockito-core:2.12.0" } app/build.gradle #droidconvn public class CalculatorTest { private Calculator calculator = new Calculator(); @Test public void plusTwo() { Assert.assertEquals(0, calculator.timesTwo(0)); Assert.assertEquals(2, calculator.timesTwo(1)); } } app/src/test/java/your.package.name @ericntd
  7. 3) Unit testing crash course public class CalculatorTest { private

    Calculator calculator = new Calculator(); @Test public void plusTwo() { Assert.assertEquals(0, calculator.timesTwo(0)); Assert.assertEquals(2, calculator.timesTwo(1)); Assert.assertEquals(-2, calculator.timesTwo(-1)); Assert.assertEquals(-1, calculator.timesTwo(Integer.MAX_VALUE)); Assert.assertEquals(-1, calculator.timesTwo(Integer.MIN_VALUE)); Assert.assertEquals(2147483646, calculator.timesTwo(Integer.MAX_VALUE / 2)); Assert.assertEquals(-1, calculator.timesTwo(Integer.MAX_VALUE / 2 + 1)); Assert.assertEquals(-2147483648, calculator.timesTwo(Integer.MIN_VALUE / 2)); Assert.assertEquals(-1, calculator.timesTwo(Integer.MIN_VALUE / 2 - 1)); } } @ericntd
  8. 3) Unit testing crash course Unit tests also force us

    to write modular code The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. Functions should not be 100 lines long. Functions should hardly ever be 20 lines long.” - Clean Code @ericntd
  9. 4) Challenge of unit testing in Android • Consider a

    simple activity: ◦ A TextView display a number, starting from 1 ◦ A Button named “Time Two” • @ericntd
  10. 4) Challenge of unit testing in Android public class CalculatorActivity

    extends AppCompatActivity { ` public Calculator calculator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_calculator); calculator = new Calculator(); final TextView tvNumber = findViewById(R.id.tv_number); Button ctaTimesTwo = findViewById(R.id.cta_times_two); ctaTimesTwo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { updateNumber(tvNumber); } }); } public void updateNumber(TextView tvNumber) { tvNumber.setText(calculator.timesTwo(Integer.valueOf(tvNumber.getText().toString()))); } } @ericntd
  11. 4) Challenge of unit testing in Android public class CalculatorActivity

    extends AppCompatActivity { public Calculator calculator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_calculator); calculator = new Calculator(); final TextView tvNumber = findViewById(R.id.tv_number); Button ctaTimesTwo = findViewById(R.id.cta_times_two); ctaTimesTwo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { updateNumber(tvNumber); } }); } public void updateNumber(TextView tvNumber) { tvNumber.setText(calculator.timesTwo(Integer.valueOf(tvNumber.getText().toString()))); } } @ericntd
  12. 4) Challenge of unit testing in Android public class CalculatorActivityTest

    { private CalculatorActivity activity = new CalculatorActivity(); @Test public void updateNumber() { // Preparation TextView tvNumber = Mockito.mock(TextView.class); Mockito.doReturn("1").when(tvNumber).getText(); // Trigger activity.updateNumber(tvNumber); // Validation Assert.assertEquals("2", tvNumber.getText().toString()); Mockito.verify(tvNumber).setText(2); } } • • @ericntd
  13. 4) Challenge of unit testing in Android We run CalculatorActivityTest

    and we get a NullPointerException. @ericntd
  14. 5) Solution for unit testing in Android • We need

    to move our business logics outside of the Activity or Fragment • We need to refactor our app from MVC to a better architecture such as Model-View-Presenter (MVP) @ericntd
  15. 5) Solution for unit testing in Android - Architectures #droidconvn

    • Difficult to test logics within Activity/ Fragment • Activity/ Fragment are bloated Model-View-Presenter View (Fragment/ Activity) Presenter Activity/ Fragment Model-View-Controller Controller View • Business logics easily tested • Activity/ Fragment much thinner without business logics @ericntd
  16. 6) Steps to your first Android unit tests A. Refactor

    your app to MVP B. Write tests for your Presenter (contains business logics) C. Profit! #droidconvn @ericntd
  17. A) MVC => MVP + #droidconvn public class MainActivity extends

    AppCompatActivity { //... private void searchGitHubRepos(GitHubApi gitHubApi, String query) { gitHubApi.searchRepos(query).enqueue(new Callback<SearchResponse>() { @Override public void onResponse(Call<SearchResponse> call, Response<SearchResponse> response) { handleResponse(response); } @Override public void onFailure(Call<SearchResponse> call, Throwable t) { handleError("E103 - System error"); } }); } } • • @ericntd
  18. A) MVC => MVP public class SearchPresenter implements SearchPresenterContract, GitHubRepository

    .GitHubRepositoryCallback { private final SearchViewContract viewContract; private final GitHubRepository repository; //... @Override public void searchGitHubRepos(@Nullable final String query) { if (query != null && query.length() > 0) { repository.searchRepos(query, this); } } } • • @ericntd
  19. A) MVC => MVP public class MainActivity extends AppCompatActivity implements

    SearchViewContract { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... } @Override public void displaySearchResults(@NonNull List<SearchResult> searchResults, @Nullable Integer totalCount) { rvAdapter.updateResults(searchResults); tvStatus.setText(String.format(Locale.US, "Number of results: %d", totalCount)); } @Override public void displayError() { Toast.makeText(this, "some error happened", Toast.LENGTH_SHORT).show(); } @Override public void displayError(String s) { Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); } } @ericntd
  20. B) Write tests for Presenter - Setup package tech.ericntd.githubsearch.search; public

    class SearchPresenterTest { private SearchPresenter presenter; @Mock private GitHubRepository repository; @Mock private SearchViewContract viewContract; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this);// required for the "@Mock" annotations // Make presenter a mock while using mock repository and viewContract created above presenter = Mockito.spy(new SearchPresenter(viewContract, repository)); } // The tests } dependencies { //... testImplementation 'junit:junit:4.12' testImplementation "org.mockito:mockito-core:2.12.0" } app/build.gradle app/src/test/java/ • #droidconvn @ericntd
  21. B) Write test for Presenter - Example package tech.ericntd.githubsearch.search; public

    class SearchPresenterTest { @Test public void searchGitHubRepos() { String searchQuery = "some query"; // Trigger presenter.searchGitHubRepos(searchQuery); // Validation Mockito.verify(repository, Mockito.times(1)).searchRepos(searchQuery, presenter); } } package tech.ericntd.githubsearch.search; public class SearchPresenter implements SearchPresenterContract, GitHubRepository.GitHubRepositoryCallback { @Override public void searchGitHubRepos(@Nullable final String query) { if (query != null && query.length() > 0) { repository.searchRepos(query, this); } } } #droidconvn @ericntd
  22. B) Write test for Presenter - Example package tech.ericntd.githubsearch.search; public

    class SearchPresenterTest { @Test public void searchGitHubRepos() { String searchQuery = "some query"; // Trigger presenter.searchGitHubRepos(searchQuery); // Validation Mockito.verify(repository, Mockito.times(1)).searchRepos(searchQuery, presenter); } } package tech.ericntd.githubsearch.search; public class SearchPresenter implements SearchPresenterContract, GitHubRepository.GitHubRepositoryCallback { @Override public void searchGitHubRepos(@Nullable final String query) { if (query != null && query.length() > 0) { repository.searchRepos(query, this); } } } #droidconvn @ericntd
  23. B) Write test for Presenter - Example package tech.ericntd.githubsearch.search; public

    class SearchPresenterTest { @Test public void searchGitHubRepos() { String searchQuery = "some query"; // Trigger presenter.searchGitHubRepos(searchQuery); // Validation Mockito.verify(repository, Mockito.times(1)).searchRepos(searchQuery, presenter); } } package tech.ericntd.githubsearch.search; public class SearchPresenter implements SearchPresenterContract, GitHubRepository.GitHubRepositoryCallback { @Override public void searchGitHubRepos(@Nullable final String query) { if (query != null && query.length() > 0) { repository.searchRepos(query, this); } } } #droidconvn @ericntd
  24. B) Write test for Presenter - Example package tech.ericntd.githubsearch.search; public

    class SearchPresenterTest { @Test public void searchGitHubRepos() { String searchQuery = "some query"; // Trigger presenter.searchGitHubRepos(searchQuery); // Validation Mockito.verify(repository, Mockito.times(1)).searchRepos(searchQuery, presenter); } } package tech.ericntd.githubsearch.search; public class SearchPresenter implements SearchPresenterContract, GitHubRepository.GitHubRepositoryCallback { @Override public void searchGitHubRepos(@Nullable final String query) { if (query != null && query.length() > 0) { repository.searchRepos(query, this); } } } app/src/test/java/ app/src/main/java/ #droidconvn @ericntd
  25. C) Run test & Profit! package tech.ericntd.githubsearch.search; public class SearchPresenterTest

    { @Test public void searchGitHubRepos_noQuery() { String searchQuery = null; // Trigger presenter.searchGitHubRepos(searchQuery); // Validation Mockito.verify(repository, Mockito.never()).searchRepos(searchQuery, presenter); } } package tech.ericntd.githubsearch.search; public class SearchPresenter implements SearchPresenterContract, GitHubRepository.GitHubRepositoryCallback { @Override public void searchGitHubRepos(@Nullable final String query) { if (query != null && query.length() > 0) { repository.searchRepos(query, this); } } } @ericntd
  26. C) Run test & Profit! @Test public void handleGitHubResponse_Failure() {

    Response response = Mockito.mock(Response.class); Mockito.doReturn(false).when(response).isSuccessful(); // Trigger presenter.handleGitHubResponse(response); // Validation Mockito.verify(viewContract, Mockito.times(1)).displayError("E101 - System error"); } @ericntd @Test public void handleGitHubResponse_EmptyResponse() { Response response = Mockito.mock(Response.class); Mockito.doReturn(true).when(response).isSuccessful(); Mockito.doReturn(null).when(response).body(); // Trigger presenter.handleGitHubResponse(response); // Validation Mockito.verify(viewContract, Mockito.times(1)).displayError("E102 - System error"); }
  27. 7) MVP architecture limitations public class MainActivity extends AppCompatActivity implements

    SearchViewContract { // ... @Override public void displaySearchResults(@NonNull List<SearchResult> searchResults, @Nullable Integer totalCount) { rvAdapter.updateResults(searchResults); tvStatus.setText(String.format(Locale.US, "Number of results: %d", totalCount)); } } #droidconvn @ericntd
  28. 7) MVP architecture limitations // no more logic here MainActivity

    <?xml version="1.0" encoding="utf-8"?> <layout > <data> <variable name="vm" type="tech.ericntd.githubsearch.search.SearchViewModel" /> </data> <android.support.constraint.ConstraintLayout> <TextView android:id="@+id/tv_status" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingEnd="16dp" android:paddingStart="16dp" android:text="@{vm.status}" app:layout_constraintTop_toBottomOf="@id/et_search_query" tools:text="Number of results: 1000000" /> </android.support.constraint.ConstraintLayout> </layout> activity_main.xml @ericntd
  29. 7) MVP architecture limitations public class SearchViewModel implements GitHubRepository .GitHubRepositoryCallback

    { public ObservableField<String> status = new ObservableField<>(); @Override public void handleGitHubResponse(@NonNull final Response<SearchResponse> response) { if (response.isSuccessful()) { SearchResponse searchResponse = response.body(); if (searchResponse != null && searchResponse.getSearchResults() != null) { renderSuccess(searchResponse); } } } private void renderSuccess(SearchResponse searchResponse) { status.set(String.format(Locale.US, "Number of results: %d", searchResponse .getTotalCount())); } } SearchViewModel public class SearchViewModelTest { @Test public void renderSuccess() { Response response = Mockito.mock(Response.class); SearchResponse searchResponse = Mockito.mock(SearchResponse.class); Mockito.doReturn(true).when(response).isSuccessful(); Mockito.doReturn(searchResponse).when(response).body(); Mockito.doReturn(1001).when(searchResponse).getTotalCount(); // Trigger viewModel.handleGitHubResponse(response); // Validation Assert.assertEquals("Number of results: 1001", viewModel.status.get()); } } SearchViewModelTest @ericntd
  30. 7) MVP architecture limitations • Notes on MVVM ◦ Your

    XML layout is not tested, so avoid complex logic #droidconvn @ericntd
  31. Android testing articles series Part 1: A beginner’s guide to

    automated (unit) testing (today’s talk) Part 2: Better tests with MVVM + data binding Part 3: Component UI tests with Espresso from 0 to 1 #droidconvn @ericntd
  32. Thank you @ericntd • @ericntd (LinkedIn, Medium, Github, StackoverFlow, Twitter)

    • Send me your resume eric.nguyen at grab.com - https://grab.careers/