61
TDD for ANDROID Matteo Vaccari [email protected] @xpmatteo

TDD For Android -- DroidCon Torino 2015

Embed Size (px)

Citation preview

Page 1: TDD For Android -- DroidCon Torino 2015

TDD for ANDROID

Matteo Vaccari !

[email protected] @xpmatteo

Page 2: TDD For Android -- DroidCon Torino 2015

WHY TEST?

Page 3: TDD For Android -- DroidCon Torino 2015

Deliver software faster

Page 4: TDD For Android -- DroidCon Torino 2015

Deliver valuable software faster

Page 5: TDD For Android -- DroidCon Torino 2015

Deliver valuable software faster,

sustainably

Page 6: TDD For Android -- DroidCon Torino 2015

WHY TEST?

Page 7: TDD For Android -- DroidCon Torino 2015

WHY TEST?

• Tests help me design my code

• Tests check that my code works

the developer perspective

the tester perspective

Page 8: TDD For Android -- DroidCon Torino 2015

Some tests are less useful than others…

Page 9: TDD For Android -- DroidCon Torino 2015
Page 10: TDD For Android -- DroidCon Torino 2015
Page 11: TDD For Android -- DroidCon Torino 2015
Page 12: TDD For Android -- DroidCon Torino 2015
Page 13: TDD For Android -- DroidCon Torino 2015
Page 14: TDD For Android -- DroidCon Torino 2015

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

Page 15: TDD For Android -- DroidCon Torino 2015

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 !!!

Page 16: TDD For Android -- DroidCon Torino 2015

What is TDD?

Page 17: TDD For Android -- DroidCon Torino 2015

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

Page 18: TDD For Android -- DroidCon Torino 2015

Why is it difficult to TDD on Android?

Page 19: TDD For Android -- DroidCon Torino 2015

The TDD cycle should be fast!

Page 20: TDD For Android -- DroidCon Torino 2015
Page 21: TDD For Android -- DroidCon Torino 2015

Value adding ? Or waste?

Page 22: TDD For Android -- DroidCon Torino 2015

fixing Gradle builds is waste

and is not funHow 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?

Page 23: TDD For Android -- DroidCon Torino 2015

Programming Skill > Fancy

Libraries

Page 24: TDD For Android -- DroidCon Torino 2015

Model-View Separation

Page 25: TDD For Android -- DroidCon Torino 2015

Model-View Separation

Page 26: TDD For Android -- DroidCon Torino 2015

Model-View Separation

App!(Android)

Core!(Pure Java)

Acceptance!Test

Unit!Test

Page 27: TDD For Android -- DroidCon Torino 2015

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

Page 28: TDD For Android -- DroidCon Torino 2015

Unit Doctor Acceptance Tests

public void testInchesToCentimeters() throws Throwable { givenTheUserSelectedConversion("in", "cm"); whenTheUserEnters("2"); thenTheResultIs("2.00 in = 5.08 cm"); }! public void testFahrenheitToCelsius() throws Throwable { 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"); }

Page 29: TDD For Android -- DroidCon Torino 2015

public class UnitConversionAcceptanceTest extends ActivityInstrumentationTestCase2<MainActivity> { 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 Throwable { 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

Page 30: TDD For Android -- DroidCon Torino 2015

}! private void givenTheUserSelectedConversion(String fromUnit, String toUnit) throws Throwable { 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); }}

Page 31: TDD For Android -- DroidCon Torino 2015

The simplest thingpublic 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); }!

Page 32: TDD For Android -- DroidCon Torino 2015

The simplest thingpublic 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); }!

Page 33: TDD For Android -- DroidCon Torino 2015

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

Page 34: TDD For Android -- DroidCon Torino 2015

Yes yes… but my app interacts heavily with

Android widgets!

Page 35: TDD For Android -- DroidCon Torino 2015

Presenter-Firstpublic 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(); }! @Test

Page 36: TDD For Android -- DroidCon Torino 2015

What are mocks good for?

Mocking Android APIs?

Designing interfaces ✅

Page 37: TDD For Android -- DroidCon Torino 2015

Discover the “view” interface

public interface UnitDoctorView { double inputNumber();! String fromUnit();! String toUnit();! void showResult(double result); }

Page 38: TDD For Android -- DroidCon Torino 2015

Presenter-Firstpublic 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(); }! @Test

Page 39: TDD For Android -- DroidCon Torino 2015

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(); }

Page 40: TDD For Android -- DroidCon Torino 2015

Discover the “view” interface

public interface UnitDoctorView { double inputNumber();! String fromUnit();! String toUnit();! void showResult(double result);! void showConversionNotSupported(); }

Page 41: TDD For Android -- DroidCon Torino 2015

Implement the “view” interface

• In the Activity class

• In an Android View class

• In a new POJO class

Page 42: TDD For Android -- DroidCon Torino 2015

Yes, yes, yes… but my app interacts heavily with

Android APIs!

Page 43: TDD For Android -- DroidCon Torino 2015
Page 44: TDD For Android -- DroidCon Torino 2015

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 —— —-— —-————————————

Page 45: TDD For Android -- DroidCon Torino 2015

Start with a spike!

Page 46: TDD For Android -- DroidCon Torino 2015

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); } }

Page 47: TDD For Android -- DroidCon Torino 2015

More spike!

Page 48: TDD For Android -- DroidCon Torino 2015

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; }}

Page 49: TDD For Android -- DroidCon Torino 2015

FairyFingersCore

AndroidMotionEvent

AndroidCanvas

FairyFingersView

onDraw(Canvas) { ... }

onTouchEvent(MotionEvent) { ... }

Android dependent

Pure Java

Page 50: TDD For Android -- DroidCon Torino 2015

FairyFingersCore

AndroidMotionEvent

AndroidCanvas

FairyFingersView

onDraw(Canvas) { ... }

onTouchEvent(MotionEvent) { ... }

CoreCanvas

CoreMotionEvent Android dependent

Pure Java

Page 51: TDD For Android -- DroidCon Torino 2015

Model-View Separation

App!(Android)

Core!(Pure Java)

Acceptance!Test

Unit!Test

Page 52: TDD For Android -- DroidCon Torino 2015

! 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); } }); } }

Page 53: TDD For Android -- DroidCon Torino 2015

public interface CoreMotionEvent { int getAction(); int getPointerCount(); int getPointerId(int pointerIndex); void getPointerCoords(int pointerIndex, CorePoint outPointerCoords); }

// Constants copied from android.view.MotionEvent 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;

Page 54: TDD For Android -- DroidCon Torino 2015

@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() { return event.getAction();

Page 55: TDD For Android -- DroidCon Torino 2015

!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)); core.onTouch(move(30.0f, 130.0f));

TDD!

Page 56: TDD For Android -- DroidCon Torino 2015

Programming Skill > Fancy

Libraries

Page 57: TDD For Android -- DroidCon Torino 2015

Value adding ? Or waste?

Page 58: TDD For Android -- DroidCon Torino 2015

Deliver valuable software faster,

sustainably

Page 59: TDD For Android -- DroidCon Torino 2015

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

Page 60: TDD For Android -- DroidCon Torino 2015

Credits• Carlo Bellettini was my sparring partner for this work

• 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 one of Dan’s seminars. They are illuminating.

• The “Fairy Fingers” idea is from “Ribbons” by Carlo Pescio (www.aspectroid.com)

• The “gravity test” example is derived from a presentation by Diego Torres Milano

Page 61: TDD For Android -- DroidCon Torino 2015

Questions?

THANK YOU