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

TDD For Android -- DroidCon Torino 2015

TDD For Android -- DroidCon Torino 2015

Delivering updates with confidence; shortening time to market; writing clean and correct code every day: this is the promise of Test-Driven Development. But, it’s not easy to do TDD in Android. You have to either run the tests on the device, or install a complex framework that mimics the Android APIs. Both options slow you down.

In this session we’ll get back to the roots of TDD and show how to deal with this problem. We’ll learn time-tested techniques that greatly reduce the need to run tests on the device. The good side-effect of these techniques is that our code becomes simpler and better. We’ll learn about old workhorses such as presenter-first, model-view separation, humble dialog box and spikes.

I will present an example of how to test-drive a drawing application that supports multi-touch.

Warning: this is not a session about testing! It’s about writing well-written, well-designed code. Testing is important, but it’s not our focus here; we use test to drive the design.

Matteo Vaccari

April 15, 2015
Tweet

More Decks by Matteo Vaccari

Other Decks in Technology

Transcript

  1. TDD for ANDROID
    Matteo Vaccari
    !
    [email protected]
    @xpmatteo

    View full-size slide

  2. Deliver software faster

    View full-size slide

  3. Deliver valuable
    software faster

    View full-size slide

  4. Deliver valuable
    software faster,
    sustainably

    View full-size slide

  5. WHY TEST?
    • Tests help me design my code
    • Tests check that my code works
    the developer perspective
    the tester perspective

    View full-size slide

  6. Some tests are less
    useful than others…

    View full-size slide

  7. Bureaucratic tests
    1. Think a line of production code
    2. Write a test that proves that the line of code exists
    3. Write the line of code
    4. .

    View full-size slide

  8. Useful tests
    1. Think a behaviour that I would like to have
    2. Write a test that proves that the behaviour exists
    3. Experiment ways to pass the test
    4. $$$!………… Value !!!

    View full-size slide

  9. What is TDD?

    View full-size slide

  10. From Growing Object-Oriented Software by Nat Pryce and Steve Freeman

    View full-size slide

  11. Why is it difficult to
    TDD on Android?

    View full-size slide

  12. The TDD cycle should be
    fast!

    View full-size slide

  13. Value adding ?
    Or waste?

    View full-size slide

  14. fixing Gradle builds
    is waste
    and is not fun
    How do you tell if an activity is value adding or waste?
    Imagine doing that activity all the time… how does it feel?
    What would the customer say if you do it all the time?

    View full-size slide

  15. Programming
    Skill
    > Fancy
    Libraries

    View full-size slide

  16. Model-View
    Separation

    View full-size slide

  17. Model-View Separation

    View full-size slide

  18. Model-View Separation
    App!
    (Android)
    Core!
    (Pure Java)
    Acceptance!
    Test
    Unit!
    Test

    View full-size slide

  19. From Growing Object-Oriented Software by Nat Pryce and Steve Freeman

    View full-size slide

  20. Unit Doctor Acceptance Tests
    public void testInchesToCentimeters() throws Throwab
    givenTheUserSelectedConversion("in", "cm");
    whenTheUserEnters("2");
    thenTheResultIs("2.00 in = 5.08 cm");
    }
    !
    public void testFahrenheitToCelsius() throws Throwab
    givenTheUserSelectedConversion("F", "C");
    whenTheUserEnters("50");
    thenTheResultIs("50.00 F = 10.00 C");
    }
    !
    public void testUnknownUnits() throws Throwable {
    givenTheUserSelectedConversion("ABC", "XYZ");
    thenTheResultIs("I don't know how to convert this"
    }

    View full-size slide

  21. public class UnitConversionAcceptanceTest extends ActivityInstrumentationTestCase2public UnitConversionAcceptanceTest() {
    super(MainActivity.class);
    }
    !
    public void testInchesToCentimeters() throws Throwable {
    givenTheUserSelectedConversion("in", "cm");
    whenTheUserEnters("2");
    thenTheResultIs("2.00 in = 5.08 cm");
    }
    !
    private void givenTheUserSelectedConversion(String fromUnit, String toUnit) throws
    setText(R.id.fromUnit, fromUnit);
    setText(R.id.toUnit, toUnit);
    }
    !
    private void whenTheUserEnters(String inputNumber) throws Throwable {
    setText(R.id.inputNumber, inputNumber);
    }
    !
    private void thenTheResultIs(String expectedResult) {
    assertEquals(expectedResult, getField(R.id.result).getText());
    }
    !
    private void setText(final int id, final String text) throws Throwable {
    final TextView field = getField(id);
    runTestOnUiThread(new Runnable() {

    View full-size slide

  22. }
    !
    private void givenTheUserSelectedConversion(String fromUnit, String toUnit) throws
    setText(R.id.fromUnit, fromUnit);
    setText(R.id.toUnit, toUnit);
    }
    !
    private void whenTheUserEnters(String inputNumber) throws Throwable {
    setText(R.id.inputNumber, inputNumber);
    }
    !
    private void thenTheResultIs(String expectedResult) {
    assertEquals(expectedResult, getField(R.id.result).getText());
    }
    !
    private void setText(final int id, final String text) throws Throwable {
    final TextView field = getField(id);
    runTestOnUiThread(new Runnable() {
    @Override
    public void run() {
    field.setText(text);
    }
    });
    }
    !
    private TextView getField(int id) {
    return (TextView) getActivity().findViewById(id);
    }
    }

    View full-size slide

  23. The simplest thing
    public class MyActivity extends Activity implements View.OnKeyListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my);
    !
    getEditText(R.id.inputNumber).setOnKeyListener(this);
    getEditText(R.id.fromUnit).setOnKeyListener(this);
    getEditText(R.id.toUnit).setOnKeyListener(this);
    }
    !
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
    String input = getEditText(R.id.inputNumber).getText();
    String fromUnit = getEditText(R.id.fromUnit).getText();
    String toUnit = getEditText(R.id.toUnit).getText();
    getEditText(R.id.result).setText(" Hello! " + input + fromUnit + toUnit);
    return false;
    }
    !
    private EditText getEditText(int id) {
    return (EditText) findViewById(id);
    }

    View full-size slide

  24. The simplest thing
    public class MyActivity extends Activity implements View.OnKeyListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my);
    !
    getEditText(R.id.inputNumber).setOnKeyListener(this);
    getEditText(R.id.fromUnit).setOnKeyListener(this);
    getEditText(R.id.toUnit).setOnKeyListener(this);
    }
    !
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
    String input = getEditText(R.id.inputNumber).getText();
    String fromUnit = getEditText(R.id.fromUnit).getText();
    String toUnit = getEditText(R.id.toUnit).getText();
    getEditText(R.id.result).setText(new UnitDoctor().convert(input, fromUnit, toUnit));
    return false;
    }
    !
    private EditText getEditText(int id) {
    return (EditText) findViewById(id);
    }

    View full-size slide

  25. Very easy to test
    public class UnitDoctorTest {
    !
    @Test
    public void convertsInchesToCentimeters() {
    UnitDoctor unitDoctor = new UnitDoctor();
    assertEquals("2.54", unitDoctor.convert("1", "in", "cm"));
    }
    !
    }

    View full-size slide

  26. Yes yes… but my app
    interacts heavily with
    Android widgets!

    View full-size slide

  27. Presenter-First
    public class UnitDoctorTest {
    @Rule public JUnitRuleMockery context = new JUnitRuleMockery()
    !
    UnitDoctorView view = context.mock(UnitDoctorView.class);
    UnitDoctor unitDoctor = new UnitDoctor(view);
    !
    @Test
    public void convertInchesToCm() throws Exception {
    context.checking(new Expectations() {{
    allowing(view).inputNumber(); will(returnValue(1.0));
    allowing(view).fromUnit(); will(returnValue("in"));
    allowing(view).toUnit(); will(returnValue("cm"));
    oneOf(view).showResult(2.54);
    }});
    !
    unitDoctor.convert();
    }
    !

    View full-size slide

  28. What are mocks good for?
    Mocking Android APIs?
    Designing interfaces

    View full-size slide

  29. Discover the “view” interface
    public interface UnitDoctorView {
    double inputNumber();
    !
    String fromUnit();
    !
    String toUnit();
    !
    void showResult(double result);
    }

    View full-size slide

  30. Presenter-First
    public class UnitDoctorTest {
    @Rule public JUnitRuleMockery context = new JUnitRuleMockery()
    !
    UnitDoctorView view = context.mock(UnitDoctorView.class);
    UnitDoctor unitDoctor = new UnitDoctor(view);
    !
    @Test
    public void convertInchesToCm() throws Exception {
    context.checking(new Expectations() {{
    allowing(view).inputNumber(); will(returnValue(1.0));
    allowing(view).fromUnit(); will(returnValue("in"));
    allowing(view).toUnit(); will(returnValue("cm"));
    oneOf(view).showResult(2.54);
    }});
    !
    unitDoctor.convert();
    }
    !

    View full-size slide

  31. Discover the “view” interface
    @Test
    public void showsConversionNotSupported() throws Exception {
    context.checking(new Expectations() {{
    allowing(view).inputNumber(); will(returnValue(anyDouble()));
    allowing(view).fromUnit(); will(returnValue("XYZ"));
    allowing(view).toUnit(); will(returnValue("ABC"));
    oneOf(view).showConversionNotSupported();
    }});
    !
    unitDoctor.convert();
    }

    View full-size slide

  32. Discover the “view” interface
    public interface UnitDoctorView {
    double inputNumber();
    !
    String fromUnit();
    !
    String toUnit();
    !
    void showResult(double result);
    !
    void showConversionNotSupported();
    }

    View full-size slide

  33. Implement the “view”
    interface
    • In the Activity class
    • In an Android View class
    • In a new POJO class

    View full-size slide

  34. Yes, yes, yes… but my
    app interacts heavily with
    Android APIs!

    View full-size slide

  35. Acceptance tests
    • Single touch. Dragging the finger should produce
    a colored trail that fades to nothing
    • Two touches. Dragging two fingers should
    produce two decaying trails
    • Multi-touch dashes. Draw a pattern like

    —— —-— —-—

    ———————————

    View full-size slide

  36. Start with a spike!

    View full-size slide

  37. Start with a spike!
    public class FingerView extends View {
    @Override
    protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();
    paint.setColor(Color.BLUE);
    paint.setStrokeWidth(3);
    paint.setStyle(Paint.Style.STROKE);
    canvas.canvas.drawLine(100, 100, 200, 200, paint);
    }
    }

    View full-size slide

  38. More spike!
    public class MyView extends View {
    List points = new ArrayList();
    !
    @Override
    protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();
    paint.setColor(Color.BLUE);
    paint.setStrokeWidth(3);
    for (int i=1; iPoint from = points.get(i-1);
    Point to = points.get(i);
    canvas.drawLine(from.x, from.y, to.x, to.y, paint);
    }
    }
    !
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    int action = event.getActionMasked();
    if (action == MotionEvent.ACTION_DOWN) {
    points.clear();
    points.add(new Point((int) event.getX(), (int) event.getY()));
    } else if (action == MotionEvent.ACTION_MOVE) {
    points.add(new Point((int) event.getX(), (int) event.getY()));
    }
    invalidate();
    return true;
    }
    }

    View full-size slide

  39. FairyFingers
    Core
    Android
    MotionEvent
    Android
    Canvas
    FairyFingersView
    onDraw(Canvas) { ... }
    onTouchEvent(MotionEvent) { ... }
    Android dependent
    Pure Java

    View full-size slide

  40. FairyFingers
    Core
    Android
    MotionEvent
    Android
    Canvas
    FairyFingersView
    onDraw(Canvas) { ... }
    onTouchEvent(MotionEvent) { ... }
    Core
    Canvas
    Core
    MotionEvent
    Android dependent
    Pure Java

    View full-size slide

  41. Model-View Separation
    App!
    (Android)
    Core!
    (Pure Java)
    Acceptance!
    Test
    Unit!
    Test

    View full-size slide

  42. !
    public interface CoreCanvas {
    void drawLine(float startX, float startY, float stopX, float stopY);
    }
    !
    !
    !
    !
    @Override
    protected void onDraw(final Canvas canvas) {
    paint.setColor(Color.BLUE);
    paint.setStrokeWidth(4);
    for (Line line : core.lines()) {
    line.drawOn(new CoreCanvas() {
    @Override
    public void drawLine(float startX, float startY, float stopX, float stopY) {
    canvas.drawLine(startX, startY, stopX, stopY, paint);
    }
    });
    }
    }

    View full-size slide

  43. public interface CoreMotionEvent {
    int getAction();
    int getPointerCount();
    int getPointerId(int pointerIndex);
    void getPointerCoords(int pointerIndex, CorePoint outPointerCoords);
    }
    // Constants copied from android.view.MotionEven
    static final int ACTION_DOWN = 0;
    static final int ACTION_UP = 1;
    static final int ACTION_MOVE = 2;
    static final int ACTION_CANCEL = 3;
    static final int ACTION_POINTER_DOWN = 5;
    static final int ACTION_POINTER_UP = 6;

    View full-size slide

  44. @Override
    public boolean onTouchEvent(final MotionEvent event) {
    core.onTouch(new CoreMotionEvent() {
    @Override
    public int getPointerCount() {
    return event.getPointerCount();
    }
    !
    @Override
    public int getPointerId(int pointerIndex) {
    return event.getPointerId(pointerIndex);
    }
    !
    @Override
    public void getPointerCoords(int pointerIndex, CorePoint outPointerCoords) {
    MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
    event.getPointerCoords(pointerIndex, coords);
    outPointerCoords.x = coords.x;
    outPointerCoords.y = coords.y;
    }
    !
    @Override
    public int getActionIndex() {
    return event.getActionIndex();
    }
    !
    @Override
    public int getAction() {

    View full-size slide

  45. !
    public class FairyFingersCoreTest {
    FairyFingersCore core = new FairyFingersCore();
    !
    @Test
    public void noLinesAtStart() throws Exception {
    assertEquals(0, core.lines().size());
    }
    !
    @Test
    public void startOneLine() throws Exception {
    core.onTouch(down(10.0f, 100.0f));
    !
    assertEquals(1, core.lines().size());
    assertEquals("(10.0,100.0)", core.lines(0).toString());
    }
    !
    @Test
    public void oneLineDownMove() throws Exception {
    core.onTouch(down(10.0f, 110.0f));
    core.onTouch(move(20.0f, 120.0f));
    core.onTouch(move(30.0f, 130.0f));
    !
    assertEquals("(10.0,110.0)->(20.0,120.0)->(30.0,130.0)", core.lines(0).toString());
    }
    !
    @Test
    public void oneLineDownMoveUp() throws Exception {
    core.onTouch(down(10.0f, 100.0f));
    core.onTouch(move(20.9f, 120.0f));
    TDD!

    View full-size slide

  46. Programming
    Skill
    > Fancy
    Libraries

    View full-size slide

  47. Value adding ?
    Or waste?

    View full-size slide

  48. Deliver valuable
    software faster,
    sustainably

    View full-size slide

  49. References
    • TDD For Android book (unfinished!) 

    leanpub.com/tddforandroid!
    • Source code for examples:

    github.com/xpmatteo!
    • Learn OOP and TDD well:

    Growing Object-Oriented Software, Nat Pryce and Steve Freeman

    TDD By Example, Kent Beck

    Applying UML and Patterns, Craig Larman

    View full-size slide

  50. Credits
    • The motto “Deliver valuable software faster,
    sustainably” is my elaboration on Dan North’s “The
    goal of software delivery is to minimise the lead
    time to business impact. Everything else is detail.” I
    advise to attend Dan’s seminars. They are
    illuminating.
    • The “gravity test” example is derived from a
    presentation by Diego Torres Milano

    View full-size slide

  51. Questions?
    THANK YOU

    View full-size slide