Android
 TDD &
Marcin Gryszko
 @mgryszko
New
gig
User stories
  written
Kickoff?
walking
     skeleton
“Implementation of the
thinnest possible slice of
real functionality that we
can automatically build,
deploy, and test end-to-
end”
original idea by Alistair Cockburn
Iteration “zero”
  TDD cycle
0
1. Understand the problem
2. Think about architecture
3. Automate
Android testing framework
Architecture
Automate
+ Android
  plugin
Failing acceptance test
public class TimelineTest extends ActivityInstrumentationTestCase2<TimelineActivity> {

    public void test_chirps_are_displayed_timely_ordered() throws Exception {
        timelineDriver.waitUntilTimelineLoaded(mgryszkosChirps().size());
        assertTimelineEqualTo(timelineDriver.getDisplayedTimeline(), mgryszkosChirps());
    }
}
Failing acceptance test
public class TimelineTest extends ActivityInstrumentationTestCase2<TimelineActivity> {

    public void test_chirps_are_displayed_timely_ordered() throws Exception {
        timelineDriver.waitUntilTimelineLoaded(mgryszkosChirps().size());
        assertTimelineEqualTo(timelineDriver.getDisplayedTimeline(), mgryszkosChirps());
    }
}
Page object
public class TimelineDriver {
    private Solo solo;
    public TimelineDriver(Solo solo) { this.solo = solo; }

    public void waitUntilTimelineLoaded(int timelineSize) {
        solo.waitForView(TableRow.class, timelineSize, 5000);
    }

    public List<Chirp> getDisplayedTimeline() {
        ListView timelineView = (ListView) solo.getView(R.id.timelineView);
        List<Chirp> chirps = new ArrayList<Chirp>();
        for (int i = 0; i < timelineView.getCount(); i++) {
            chirps.add((Chirp) timelineView.getItemAtPosition(i));
        }
        return chirps;
    }
}
Failing acceptance test
public class TimelineTest extends ActivityInstrumentationTestCase2<TimelineActivity> {

    private Solo solo;
    private TimelineDriver timelineDriver;

    protected void setUp() throws Exception {
        super.setUp();
        solo = new Solo(getInstrumentation(), getActivity());
        timelineDriver = new TimelineDriver(solo);
    }

    public void test_chirps_are_displayed_timely_ordered() throws Exception {
        timelineDriver.waitUntilTimelineLoaded(mgryszkosChirps().size());
        assertTimelineEqualTo(timelineDriver.getDisplayedTimeline(), mgryszkosChirps());
    }

    private void assertTimelineEqualTo(List<Chirp> actual, List<Chirp> expected) {
        assertEquals(expected, actual);
    }
}
Passing acceptance test
public class TimelineActivity extends Activity {
    public void onCreate(Bundle savedInstanceState) {   // super.onCreate & setContentView omitted
        List<Chirp> chirps = loadTimeline();
        displayTimeline(chirps);
    }

    private List<Chirp> loadTimeline() {
        return executeJsonRequest("http://...", "mgryszko");
    }

    private List<Chirp> executeJsonRequest(String uri, Object... requestParams) {
        RestTemplate restTemplate = new RestTemplate();
        return asList(restTemplate.getForObject(uri, Chirp[].class, requestParams));
    }

    private void displayTimeline(List<Chirp> chirps) {
        TimelineAdapter adapter = new TimelineAdapter();
        adapter.setActivity(this);
        adapter.setChirps(chirps);
        ListView timelineView = (ListView) findViewById(R.id.timelineView);
        timelineView.setAdapter(adapter);
    }
}
Tracer bullet
     hit
Let’s
 ref
 ac
 tor
Hamcrest
public class TimelineTest extends ActivityInstrumentationTestCase2<TimelineActivity> {

    private void assertTimelineEqualTo(List<Chirp> actual, List<Chirp> expected) {
        assertEquals(expected, actual);
    }
}
Hamcrest
public class TimelineTest extends ActivityInstrumentationTestCase2<TimelineActivity> {

    private void assertTimelineEqualTo(List<Chirp> actual, List<Chirp> expected) {
        assertThat(actual.size(), equalTo(expected.size()));
        for (Chirp chirp : expected) {
            assertThat(format("Chirp %s not in the timeline", chirp), actual, hasItem(chirp));
        }
    }
}
Roboguice
public class TimelineActivity extends Activity {
    private void displayTimeline(List<Chirp> chirps) {
        TimelineAdapter adapter = new TimelineAdapter();
        adapter.setActivity(this);
        adapter.setChirps(chirps);
        ListView timelineView = (ListView) findViewById(R.id.timelineView);
        timelineView.setAdapter(adapter);
    }
}
Roboguice
public class TimelineActivity extends RoboActivity {
    @InjectView(R.id.timelineView)
    private ListView timelineView;

    private void displayTimeline(List<Chirp> chirps) {
        TimelineAdapter adapter = getInjector()
            .getInstance(TimelineAdapter.class)
            .withChirps(chirps));
        timelineView.setAdapter(adapter);
    }
}
Falling integration test
public class ChirpJsonRepositoryTest extends TestCase {
    private ChirpRepository repository = new ChirpJsonRepository();

    public void test_returns_the_timeline_of_a_chirper() {
        assertTimelineEqualTo(repository.findTimelineOf("mgryszko"), mgryszkosChirps());
    }
}
Passing integration test
public class ChirpJsonRepository implements ChirpRepository {
    public List<Chirp> findTimelineOf(String chirper) {
        return executeJsonRequest("http://...", chirper);
    }

    private List<Chirp> executeJsonRequest(String uri, Object... requestParams) {
        RestTemplate restTemplate = new RestTemplate();
        return asList(restTemplate.getForObject(uri, Chirp[].class, requestParams));
    }
}
Passing integration test
public class TimelineActivity extends Activity {
    private List<Chirp> loadTimeline() {
        return executeJsonRequest("http://...", "mgryszko");
    }

    private List<Chirp> executeJsonRequest(String uri, Object... requestParams) {
        RestTemplate restTemplate = new RestTemplate();
        return asList(restTemplate.getForObject(uri, Chirp[].class, requestParams));
    }
}
Passing integration test
public class TimelineActivity extends RoboActivity {
    @Inject
    private ChirpRepository chirpRepository;

    private List<Chirp> loadTimeline() {
        return chirpRepository.findTimelineOf("mgryszko");
    }
}
Optimization
Async loading
public class TimelineActivity extends RoboActivity
    implements TimelineLoadListener {

    @Inject
    private AsyncTimelineLoader timelineLoader;

    private void loadTimeline() {
        timelineLoader.loadChirperTimeline("mgryszko", this);
    }

    public void timelineLoaded(List<Chirp> timeline) {
        displayTimeline(timeline);
    }
}

public interface TimelineLoadListener {
    void timelineLoading();

    void timelineLoaded(List<Chirp> timeline);
}
Async loading
public class TimelineLoadTask extends RoboAsyncTask<List<Chirp>>
    implements AsyncTimelineLoader {

    public void loadChirperTimeline(String chirper, TimelineLoadListener loadListener) {
        this.execute();
    }

    @Override
    protected void onPreExecute() throws Exception {
        loadListener.timelineLoading();
    }

    public List<Chirp> call() throws Exception {
        return repository.findTimelineOf(chirper);
    }

    @Override
    protected void onSuccess(List<Chirp> chirps) throws Exception {
        loadListener.timelineLoaded(chirps);
    }
}
First unit test
public class TimelineLoadTaskTest extends RoboUnitTestCase<Application> {

    private Mockery context = new Mockery();
    private ChirpRepository repository = context.mock(ChirpRepository.class);
    private TimelineLoadListener loadListener = context.mock(TimelineLoadListener.class);

    public void test_loads_timeline_successfully_and_notifies_the_listener() {
        TimelineLoadTask task = createTimelineLoadTask();

        context.checking(new Expectations() {{
            List<Chirp> timeline = asList(A_CHIRP);

               oneOf(loadListener).timelineLoading();
               oneOf(repository).findTimelineOf(CHIRPER); will(returnValue(timeline));
               oneOf(loadListener).timelineLoaded(with(timeline));
        }});

        executeInFakeUIThread(task);
    }
}
First unit test
    private CountDownLatch taskDone = new CountDownLatch(1);

    private TimelineLoadTask createTimelineLoadTask() {
        return new TimelineLoadTask(repository) {
            @Override
            protected void onFinally() {
                taskDone.countDown();
            }
        };
    }

    private void executeInFakeUIThread(final TimelineLoadTask task) {
        new RoboLooperThread() {
            public void run() {
                task.loadChirperTimeline(CHIRPER, loadListener);
            }
        }.start();
        taskDone.await();
    }
}
Really unit?
straight JUnit
IntelliJ
java.lang.RuntimeException: Stub!
at junit.runner.BaseTestRunner.(BaseTestRunner.java:5)




Maven
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.024
sec
TestNG
IntelliJ
===============================================
Custom suite
Total tests run: 1, Failures: 0, Skips: 0
===============================================



Maven
Running TestSuite
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.298
sec
Robolectric
@RunWith(InjectingTestRunner.class)
public class TimelineUnitTest {
    @Inject
    private TimelineActivity activity;

    @Inject
    private AsyncTimelineLoaderStub timelineLoader;

    @Test
    public void triggers_timeline_loading() {
        activity.onCreate(NO_SAVED_INSTANCE_STATE);
        assertThat(timelineLoader.isTimelineLoaded(), is(true));
    }

    @Test
    public void progress_dialog_is_displayed_when_timeline_is_loaded() {
        activity.timelineLoading();
        assertThat(Robolectric.shadowOf(activity).getLastShownDialogId(),
            is(TimelineActivity.PROGRESS_DIALOG_ID));
    }
}
Wrap-up
Thanks!
https://github.com/mgryszko/android-
                 tdd-ci
you
              ?
  jobs     Marcin
   @
osoco.es
Image sources:
http://www.flickr.com/photos/sarah_mccans/219287847/
http://www.flickr.com/photos/f-oxymoron/5005146417/
http://www.bluebison.net/content/2007/a-skeleton-walking-his-pets/
http://www.flickr.com/photos/familymwr/5009855774/
http://www.flickr.com/photos/zbraineater/2213219097/
http://www.flickr.com/photos/stusev/3296738594/
http://www.flickr.com/photos/hadamsky/293310259/
http://www.flickr.com/photos/qthomasbower/3392847831/
http://www.flickr.com/photos/mikecogh/5113779851/

Android TDD & CI

  • 1.
    Android TDD & MarcinGryszko @mgryszko
  • 2.
  • 3.
  • 4.
  • 5.
    walking skeleton “Implementation of the thinnest possible slice of real functionality that we can automatically build, deploy, and test end-to- end” original idea by Alistair Cockburn
  • 6.
  • 7.
    0 1. Understand theproblem 2. Think about architecture 3. Automate
  • 8.
  • 9.
  • 10.
  • 11.
    + Android plugin
  • 13.
    Failing acceptance test publicclass TimelineTest extends ActivityInstrumentationTestCase2<TimelineActivity> { public void test_chirps_are_displayed_timely_ordered() throws Exception { timelineDriver.waitUntilTimelineLoaded(mgryszkosChirps().size()); assertTimelineEqualTo(timelineDriver.getDisplayedTimeline(), mgryszkosChirps()); } }
  • 14.
    Failing acceptance test publicclass TimelineTest extends ActivityInstrumentationTestCase2<TimelineActivity> { public void test_chirps_are_displayed_timely_ordered() throws Exception { timelineDriver.waitUntilTimelineLoaded(mgryszkosChirps().size()); assertTimelineEqualTo(timelineDriver.getDisplayedTimeline(), mgryszkosChirps()); } }
  • 15.
    Page object public classTimelineDriver { private Solo solo; public TimelineDriver(Solo solo) { this.solo = solo; } public void waitUntilTimelineLoaded(int timelineSize) { solo.waitForView(TableRow.class, timelineSize, 5000); } public List<Chirp> getDisplayedTimeline() { ListView timelineView = (ListView) solo.getView(R.id.timelineView); List<Chirp> chirps = new ArrayList<Chirp>(); for (int i = 0; i < timelineView.getCount(); i++) { chirps.add((Chirp) timelineView.getItemAtPosition(i)); } return chirps; } }
  • 16.
    Failing acceptance test publicclass TimelineTest extends ActivityInstrumentationTestCase2<TimelineActivity> { private Solo solo; private TimelineDriver timelineDriver; protected void setUp() throws Exception { super.setUp(); solo = new Solo(getInstrumentation(), getActivity()); timelineDriver = new TimelineDriver(solo); } public void test_chirps_are_displayed_timely_ordered() throws Exception { timelineDriver.waitUntilTimelineLoaded(mgryszkosChirps().size()); assertTimelineEqualTo(timelineDriver.getDisplayedTimeline(), mgryszkosChirps()); } private void assertTimelineEqualTo(List<Chirp> actual, List<Chirp> expected) { assertEquals(expected, actual); } }
  • 17.
    Passing acceptance test publicclass TimelineActivity extends Activity { public void onCreate(Bundle savedInstanceState) { // super.onCreate & setContentView omitted List<Chirp> chirps = loadTimeline(); displayTimeline(chirps); } private List<Chirp> loadTimeline() { return executeJsonRequest("http://...", "mgryszko"); } private List<Chirp> executeJsonRequest(String uri, Object... requestParams) { RestTemplate restTemplate = new RestTemplate(); return asList(restTemplate.getForObject(uri, Chirp[].class, requestParams)); } private void displayTimeline(List<Chirp> chirps) { TimelineAdapter adapter = new TimelineAdapter(); adapter.setActivity(this); adapter.setChirps(chirps); ListView timelineView = (ListView) findViewById(R.id.timelineView); timelineView.setAdapter(adapter); } }
  • 18.
  • 19.
  • 20.
    Hamcrest public class TimelineTestextends ActivityInstrumentationTestCase2<TimelineActivity> { private void assertTimelineEqualTo(List<Chirp> actual, List<Chirp> expected) { assertEquals(expected, actual); } }
  • 21.
    Hamcrest public class TimelineTestextends ActivityInstrumentationTestCase2<TimelineActivity> { private void assertTimelineEqualTo(List<Chirp> actual, List<Chirp> expected) { assertThat(actual.size(), equalTo(expected.size())); for (Chirp chirp : expected) { assertThat(format("Chirp %s not in the timeline", chirp), actual, hasItem(chirp)); } } }
  • 22.
    Roboguice public class TimelineActivityextends Activity { private void displayTimeline(List<Chirp> chirps) { TimelineAdapter adapter = new TimelineAdapter(); adapter.setActivity(this); adapter.setChirps(chirps); ListView timelineView = (ListView) findViewById(R.id.timelineView); timelineView.setAdapter(adapter); } }
  • 23.
    Roboguice public class TimelineActivityextends RoboActivity { @InjectView(R.id.timelineView) private ListView timelineView; private void displayTimeline(List<Chirp> chirps) { TimelineAdapter adapter = getInjector() .getInstance(TimelineAdapter.class) .withChirps(chirps)); timelineView.setAdapter(adapter); } }
  • 24.
    Falling integration test publicclass ChirpJsonRepositoryTest extends TestCase { private ChirpRepository repository = new ChirpJsonRepository(); public void test_returns_the_timeline_of_a_chirper() { assertTimelineEqualTo(repository.findTimelineOf("mgryszko"), mgryszkosChirps()); } }
  • 25.
    Passing integration test publicclass ChirpJsonRepository implements ChirpRepository { public List<Chirp> findTimelineOf(String chirper) { return executeJsonRequest("http://...", chirper); } private List<Chirp> executeJsonRequest(String uri, Object... requestParams) { RestTemplate restTemplate = new RestTemplate(); return asList(restTemplate.getForObject(uri, Chirp[].class, requestParams)); } }
  • 26.
    Passing integration test publicclass TimelineActivity extends Activity { private List<Chirp> loadTimeline() { return executeJsonRequest("http://...", "mgryszko"); } private List<Chirp> executeJsonRequest(String uri, Object... requestParams) { RestTemplate restTemplate = new RestTemplate(); return asList(restTemplate.getForObject(uri, Chirp[].class, requestParams)); } }
  • 27.
    Passing integration test publicclass TimelineActivity extends RoboActivity { @Inject private ChirpRepository chirpRepository; private List<Chirp> loadTimeline() { return chirpRepository.findTimelineOf("mgryszko"); } }
  • 28.
  • 29.
    Async loading public classTimelineActivity extends RoboActivity implements TimelineLoadListener { @Inject private AsyncTimelineLoader timelineLoader; private void loadTimeline() { timelineLoader.loadChirperTimeline("mgryszko", this); } public void timelineLoaded(List<Chirp> timeline) { displayTimeline(timeline); } } public interface TimelineLoadListener { void timelineLoading(); void timelineLoaded(List<Chirp> timeline); }
  • 30.
    Async loading public classTimelineLoadTask extends RoboAsyncTask<List<Chirp>> implements AsyncTimelineLoader { public void loadChirperTimeline(String chirper, TimelineLoadListener loadListener) { this.execute(); } @Override protected void onPreExecute() throws Exception { loadListener.timelineLoading(); } public List<Chirp> call() throws Exception { return repository.findTimelineOf(chirper); } @Override protected void onSuccess(List<Chirp> chirps) throws Exception { loadListener.timelineLoaded(chirps); } }
  • 31.
    First unit test publicclass TimelineLoadTaskTest extends RoboUnitTestCase<Application> { private Mockery context = new Mockery(); private ChirpRepository repository = context.mock(ChirpRepository.class); private TimelineLoadListener loadListener = context.mock(TimelineLoadListener.class); public void test_loads_timeline_successfully_and_notifies_the_listener() { TimelineLoadTask task = createTimelineLoadTask(); context.checking(new Expectations() {{ List<Chirp> timeline = asList(A_CHIRP); oneOf(loadListener).timelineLoading(); oneOf(repository).findTimelineOf(CHIRPER); will(returnValue(timeline)); oneOf(loadListener).timelineLoaded(with(timeline)); }}); executeInFakeUIThread(task); } }
  • 32.
    First unit test private CountDownLatch taskDone = new CountDownLatch(1); private TimelineLoadTask createTimelineLoadTask() { return new TimelineLoadTask(repository) { @Override protected void onFinally() { taskDone.countDown(); } }; } private void executeInFakeUIThread(final TimelineLoadTask task) { new RoboLooperThread() { public void run() { task.loadChirperTimeline(CHIRPER, loadListener); } }.start(); taskDone.await(); } }
  • 33.
  • 34.
    straight JUnit IntelliJ java.lang.RuntimeException: Stub! atjunit.runner.BaseTestRunner.(BaseTestRunner.java:5) Maven Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.024 sec
  • 35.
    TestNG IntelliJ =============================================== Custom suite Total testsrun: 1, Failures: 0, Skips: 0 =============================================== Maven Running TestSuite Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.298 sec
  • 36.
    Robolectric @RunWith(InjectingTestRunner.class) public class TimelineUnitTest{ @Inject private TimelineActivity activity; @Inject private AsyncTimelineLoaderStub timelineLoader; @Test public void triggers_timeline_loading() { activity.onCreate(NO_SAVED_INSTANCE_STATE); assertThat(timelineLoader.isTimelineLoaded(), is(true)); } @Test public void progress_dialog_is_displayed_when_timeline_is_loaded() { activity.timelineLoading(); assertThat(Robolectric.shadowOf(activity).getLastShownDialogId(), is(TimelineActivity.PROGRESS_DIALOG_ID)); } }
  • 37.
  • 38.
  • 39.
  • 40.
    you ? jobs Marcin @ osoco.es
  • 41.