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 Slide

  2. WHY TEST?

    View Slide

  3. Deliver software faster

    View Slide

  4. Deliver valuable
    software faster

    View Slide

  5. Deliver valuable
    software faster,
    sustainably

    View Slide

  6. WHY TEST?

    View Slide

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

    View Slide

  8. Some tests are less
    useful than others…

    View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. 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 Slide

  15. 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 Slide

  16. What is TDD?

    View Slide

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

    View Slide

  18. Why is it difficult to
    TDD on Android?

    View Slide

  19. The TDD cycle should be
    fast!

    View Slide

  20. View Slide

  21. Value adding ?
    Or waste?

    View Slide

  22. 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 Slide

  23. Programming
    Skill
    > Fancy
    Libraries

    View Slide

  24. Model-View
    Separation

    View Slide

  25. Model-View Separation

    View Slide

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

    View Slide

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

    View Slide

  28. 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 Slide

  29. 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 Slide

  30. }
    !
    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 Slide

  31. 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 Slide

  32. 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 Slide

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

    View Slide

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

    View Slide

  35. 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 Slide

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

    View Slide

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

    View Slide

  38. 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 Slide

  39. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  43. View Slide

  44. 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 Slide

  45. Start with a spike!

    View Slide

  46. 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 Slide

  47. More spike!

    View Slide

  48. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  52. !
    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 Slide

  53. 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 Slide

  54. @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 Slide

  55. !
    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 Slide

  56. Programming
    Skill
    > Fancy
    Libraries

    View Slide

  57. Value adding ?
    Or waste?

    View Slide

  58. Deliver valuable
    software faster,
    sustainably

    View Slide

  59. 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 Slide

  60. 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 Slide

  61. Questions?
    THANK YOU

    View Slide