$30 off During Our Annual Pro Sale. View Details »

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 ! matteo.vaccari@thoughtworks.com @xpmatteo

  2. WHY TEST?

  3. Deliver software faster

  4. Deliver valuable software faster

  5. Deliver valuable software faster, sustainably

  6. WHY TEST?

  7. WHY TEST? • Tests help me design my code •

    Tests check that my code works the developer perspective the tester perspective
  8. Some tests are less useful than others…

  9. None
  10. None
  11. None
  12. None
  13. None
  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. .
  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 !!!
  16. What is TDD?

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

  18. Why is it difficult to TDD on Android?

  19. The TDD cycle should be fast!

  20. None
  21. Value adding ? Or waste?

  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?
  23. Programming Skill > Fancy Libraries

  24. Model-View Separation

  25. Model-View Separation

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

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

  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" }
  29. public class UnitConversionAcceptanceTest extends ActivityInstrumentationTestCase2<M public 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() {
  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); } }
  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); }
  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); }
  33. Very easy to test public class UnitDoctorTest { ! @Test

    public void convertsInchesToCentimeters() { UnitDoctor unitDoctor = new UnitDoctor(); assertEquals("2.54", unitDoctor.convert("1", "in", "cm")); } ! }
  34. Yes yes… but my app interacts heavily with Android widgets!

  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(); } !
  36. What are mocks good for? Mocking Android APIs? Designing interfaces

  37. Discover the “view” interface public interface UnitDoctorView { double inputNumber();

    ! String fromUnit(); ! String toUnit(); ! void showResult(double result); }
  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(); } !
  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(); }
  40. Discover the “view” interface public interface UnitDoctorView { double inputNumber();

    ! String fromUnit(); ! String toUnit(); ! void showResult(double result); ! void showConversionNotSupported(); }
  41. Implement the “view” interface • In the Activity class •

    In an Android View class • In a new POJO class
  42. Yes, yes, yes… but my app interacts heavily with Android

    APIs!
  43. None
  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
 —— —-— —-—
 ———————————
  45. Start with a spike!

  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); } }
  47. More spike!

  48. More spike! public class MyView extends View { List<Point> points

    = new ArrayList<Point>(); ! @Override protected void onDraw(Canvas canvas) { Paint paint = new Paint(); paint.setColor(Color.BLUE); paint.setStrokeWidth(3); for (int i=1; i<points.size(); i++) { Point 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; } }
  49. FairyFingers Core Android MotionEvent Android Canvas FairyFingersView onDraw(Canvas) { ...

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

    } onTouchEvent(MotionEvent) { ... } Core Canvas Core MotionEvent Android dependent Pure Java
  51. Model-View Separation App! (Android) Core! (Pure Java) Acceptance! Test Unit!

    Test
  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); } }); } }
  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;
  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() {
  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!
  56. Programming Skill > Fancy Libraries

  57. Value adding ? Or waste?

  58. Deliver valuable software faster, sustainably

  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
  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
  61. Questions? THANK YOU