Upload
fabio-collini
View
1.479
Download
2
Tags:
Embed Size (px)
Citation preview
Testable Android AppsFabio Collini
DroidCon Italy – Torino – April 2015 – @fabioCollini 2
Fabio Collini@fabioCollini linkedin.com/in/fabiocollini Folder Organizer cosenonjaviste.it
nana bianca Freapp instal.com Rain tomorrow?
DroidCon Italy – Torino – April 2015 – @fabioCollini 3
Quick survey
Do you write automated tests?
DroidCon Italy – Torino – April 2015 – @fabioCollini 4
Return of Investment - ROI
Net profit Investment
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
DroidCon Italy – Torino – April 2015 – @fabioCollini 6
TestableAndroidAppsDroidCon15
https://github.com/fabioCollini/TestableAndroidAppsDroidCon15
DroidCon Italy - Torino - April 2015 - @fabioCollini
1Legacy code
DroidCon Italy – Torino – April 2015 – @fabioCollini 8
Legacy code
Edit and pray Vs
Cover and modify
Legacy code is code without unit tests
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
DroidCon Italy – Torino – April 2015 – @fabioCollini 10
Espressopublic 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())); }
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
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; }}
DroidCon Italy – Torino – April 2015 – @fabioCollini 13
PostListActivityTestpublic 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())); }
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()));}
DroidCon Italy – Torino – April 2015 – @fabioCollini 15
Legacy code
Not the perfect solution First step to increase coverage Then modify and refactor
DroidCon Italy - Torino - April 2015 - @fabioCollini
2Dependency Injection
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() { //...}
DroidCon Italy – Torino – April 2015 – @fabioCollini 18
PostBatch
WordPressService
EmailSender
Class under test
Collaborator
Collaborator
DroidCon Italy – Torino – April 2015 – @fabioCollini 19
PostBatchTest
public class PostBatchTest { private PostBatch postBatch = new PostBatch(); @Test public void testExecute() { postBatch.execute(); //??? } }
DroidCon Italy – Torino – April 2015 – @fabioCollini 20
Inversion Of Controlprivate 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); } }
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() { //... } }
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; }}
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; }}
DroidCon Italy – Torino – April 2015 – @fabioCollini 24
PostBatch
WordPressService
EmailSender
Stub
Spy
DroidCon Italy – Torino – April 2015 – @fabioCollini 25
Test doublesprivate PostBatch postBatch; private EmailSenderSpy emailSenderSpy; private WordPressServiceStub serviceStub;
@Testpublic 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); }
DroidCon Italy - Torino - April 2015 - @fabioCollini
3Mockito
DroidCon Italy – Torino – April 2015 – @fabioCollini 27
Mockitoprivate WordPressService service; private EmailSender emailSender; private PostBatch postBatch;
@Before public void init() { emailSender = Mockito.mock(EmailSender.class); service = Mockito.mock(WordPressService.class); postBatch = new PostBatch(service, emailSender); }
@Test public void testExecute() { when(service.listPosts()).thenReturn( new PostResponse(new Post(), new Post(), new Post())); postBatch.execute(); verify(emailSender, times(3)).sendEmail(any(Post.class));}
Arrange Act Assert
DroidCon Italy – Torino – April 2015 – @fabioCollini 28
@InjectMocks@RunWith(MockitoJUnitRunner.class) public class PostBatchTest { @Mock WordPressService service; @Mock EmailSender sender; @InjectMocks PostBatch postBatch; @Test public void testExecute() { when(service.listPosts()).thenReturn( new PostResponse(new Post(), new Post(), new Post())); postBatch.execute(); verify(sender, times(3)).sendEmail(any(Post.class)); }}
DroidCon Italy - Torino - April 2015 - @fabioCollini
4Dagger
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)
DroidCon Italy – Torino – April 2015 – @fabioCollini 31
Module@Modulepublic 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); }}
DroidCon Italy – Torino – April 2015 – @fabioCollini 32
PostBatch
WordPressService
EmailSenderCo
mpo
nent
Main
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(); }}
DroidCon Italy – Torino – April 2015 – @fabioCollini 34
Inject annotationpublic 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() { }}
DroidCon Italy - Torino - April 2015 - @fabioCollini
5Dagger & Android
DroidCon Italy – Torino – April 2015 – @fabioCollini 36
PostListActivity
WordPressService
ShareActivity
ShareExecutor
DroidCon Italy – Torino – April 2015 – @fabioCollini 37
ShareExecutorpublic 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); }}
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); }}
DroidCon Italy – Torino – April 2015 – @fabioCollini 39
ApplicationComponent
@Singleton @Component(modules = ApplicationModule.class) public interface ApplicationComponent { void inject(PostListActivity activity);
void inject(ShareActivity activity);
}
DroidCon Italy – Torino – April 2015 – @fabioCollini 40
Component
PostListActivity
Application
ShareActivity
ShareExecutorWordPressService
DroidCon Italy – Torino – April 2015 – @fabioCollini 41
Applicationpublic 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; }}
DroidCon Italy – Torino – April 2015 – @fabioCollini 42
PostListActivitypublic class PostListActivity extends ActionBarActivity { //... @Inject WordPressService wordPressService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
CnjApplication app = (CnjApplication) getApplicationContext(); ApplicationComponent component = app.getComponent(); component.inject(this); //... } //...}
DroidCon Italy – Torino – April 2015 – @fabioCollini 43
Component
PostListActivity
Application
ShareExecutorWordPressService
TestComponent
MockMock
Test
DroidCon Italy – Torino – April 2015 – @fabioCollini 44
TestModule
@Modulepublic class TestModule { @Provides @Singleton ShareExecutor provideShareExecutor() { return Mockito.mock(ShareExecutor.class); } @Provides @Singleton WordPressService providesWordPressService() { return Mockito.mock(WordPressService.class); }}
DroidCon Italy – Torino – April 2015 – @fabioCollini 45
TestComponent
@Singleton@Component(modules = TestModule.class) public interface TestComponent extends ApplicationComponent { void inject(PostListActivityTest test); void inject(ShareActivityTest test);
}
DroidCon Italy – Torino – April 2015 – @fabioCollini 46
PostListActivityTestpublic class PostListActivityTest { @Inject WordPressService wordPressService; @Rule public ActivityRule<PostListActivity> rule = new ActivityRule<>(PostListActivity.class, false);
@Before public void setUp() { TestComponent component = DaggerTestComponent.create(); CnjApplication application = (CnjApplication) rule.getApplication(); application.setComponent(component); component.inject(this); }
//...
DroidCon Italy – Torino – April 2015 – @fabioCollini 47
PostListActivityTest
@Test public void showListActivity() { when(wordPressService.listPosts()).thenReturn( Observable.just(new PostResponse(createPost(1), createPost(2), createPost(3)))); rule.launchActivity(); onView(withText("title 1”)) .check(matches(isDisplayed()));}
DroidCon Italy – Torino – April 2015 – @fabioCollini 48
PostListActivityTest
@Test public void showErrorLayoutOnServerError() { when(wordPressService.listPosts()).thenReturn( Observable.error(new IOException("error!"))); rule.launchActivity(); onView(withId(R.id.error_layout)) .check(matches(isDisplayed()));}
DroidCon Italy - Torino - April 2015 - @fabioCollini
6Model View Presenter
DroidCon Italy – Torino – April 2015 – @fabioCollini 50
Model View Presenter
View
Presenter
Model
DroidCon Italy – Torino – April 2015 – @fabioCollini 51
View Presenter RetrofitService
onClick
updateupdate(model)
Model
View Presenter RetrofitServiceModel
request
response
DroidCon Italy – Torino – April 2015 – @fabioCollini 52
View Presenter MockService
perform(click())
updateupdate(model)
Model
requestresponse
EspressoTest
View Presenter MockServiceModelEspressoTest
onView
verify
when().thenReturn()
onClick
DroidCon Italy – Torino – April 2015 – @fabioCollini 53
MockView Presenter MockService
onClick
updateupdate(model)
Model
requestresponse
JVM Test
MockView Presenter MockServiceModelJVM Test
verify
verify
assert
when().thenReturn()
DroidCon Italy – Torino – April 2015 – @fabioCollini 54
View Presenter
init
update(model)
ModelEspressoTest
View Presenter ModelEspressoTest
onView
create
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
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 ); } //...
DroidCon Italy – Torino – April 2015 – @fabioCollini 57
PostListPresenter JVM Test
@Testpublic 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());}
DroidCon Italy – Torino – April 2015 – @fabioCollini 58
PostListPresenter JVM Test
@Testpublic 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 2\nexcerpt 2"));}
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")); }}
DroidCon Italy – Torino – April 2015 – @fabioCollini 60
TL;DR
Dagger Mockito Espresso UI tests JVM Presenter tests
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