Testable Android Apps DroidCon Italy 2015

Post on 15-Jul-2015

1.479 views 2 download

Tags:

Transcript of Testable Android Apps DroidCon Italy 2015

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