A lot of the value from local JVM unit tests comes from the way you design your application. You have to design it in such a way where you can decouple your business logic from your Android Components. Here is an example of such a way using the Model-View-Presenter pattern. Lets practice this out by implementing a basic sign up screen that only takes a username and password. Our Android app is responsible for validating that the username the user supplies is not blank and that the password is at least eight characters long and contains at least one digit. If the username/password is valid we perform our sign up api call, otherwise we display an error message.
Example where business logic is highly coupled with Android Component.
public class LoginActivity extends Activity{
...
private void onSubmitButtonClicked(){
String username = findViewById(R.id.username).getText().toString();
String password = findViewById(R.id.password).getText().toString();
boolean isUsernameValid = username != null && username.trim().length() != 0;
boolean isPasswordValid = password != null && password.trim().length() >= 8 && password.matches(".*\\d+.*");
if(isUsernameValid && isPasswordValid){
performSignUpApiCall(username, password);
} else {
displayInvalidCredentialsErrorMessage();
}
}
}
Example where business logic is decoupled from Android Component.
Here we define in a single class, LoginContract, that will house the various interactions between our various classes.
public interface LoginContract {
public interface View {
performSignUpApiCall(String username, String password);
displayInvalidCredentialsErrorMessage();
}
public interface Presenter {
void validateUserCredentials(String username, String password);
}
}
Our LoginActivity is for the most part the same except that we have removed the responsibility of having to know how to validate a user's sign up form (our business logic). The LoginActivity will now rely on our new LoginPresenter to perform validation.
public class LoginActivity extends Activity implements LoginContract.View{
private LoginContract.Presenter presenter;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
presenter = new LoginPresenter(this);
....
}
...
private void onSubmitButtonClicked(){
String username = findViewById(R.id.username).getText().toString();
String password = findViewById(R.id.password).getText().toString();
presenter.validateUserCredentials(username, password);
}
...
}
Now your business logic will reside in your new LoginPresenter class.
public class LoginPresenter implements LoginContract.Presenter{
private LoginContract.View view;
public LoginPresenter(LoginContract.View view){
this.view = view;
}
public void validateUserCredentials(String username, String password){
boolean isUsernameValid = username != null && username.trim().length() != 0;
boolean isPasswordValid = password != null && password.trim().length() >= 8 && password.matches(".*\\d+.*");
if(isUsernameValid && isPasswordValid){
view.performSignUpApiCall(username, password);
} else {
view.displayInvalidCredentialsErrorMessage();
}
}
}
And now we can create local JVM unit tests against your new LoginPresenter class.
public class LoginPresenterTest {
@Mock
LoginContract.View view;
private LoginPresenter presenter;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
presenter = new LoginPresenter(view);
}
@Test
public void test_validateUserCredentials_userDidNotEnterUsername_displayErrorMessage() throws Exception {
String username = "";
String password = "kingslayer1";
presenter.validateUserCredentials(username, password);
Mockito.verify(view). displayInvalidCredentialsErrorMessage();
}
@Test
public void test_validateUserCredentials_userEnteredFourLettersAndOneDigitPassword_displayErrorMessage() throws Exception {
String username = "Jaime Lanninster";
String password = "king1";
presenter.validateUserCredentials(username, password);
Mockito.verify(view). displayInvalidCredentialsErrorMessage();
}
@Test
public void test_validateUserCredentials_userEnteredNineLettersButNoDigitsPassword_displayErrorMessage() throws Exception {
String username = "Jaime Lanninster";
String password = "kingslayer";
presenter.validateUserCredentials(username, password);
Mockito.verify(view). displayInvalidCredentialsErrorMessage();
}
@Test
public void test_validateUserCredentials_userEnteredNineLettersButOneDigitPassword_performApiCallToSignUpUser() throws Exception {
String username = "Jaime Lanninster";
String password = "kingslayer1";
presenter.validateUserCredentials(username, password);
Mockito.verify(view).performSignUpApiCall(username, password);
}
}
As you can see, when we extracted our business logic out of the LoginActivity and placed it in the LoginPresenter POJO. We can now create local JVM unit tests against our business logic.
It should be noted that there are various other implications from our change in architecture such as we are close to adhering to each class having a single responsibility, additional classes, etc. These are just side effects of the way I choose to go about performing this decoupling via the MVP style. MVP is just one way to go about this but there are other alternatives that you may want to look at such as MVVM. You just have to pick the best system that works for you.