What’s the best testing framework on Android? Espresso or Robotium? Robolectric or a plain JUnit test?
The reason why many developers don’t write tests is not due to the testing libraries but because of the low testability of the Android code.
In this talk we’ll see, thanks to a practical example, how to use Dependency Injection (using Dagger) and the Model View Presenter pattern to write a testable Android application.
3. DroidCon Italy – Torino – April 2015 – @fabioCollini 3
Quick survey
Do you write automated tests?
4. DroidCon Italy – Torino – April 2015 – @fabioCollini 4
Return of Investment - ROI
Net profit
Investment
5. DroidCon Italy – Torino – April 2015 – @fabioCollini 5
Agenda
1. Legacy code
2. Dependency Injection
3. Mockito
4. Dagger
5. Dagger & Android
6. Model View Presenter
6. DroidCon Italy – Torino – April 2015 – @fabioCollini 6
TestableAndroidAppsDroidCon15
https://github.com/fabioCollini/
TestableAndroidAppsDroidCon15
7. DroidCon Italy - Torino - April 2015 - @fabioCollini
1Legacy code
8. DroidCon Italy – Torino – April 2015 – @fabioCollini 8
Legacy code
Edit and pray
Vs
Cover and modify
Legacy code is code
without unit tests
9. DroidCon Italy – Torino – April 2015 – @fabioCollini 9
Instrumentation tests
run on a device (real or emulated)
high code coverage
Vs
JVM tests
fast
low code coverage
10. DroidCon Italy – Torino – April 2015 – @fabioCollini 10
Espresso
public class PostListActivityTest {
//see https://gist.github.com/JakeWharton/1c2f2cadab2ddd97f9fb
@Rule
public ActivityRule<PostListActivity> rule =
new ActivityRule<>(PostListActivity.class);
}
@Test public void showListActivity() {
onView(withText("???"))
.check(matches(isDisplayed()));
}
@Test public void showErrorLayoutOnServerError() {
//???
onView(withId(R.id.error_layout))
.check(matches(isDisplayed()));
}
11. DroidCon Italy – Torino – April 2015 – @fabioCollini 11
Legacy code dilemma
When we change code,
we should have tests in place.
To put tests in place,
we often have to change code.
Michael Feathers
12. DroidCon Italy – Torino – April 2015 – @fabioCollini 12
TestablePostListActivity
public class TestablePostListActivity
extends PostListActivity {
public static Observable<List<Post>> result;
@Override
protected Observable<List<Post>> createListObservable() {
return result;
}
}
13. DroidCon Italy – Torino – April 2015 – @fabioCollini 13
PostListActivityTest
public class PostListActivityTest {
@Rule public ActivityRule<TestablePostListActivity> rule =
new ActivityRule<>(
TestablePostListActivity.class,
false
);
@Test public void showListActivity() {
TestablePostListActivity.result = Observable.just(
createPost(1), createPost(2), createPost(3)
).toList();
rule.launchActivity();
onView(withText("title 1”))
.check(matches(isDisplayed()));
}
14. DroidCon Italy – Torino – April 2015 – @fabioCollini 14
PostListActivityTest
@Test public void checkErrorLayoutDisplayed() {
TestablePostListActivity.result =
Observable.error(new IOException());
rule.launchActivity();
onView(withId(R.id.error_layout))
.check(matches(isDisplayed()));
}
15. DroidCon Italy – Torino – April 2015 – @fabioCollini 15
Legacy code
Not the perfect solution
First step to increase coverage
Then modify and refactor
16. DroidCon Italy - Torino - April 2015 - @fabioCollini
2Dependency Injection
17. DroidCon Italy – Torino – April 2015 – @fabioCollini 17
PostBatch
public void execute() {
PostResponse postResponse = createService().listPosts();
EmailSender emailSender = new EmailSender();
List<Post> posts = postResponse.getPosts();
for (Post post : posts) {
emailSender.sendEmail(post);
}
}
private static WordPressService createService() {
//...
}
18. DroidCon Italy – Torino – April 2015 – @fabioCollini 18
PostBatch
WordPressService
EmailSender
Class under test
Collaborator
Collaborator
19. DroidCon Italy – Torino – April 2015 – @fabioCollini 19
PostBatchTest
public class PostBatchTest {
private PostBatch postBatch = new PostBatch();
@Test
public void testExecute() {
postBatch.execute();
//???
}
}
20. DroidCon Italy – Torino – April 2015 – @fabioCollini 20
Inversion Of Control
private WordPressService wordPressService;
private EmailSender emailSender;
public PostBatch(WordPressService wordPressService,
EmailSender emailSender) {
this.wordPressService = wordPressService;
this.emailSender = emailSender;
}
public void execute() {
PostResponse postResponse = wordPressService.listPosts();
List<Post> posts = postResponse.getPosts();
for (Post post : posts) {
emailSender.sendEmail(post);
}
}
21. DroidCon Italy – Torino – April 2015 – @fabioCollini 21
Dependency Injection
public class Main {
public static void main(String[] args) {
new PostBatch(
createService(), new EmailSender()
).execute();
}
private static WordPressService createService() {
//...
}
}
22. DroidCon Italy – Torino – April 2015 – @fabioCollini 22
WordPressServiceStub
public class WordPressServiceStub
implements WordPressService {
private PostResponse postResponse;
public WordPressServiceStub(PostResponse postResponse) {
this.postResponse = postResponse;
}
@Override public PostResponse listPosts() {
return postResponse;
}
}
23. DroidCon Italy – Torino – April 2015 – @fabioCollini 23
EmailSenderSpy
public class EmailSenderSpy extends EmailSender {
private int emailCount;
@Override public void sendEmail(Post p) {
emailCount++;
}
public int getEmailCount() {
return emailCount;
}
}
24. DroidCon Italy – Torino – April 2015 – @fabioCollini 24
PostBatch
WordPressService
EmailSender
Stub
Spy
25. DroidCon Italy – Torino – April 2015 – @fabioCollini 25
Test doubles
private PostBatch postBatch;
private EmailSenderSpy emailSenderSpy;
private WordPressServiceStub serviceStub;
@Test
public void testExecute() {
postBatch.execute();
assertEquals(3, emailSenderSpy.getEmailCount());
}
@Before public void init() {
emailSenderSpy = new EmailSenderSpy();
serviceStub = new WordPressServiceStub(
new PostResponse(new Post(), new Post(), new Post())
);
postBatch = new PostBatch(serviceStub, emailSenderSpy);
}
30. DroidCon Italy – Torino – April 2015 – @fabioCollini 30
Dagger
A fast dependency injector for Android and Java
v1 developed at Square
https://github.com/square/dagger
v2 developed at Google
https://github.com/google/dagger
Configuration using annotations and Java classes
Based on annotation processing (no reflection)
31. DroidCon Italy – Torino – April 2015 – @fabioCollini 31
Module
@Module
public class MainModule {
@Provides @Singleton EmailSender provideEmailSender() {
return new EmailSender();
}
@Provides @Singleton WordPressService provideService() {
//...
}
@Provides PostBatch providePostsBatch(
WordPressService wordPressService,
EmailSender emailSender) {
return new PostBatch(wordPressService, emailSender);
}
}
32. DroidCon Italy – Torino – April 2015 – @fabioCollini 32
PostBatch
WordPressService
EmailSender
Component
Main
33. DroidCon Italy – Torino – April 2015 – @fabioCollini 33
Component
@Singleton
@Component(modules = MainModule.class)
public interface MainComponent {
PostBatch getBatch();
}
public class Main {
public static void main(String[] args) {
MainComponent component = DaggerMainComponent.create();
PostBatch batch = component.getBatch();
batch.execute();
}
}
34. DroidCon Italy – Torino – April 2015 – @fabioCollini 34
Inject annotation
public class PostBatch {
private WordPressService wordPressService;
private EmailSender emailSender;
@Inject public PostBatch(
WordPressService wordPressService,
EmailSender emailSender) {
this.wordPressService = wordPressService;
this.emailSender = emailSender;
}
}
public class PostBatch {
@Inject WordPressService wordPressService;
@Inject EmailSender emailSender;
@Inject public PostBatch() {
}
}
35. DroidCon Italy - Torino - April 2015 - @fabioCollini
5Dagger & Android
36. DroidCon Italy – Torino – April 2015 – @fabioCollini 36
PostListActivity
WordPressService
ShareActivity
ShareExecutor
37. DroidCon Italy – Torino – April 2015 – @fabioCollini 37
ShareExecutor
public class ShareExecutor {
private Context context;
public ShareExecutor(Context context) {
this.context = context;
}
public void startSendActivity(String title, String body) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TITLE, title);
intent.putExtra(Intent.EXTRA_TEXT, body);
intent.setType("text/plain");
Intent chooserIntent = Intent.createChooser(intent,
context.getResources().getText(R.string.share));
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(chooserIntent);
}
}
38. DroidCon Italy – Torino – April 2015 – @fabioCollini 38
ApplicationModule
@Module public class ApplicationModule {
private Application application;
public ApplicationModule(Application application) {
this.application = application;
}
@Provides @Singleton WordPressService providesService() {
//...
}
@Provides @Singleton ShareExecutor shareExecutor() {
return new ShareExecutor(application);
}
}
39. DroidCon Italy – Torino – April 2015 – @fabioCollini 39
ApplicationComponent
@Singleton
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {
void inject(PostListActivity activity);
void inject(ShareActivity activity);
}
40. DroidCon Italy – Torino – April 2015 – @fabioCollini 40
Component
PostListActivity
Application
ShareActivity
ShareExecutorWordPressService
41. DroidCon Italy – Torino – April 2015 – @fabioCollini 41
Application
public class CnjApplication extends Application {
private ApplicationComponent component;
@Override public void onCreate() {
super.onCreate();
component = DaggerApplicationComponent.builder()
.applicationModule(new ApplicationModule(this))
.build();
}
public ApplicationComponent getComponent() {
return component;
}
public void setComponent(ApplicationComponent c) {
this.component = c;
}
}
55. DroidCon Italy – Torino – April 2015 – @fabioCollini 55
Android Model View Presenter
Activity (or Fragment) is the View
All the business logic is in the Presenter
Presenter is managed using Dagger
Model is the Activity (or Fragment) state
Presenter is retained on configuration change
56. DroidCon Italy – Torino – April 2015 – @fabioCollini 56
PostListPresenter JVM Test
@RunWith(MockitoJUnitRunner.class)
public class PostListPresenterTest {
@Mock WordPressService wordPressService;
@Mock PostListActivity view;
private PostListPresenter postListPresenter;
@Before public void setUp() {
SchedulerManager schedulerManager =
new SchedulerManager(
Schedulers.immediate(), Schedulers.immediate());
postListPresenter = new PostListPresenter(
wordPressService, schedulerManager
);
}
//...
57. DroidCon Italy – Torino – April 2015 – @fabioCollini 57
PostListPresenter JVM Test
@Test
public void loadingPosts() {
when(wordPressService.listPosts()).thenReturn(
Observable.just(new PostResponse(new Post(),
new Post(), new Post())));
PostListModel model = new PostListModel();
postListPresenter.setModel(model);
postListPresenter.resume(view);
assertEquals(3, model.getItems().size());
}
58. DroidCon Italy – Torino – April 2015 – @fabioCollini 58
PostListPresenter JVM Test
@Test
public void clickOnItem() {
PostListModel model = new PostListModel();
model.setItems(Arrays.asList(
createPost(1), createPost(2), createPost(3)));
postListPresenter.setModel(model);
postListPresenter.resume(view);
postListPresenter.onItemClick(1);
verify(view).startShareActivity(
eq("title 2"), eq("name 2 last name 2nexcerpt 2"));
}
59. DroidCon Italy – Torino – April 2015 – @fabioCollini 59
SharePresenter JVM Test
@RunWith(MockitoJUnitRunner.class)
public class SharePresenterTest {
@Mock ShareExecutor shareExecutor;
@Mock ShareActivity view;
@InjectMocks SharePresenter sharePresenter;
@Test public void testValidationOk() {
ShareModel model = new ShareModel();
sharePresenter.init(view, model);
sharePresenter.share("title", "body");
verify(shareExecutor)
.startSendActivity(eq("title"), eq("body"));
}
}
60. DroidCon Italy – Torino – April 2015 – @fabioCollini 60
TL;DR
Dagger
Mockito
Espresso UI tests
JVM Presenter tests
61. DroidCon Italy – Torino – April 2015 – @fabioCollini 61
Thanks for your attention!
Questions?
github.com/fabioCollini/
TestableAndroidAppsDroidCon15
github.com/google/dagger
mockito.org
cosenonjaviste.it/libri