Best Practices and Writing Testable Code
Clone the project from https://github.com/Mobiquity/AndroidUnitTests
Project Wiki: mobiquity.jira.com/wiki/display/MW/Unit+Testing+for+Android
Testing in Android
Why Unit Test?
"But it was working yesterday..."
Bugs are expensive!
Fail Fast and Early
Decoupled Design and "solid" principles
Know your code "Actually" works
What will I learn?
Let's Get Started!
Configuring Your Project
git clone git@github.com:Mobiquity/AndroidUnitTests.git
Clone the Repository
DEMO!
Running the Tests
Unit Tests
./gradlew testDebugUnitTest
Results are under: app/build/reports/tests/{buildFlavor}/index.html
Running the Tests
Functional Tests
./gradlew connectedDebugAndroidTest
Results are under: app/build/reports/androidTests/connected/index.html
Tests require an emulator or a physical device
Running the Tests
Code Coverage
./gradlew testDebugUnitTestCoverage
./gradlew fullTestDebugUnitTestCoverage
Results are under: app/build/reports/jacoco/{coverageType}/html/index.html
Code Walkthrough
Finally...
Unit Tests!
A JUnit Overview
Annotation | |
---|---|
@Test | Informs JUnit that method can be run as a test -Expected: Specifies a test should throw a particular exception -Timeout: Fails test if it has not finished within a limit |
@Before | Test pre-conditions to be run before each test |
@After | Test post-conditions to be run after each test |
@Ignore | Prevent a test from running |
@Rule | Redefine test behavior |
My First Simple Unit Test: Testing Addition
public class AdditionOperator extends Operator {
public AdditionOperator() {
super("+", InputType.OPERATOR);
}
@Override
public double execute(double param1, double param2) {
return add(param1, param2);
}
@Override
public int getPrecedence() {
return Precedence.ADDITION_PRECEDENCE.getValue();
}
@Override
public boolean isLeftAssociative() {
return true;
}
private double add(double firstAddend, double secondAddend) {
return firstAddend + secondAddend;
}
}
What Do We Test?
- Does the operation add the params correctly?
- Correct precedence?
- Correct operator associativity?
public class AdditionOperatorTest {
private AdditionOperator additionOperator;
@Before
public void setUp() {
additionOperator = new AdditionOperator();
}
...
@Test
public void testGetPrecedence_IsEqualToAdditionPrecedence() {
assertThat(additionOperator.getPrecedence())
.isEqualTo(Operator.Precedence.ADDITION_PRECEDENCE.getValue());
}
@Test
public void testShouldBeLeftAssociative() {
assertThat(additionOperator.isLeftAssociative()).isTrue();
}
@Test
public void testExecute_ShouldAddOperands() {
assertThat(additionOperator.execute(2,3)).isWithin(5);
assertThat(additionOperator.execute(0,10)).isWithin(10);
assertThat(additionOperator.execute(10,-10)).isWithin(0);
assertThat(additionOperator.execute(-2,-3)).isWithin(-5);
}
}
What about something more Complex?
@AppScope
public class Calculator {
private InfixInputParser infixInputParser;
...
@Inject
public Calculator(InfixInputParser infixInputParser) {
this.infixInputParser = infixInputParser;
}
public double evaluate(Input[] inputs) throws CalculatorEvaluationException {
try {
Queue<Input> postfixInputs = infixInputParser.toPostfix(inputs);
// Return if there are no inputs to evaluate
if (postfixInputs.isEmpty()) {
return 0;
}
Stack<Double> stack = new Stack<>();
for (Input input : postfixInputs) {
switch (input.getType()) {
case NUMBER:
NumericInput numericInput = (NumericInput) input;
stack.push(numericInput.getValue());
break;
case OPERATOR:
Operator operator = (Operator) input;
double secondOperand = stack.pop();
double firstOperand = stack.pop();
double result = operator.isLeftAssociative() ?
operator.execute(firstOperand, secondOperand) :
operator.execute(secondOperand, firstOperand);
stack.push(result);
break;
}
}
// prevents calculator from returning -0
return stack.pop() + 0.0;
} catch (EmptyStackException | InfixInputParser.InputParserException | NumberFormatException e) {
throw new CalculatorEvaluationException("Invalid Expression");
}
}
}
We need to test that, given postfix input, our calculator returns the correct result.
Testing the Calculator
public Calculator(InfixInputParser infixInputParser) {
...
}
We want to test the behavior of the Calculator class, not our InfixInputParser
So... let's mock the behavior of our parser!
Mock External Dependencies with Mockito
Create Mocks: @Mock or Mockito.mock()
Configure Mocks: Mockito.when(...).thenReturn(...)
Verify Mocks: Mockito.verify(...)
Annotations are used with either MockitoRule or MockitoAnnotations.initMocks()
public class CalculatorTest {
private Calculator calculator;
@Mock InfixInputParser infixInputParser;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
calculator = new Calculator(infixInputParser);
}
...
// Our tests ...
private void mockPostFix(Input[] input, Input[] expectedInput) {
Mockito.when(infixInputParser.toPostfix(input)).thenReturn(
new LinkedList<>(Arrays.asList(expectedInput))
);
}
}
Mocking with Mockito
Our Calculator Test is Separate From our Input Parser Test
What does this mean?
We can verify that the Calculator behaves correctly without needing the real input parser to work correctly!
Testing each unit in isolation
If something
breaks
and our Calculator tests still pass
but our InfixInputParser tests
Fail...
We know where the bug is
Our Calculator Test
@Test
public void testEvaluate_AdditionMultiply() throws Exception {
//Test: 1 + 2 * 3 = 7
Input input[] = new Input[]{
new NumericInput(1),
new AdditionOperator(),
new NumericInput(2),
new MultiplicationOperator(),
new NumericInput(3)
};
double expectedResult = 7;
// Assume the infix input parser works and provide the correct postfix for the input
Input[] postfixInput = new Input[] {
new NumericInput(1),
new NumericInput(2),
new NumericInput(3),
new MultiplicationOperator(),
new AdditionOperator()
};
mockPostFix(input, postfixInput);
double result = calculator.evaluate(input);
assertThat(result).isWithin(expectedResult);
}
Infix Input Parser Test
/**
* Before: 3 + 4 * 5
* After: 3 4 5 * +
*/
@Test
public void testToPostfix_AdditionMultiplication() {
Input[] inputs = new Input[] {
three,
plus,
four,
times,
five
};
Queue<Input> postfixOutput = infixInputParser.toPostfix(inputs);
assertThat(postfixOutput).containsExactly(three, four, five, times, plus)
.inOrder();
}
Testing Networking Logic and Callbacks
Our Wolfram Presenter
@AppScope
public class WolframPresenter extends Presenter<WolframView> {
private WolframService wolframService;
@Inject
public WolframPresenter(WolframService wolframService) {
this.wolframService = wolframService;
}
public void startQuery(String query) {
wolframService.query(query).enqueue(new Callback<WolframResponse>() {
@Override
public void onResponse(Call<WolframResponse> call, Response<WolframResponse> response) {
view().updatePods(response.body().getPods());
}
@Override
public void onFailure(Call<WolframResponse> call, Throwable t) {
Timber.e(t, t.getMessage());
view().showWolframFailure();
}
});
}
}
We want to test the service call without calling the real service
We need to test the callback for both success and failure
What do we need to do?
Mock the Wolfram Service with Mockito
We should also mock our UI (WolframView)
@Mock(answer = RETURNS_DEEP_STUBS) WolframService wolframService;
@Mock WolframView wolframView;
Argument Captor and Mockito
Use it to capture argument values for further assertions
We can use ArgumentCaptor to
control
our callback path
@Captor ArgumentCaptor<Callback<WolframResponse>> responseCaptor;
Testing the Success Case
@Test
public void testStartQuery_SuccessfulQuery() {
wolframPresenter.bind(wolframView);
wolframPresenter.startQuery(Mockito.anyString());
Mockito.verify(wolframService.query(Mockito.anyString()))
.enqueue(responseCaptor.capture());
responseCaptor.getValue().onResponse(
Mockito.mock(Call.class), Response.success(mockResponse)
);
Mockito.verify(wolframView).updatePods(Mockito.anyList());
}
Testing the Failure Case
@Test
public void testStartQuery_FailedQuery() {
wolframPresenter.bind(wolframView);
wolframPresenter.startQuery(Mockito.anyString());
Mockito.verify(wolframService.query(Mockito.anyString()))
.enqueue(responseCaptor.capture());
responseCaptor.getValue().onFailure(
Mockito.mock(Call.class), Mockito.mock(Throwable.class)
);
Mockito.verify(wolframView).showWolframFailure();
}
Unit testing activities
we can mock android dependencies with robolectric!
This allows us to test classes with android dependencies WITHOUT needing an actual device
Setting up Robolectric
testCompile "org.robolectric:robolectric:3.0"
Robolectric only supports <=21 right now
@Config(constants = BuildConfig.class, sdk=21)
@RunWith(RobolectricGradleTestRunner.class)
public class MyRobolectricTest {
...
}
OR use a custom runner
Unit Testing our Calculator Activity
Our CalculatorActivity implements CalculatorView for MVP
Do the methods do what we think?
Set Up the Activity
@RunWith(CustomGradleRunner.class)
public class CalculatorActivityViewTest {
private CalculatorActivity calculatorActivity;
@Before
public void setUp() {
calculatorActivity = Robolectric.setupActivity(CalculatorActivity.class);
}
...
}
@Test
public void testUpdateDisplay_ChangesDisplayText() {
String expectedDisplayText = "1+1";
calculatorActivity.updateDisplayText(expectedDisplayText);
assertThat(calculatorActivity.displayInput.getText().toString())
.isEqualTo(expectedDisplayText);
}
@Test
public void testShowSuccessfulCalculation_ChangesResultText() {
String expectedResultText = "12";
calculatorActivity.showSuccessfulCalculation(expectedResultText);
assertThat(calculatorActivity.resultText.getText().toString())
.isEqualTo(expectedResultText);
}
Testing our CalculatorView methods in isolation
With Robolectric and Mockito, you can mock dependencies and test your components in isolation!
+
Functional Testing
Now That We Have tested the Components in Isolation...
We need to test the behavior of the app as a
whole
What is Espresso?
Provides APIs for writing UI tests to simulate user interactions
As of 2015, it is now officially part of the Android Testing Support Library
Automatic synchronization of test actions with the UI of the app you are testing
Configuration
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
dependencies {
androidTestCompile "com.android.support.test.espresso:espresso-core:2.2.1",
androidTestCompile "com.android.support.test.espresso:espresso-intents:2.2.1"
androidTestCompile ("com.android.support.test.espresso:espresso-contrib:2.2.1") {
exclude module: 'support-annotations'
exclude module: 'gridlayout-v7'
exclude module: 'recyclerview-v7'
exclude module: 'support-v4'
}
}
configurations.all {
resolutionStrategy {
force "com.android.support:support-annotations:23.3.0"
}
}
Espresso recommends disabling animations for tests
// Disable animations for functional tests with espresso
def adb = android.getAdbExe().toString()
afterEvaluate {
task grantAnimationPermission(type: Exec, dependsOn: 'installDebug') {
commandLine "$adb shell pm grant $android.defaultConfig.applicationId android.permission.SET_ANIMATION_SCALE".split(' ')
}
tasks.each { task ->
if (task.name.startsWith('assembleDebugAndroidTest')) {
task.dependsOn grantAnimationPermission
}
}
}
public class DisableAnimationsRule implements TestRule {
private Method mSetAnimationScalesMethod;
private Method mGetAnimationScalesMethod;
private Object mWindowManagerObject;
public DisableAnimationsRule() {
try {
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
mSetAnimationScalesMethod = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
mGetAnimationScalesMethod = windowManagerClazz.getDeclaredMethod("getAnimationScales");
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
mWindowManagerObject = asInterface.invoke(null, windowManagerBinder);
}
catch (Exception e) {
throw new RuntimeException("Failed to access animation methods", e);
}
}
@Override
public Statement apply(final Statement statement, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
setAnimationScaleFactors(0.0f);
try { statement.evaluate(); }
finally { setAnimationScaleFactors(1.0f); }
}
};
}
private void setAnimationScaleFactors(float scaleFactor) throws Exception {
float[] scaleFactors = (float[]) mGetAnimationScalesMethod.invoke(mWindowManagerObject);
Arrays.fill(scaleFactors, scaleFactor);
mSetAnimationScalesMethod.invoke(mWindowManagerObject, scaleFactors);
}
}
Simulating User Input with Espresso
Matchers: Provide a way to find a view instance given criteria
View Action: Perform an action on a view
Assertions: Check if view state matches criteria
https://google.github.io/android-testing-support-library/docs/espresso/cheatsheet/
Android Test Rules
Make use of the JUnit @Rule annotation
@Rule
public ActivityTestRule<CalculatorActivity> activityRule = new ActivityTestRule<>(CalculatorActivity.class);
Calculator Activity Functional Test
@RunWith(AndroidJUnit4.class)
public class CalculatorActivityTest {
@Rule
public ActivityTestRule<CalculatorActivity> activityRule = new ActivityTestRule<>(CalculatorActivity.class);
@Rule
public DisableAnimationsRule disableAnimationsRule = new DisableAnimationsRule();
@Test
public void shouldConcatNumberInput() {
onView(withId(R.id.digit_1)).perform(click());
onView(withId(R.id.add_op)).perform(click());
onView(withId(R.id.digit_1)).perform(click());
onView(withId(R.id.digit_2)).perform(click());
onView(withId(R.id.display_input)).check(matches(withText("1+12")));
}
...
}
Ex: Test that numbers are displayed correctly when the user types on the calculator
That's all, folks!
Android Unit Testing
By Justin Washington
Android Unit Testing
- 265