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