The real beginner’s guide to
Android testing
Eric Nguyen
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
My journey with unit testing
Before unit tests With unit tests
#droidconvn
@ericntd
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
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
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
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;
}
}
● We have a Calculator class with a
single timesTwo method
● The timesTwo method returns the
input times 2, except for in the
case of overflow, it returns -1
@ericntd
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
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
3) Unit testing crash course
● Our test passes!
@ericntd
3) Unit testing crash course
● Unit tests help our app’s stability
@ericntd
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
4) Challenge of unit testing in Android
● Consider a simple activity:
○ A TextView display a number, starting from 1
○ A Button named “Time Two”
● The doubling logic is from our Calculator’s
timesTwo
@ericntd
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())));
}
}
CalculatorActivity is the
Controller in a Model-View-
Controller
@ericntd
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())));
}
}
Doubling logic is based on
Calculator’s timesTwo
@ericntd
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);
}
}
● Prepare any dependency incl.
Android-specific easily
● Verify a method is called on a
mocked object
@ericntd
4) Challenge of unit testing in Android
We run CalculatorActivityTest and we
get a NullPointerException.
Explanation:
The Calculator object was instantiated inside the
Activity’s onCreate method.
In our JUnit test, we have no control over the
CalculatorActivity’s onCreate method
Conclusion:
We are unable to unit test our business logics
inside an Android Activity
@ericntd
4) Challenge of unit testing in Android
Possible Workarounds:
● Instantiate a Calculator object inside
updateNumber method itself
○ Inefficiency - multiple
Calculator objects instead of
reusing one
● Create a setter in the the activity:
setCalculator(Caculator)
○ Side effects e.g. race
condition, unexpected
behaviours
@ericntd
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
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
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
Sample app: GitHub Search
https://github.com/ericntd/Github-Search
@ericntd
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");
}
});
}
}
Original MainActivity in a MVC
settings. 2 business logics
methods:
● searchGitHubRepos
● handleSearchGitHubResp
onse
We will move these 2 methods
into the Presenter class
@ericntd
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);
}
}
}
The Presenter now contains
the 2 business logics methods:
● searchGitHubRepos
● handleSearchGitHubResp
onse
@ericntd
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();
}
Our new Activity is much
cleaner with only ~50 lines of
code
@ericntd
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/
● Pure JUnit test
#droidconvn
@ericntd
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
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
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
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
C) Run test & Profit!
#droidconvn
@ericntd
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
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");
}
C) Run test & Profit!
@ericntd
@ericntd
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));
}
}
It looks like logic, but
why is it in the View/
Activity?
#droidconvn
@ericntd
7) MVP architecture limitations
● Real 70-line-function in
production code
● 4 functions like this in the same
Activity
● 200+ lines of “setText” and
“setVisibility” and co.
● Hundreds of lines of code
required in test class
● Dozens of Mockito mocks
required in test class ⇒
significantly higher test run time
#droidconvn
@ericntd
7) MVP architecture limitations
Introducing Model-View-ViewModel
Model-View-ViewModel
View (.xml layout)
ViewModel
Data
Binding
@ericntd
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
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
7) MVP architecture limitations
● Notes on MVVM
○ Your XML layout is not tested, so avoid complex logic
#droidconvn
@ericntd
Takeaways
Architecture your app for testing
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
References
https://github.com/googlesamples/android-architecture
https://codelabs.developers.google.com/codelabs/android-testing/#0
#droidconvn
@ericntd
Q & A
#droidconvn
@ericntd
Thank you
@ericntd
● @ericntd (LinkedIn, Medium, Github, StackoverFlow, Twitter)
● Send me your resume eric.nguyen at grab.com -
https://grab.careers/

The real beginner's guide to android testing

  • 1.
    The real beginner’sguide to Android testing Eric Nguyen
  • 2.
    About myself ● SoftwareEngineer 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
  • 3.
    My journey withunit testing Before unit tests With unit tests #droidconvn @ericntd
  • 4.
    Agenda 1. Why writetests 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
  • 5.
    1) Why writetests? ● 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
  • 6.
    2) Different oftype 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
  • 7.
    3) Unit testingcrash 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; } } ● We have a Calculator class with a single timesTwo method ● The timesTwo method returns the input times 2, except for in the case of overflow, it returns -1 @ericntd
  • 8.
    3) Unit testingcrash 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
  • 9.
    3) Unit testingcrash 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
  • 10.
    3) Unit testingcrash course ● Our test passes! @ericntd
  • 11.
    3) Unit testingcrash course ● Unit tests help our app’s stability @ericntd
  • 12.
    3) Unit testingcrash 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
  • 13.
    4) Challenge ofunit testing in Android ● Consider a simple activity: ○ A TextView display a number, starting from 1 ○ A Button named “Time Two” ● The doubling logic is from our Calculator’s timesTwo @ericntd
  • 14.
    4) Challenge ofunit 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()))); } } CalculatorActivity is the Controller in a Model-View- Controller @ericntd
  • 15.
    4) Challenge ofunit 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()))); } } Doubling logic is based on Calculator’s timesTwo @ericntd
  • 16.
    4) Challenge ofunit 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); } } ● Prepare any dependency incl. Android-specific easily ● Verify a method is called on a mocked object @ericntd
  • 17.
    4) Challenge ofunit testing in Android We run CalculatorActivityTest and we get a NullPointerException. Explanation: The Calculator object was instantiated inside the Activity’s onCreate method. In our JUnit test, we have no control over the CalculatorActivity’s onCreate method Conclusion: We are unable to unit test our business logics inside an Android Activity @ericntd
  • 18.
    4) Challenge ofunit testing in Android Possible Workarounds: ● Instantiate a Calculator object inside updateNumber method itself ○ Inefficiency - multiple Calculator objects instead of reusing one ● Create a setter in the the activity: setCalculator(Caculator) ○ Side effects e.g. race condition, unexpected behaviours @ericntd
  • 19.
    5) Solution forunit 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
  • 20.
    5) Solution forunit 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
  • 21.
    6) Steps toyour first Android unit tests A. Refactor your app to MVP B. Write tests for your Presenter (contains business logics) C. Profit! #droidconvn @ericntd
  • 22.
    Sample app: GitHubSearch https://github.com/ericntd/Github-Search @ericntd
  • 23.
    A)MVC => MVP + #droidconvn publicclass 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"); } }); } } Original MainActivity in a MVC settings. 2 business logics methods: ● searchGitHubRepos ● handleSearchGitHubResp onse We will move these 2 methods into the Presenter class @ericntd
  • 24.
    A)MVC => MVP publicclass 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); } } } The Presenter now contains the 2 business logics methods: ● searchGitHubRepos ● handleSearchGitHubResp onse @ericntd
  • 25.
    A)MVC => MVP publicclass 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(); } Our new Activity is much cleaner with only ~50 lines of code @ericntd
  • 26.
    B) Write testsfor 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/ ● Pure JUnit test #droidconvn @ericntd
  • 27.
    B) Write testfor 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
  • 28.
    B) Write testfor 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
  • 29.
    B) Write testfor 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
  • 30.
    B) Write testfor 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
  • 31.
    C) Run test& Profit! #droidconvn @ericntd
  • 32.
    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
  • 33.
    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"); }
  • 34.
    C) Run test& Profit! @ericntd
  • 35.
  • 36.
    7) MVP architecturelimitations 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)); } } It looks like logic, but why is it in the View/ Activity? #droidconvn @ericntd
  • 37.
    7) MVP architecturelimitations ● Real 70-line-function in production code ● 4 functions like this in the same Activity ● 200+ lines of “setText” and “setVisibility” and co. ● Hundreds of lines of code required in test class ● Dozens of Mockito mocks required in test class ⇒ significantly higher test run time #droidconvn @ericntd
  • 38.
    7) MVP architecturelimitations Introducing Model-View-ViewModel Model-View-ViewModel View (.xml layout) ViewModel Data Binding @ericntd
  • 39.
    7) MVP architecturelimitations // 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
  • 40.
    7) MVP architecturelimitations 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
  • 41.
    7) MVP architecturelimitations ● Notes on MVVM ○ Your XML layout is not tested, so avoid complex logic #droidconvn @ericntd
  • 42.
  • 43.
    Android testing articlesseries 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
  • 44.
  • 45.
  • 46.
    Thank you @ericntd ● @ericntd(LinkedIn, Medium, Github, StackoverFlow, Twitter) ● Send me your resume eric.nguyen at grab.com - https://grab.careers/

Editor's Notes

  • #5 Ask me a question at the end for 70k GrabPay credit They say money can’t buy happiness, I’m pretty sure it can
  • #6 E.g. currency formatter
  • #8 Language being used here is Java
  • #9 Setting up JUnit dependency is easy with Android Studio and Gradle Your production code is under app/src/main/java/ folder. Whereas, your test code is under app/src/test/java folder. Once you create your test class there and write at least 1 test method (will show you in 1 second), Android Studio gives you a nice little green Play icon to run your test. To mark a method as a test method, simply annate it with @Test
  • #10 The inputs we use are 0, 1, -1 for example. Also, we need a big positive and a big negative numbers to test the overflow scenarios. For each input, we use the assertEquals method from JUnit’s Assert class to check whether our function’s actual outputs matches expected values.
  • #11 It gives us the confidence that our code works as expected It’s ok to get failing tests It’s half of the fun
  • #12 Say some teammate or yourself dislikes -1 as the output in overflow case and decides to change it to 1 later Our app will crash or misbehave We should instead run our unit tests regularly, every git push for a pull request We can catch the bug and fix it early
  • #13 I’m certain you will find some huge functions in your code. Try writing unit test for them, you will see that I mean. Can’t show my code due to confidentiality issue. It wouldn’t make much sense If I censored everything or showed you some dummy code.
  • #15 Let’s naively do everything in the Activity A hint here, architecture is the key to Android unit testing here, will expand later
  • #17 Similar to how we did CalculatorTest, we will create a class CalculatorActivityTest under src/test/java folder We will naively try to test the updateNumber() method with the help of Mockito What is Mockito? Mockito is a popular Java library to assist unit testing One could always use Robolectric to stub the TextView here However, it’s slower and may have side effects As much as they try, Robolectric’s stubs are not the same s Android’s objects Use Robolectric sparingly
  • #20 MVP is simplest to start with
  • #21 There are MVVM, Clean architecture and RIB in the market, check them out when you have time MVP is the simplest architecture that separate business logics from Activity/ Fragment We will improve our architecture later
  • #22 As said, we’ll start with MVP, the simplest architecture ha
  • #23 This app albeit simple, it fetches data from GitHub API similar to many of your actual apps This should relates to most of your apps right, communicating with a REST API on your servers or third party servers
  • #24 Repository class in Data Layer fetches data from GitHub API using Retrofit searchGitHubRepos calls Repository class to fetch data Repository class a callback handleSearchGitHubResponse handles the data returned through that callback
  • #27 Not Robolectric
  • #28 Let’s write tests Create a SearchPresenterTest class First, let’s test searchGitHubRepos method in a happy scenario i.e. a non-null non empty query is passed to it
  • #29 Switch side is confusing Reconsider
  • #32 This time, we run test with coverage This is how you quantify your work with unit testing We’ll start today at 0 We’ll soon be at 50 Let’s strive for 80 coverage, you know the 80/20 principle? It’s important to test the unhappy paths and error handling Let’s add those tests
  • #33  Don’t just test the happy path, it’s important to test the edge cases and error cases two The unhappy scenario here is when search query is null Here we use Mockito.never() to verify that repository.searchRepos is never called
  • #34 We’ll also test method handleGitHubReponse’s error handling
  • #35 Run the tests with coverage again This time, we 100% test coverage!
  • #37 To link to next slide
  • #41 Most variables are reusable, so the 200 lines of setText and setVisibility in my production code will become like 10 lines of code in SearchViewModel
  • #42 Consider removing this
  • #44 To put at the end