Building Android apps step by step, part two



    In the first part of the article, we developed an application for working with github, consisting of two screens, separated by layers using the MVP pattern. We used RxJava to simplify interaction with the server and two data models for different layers. In the second part, we will introduce Dagger 2, write unit tests, look at MockWebServer, JaCoCo and Robolectric.

    Content:


    Introduction


    In the first part of the article, we created a simple application for working with github in two stages.

    Conditional application scheme


    Class diagram


    You can find all source codes on Github . The branches in the repository correspond to the steps in the article: Step 3 Dependency injection - the third step, Step 4 Unit tests - the fourth step.

    Step 3. Dependency Injection


    Before using Dagger 2, you need to understand the principle of Dependency injection (Dependency Injection) .

    Imagine that we have object A, which includes object B. Without using DI, we must create object B in the code of class A. For example, like this:

    public class A {
       B b;
       public A() {
           b = new B();
       }
    }
    

    Such code immediately violates the SRP and DRP of SOLID principles . The simplest solution is to pass the object B to the constructor of the class A, thereby we implement the Dependency Injection “manually”:

    public class A {
       B b;
       public A(B b) {
           this.b = b;
       }
    }
    

    Usually, DI is implemented using third-party libraries, where, thanks to annotations, the object is automatically substituted.

    public class A {
       @Inject
       B b;
       public A() {
           inject();
       }
    }
    

    You can read more about this mechanism and its application on Android in this article: Get to know Dependency Injection using Dagger as an example

    Dagger 2

    Dagger 2 is a library created by Google for implementing DI. Its main advantage in code generation, i.e. all errors will be visible at the compilation stage. There is a good article on Habr about Dagger 2 , you can also read the official page or a good instruction on codepath

    To install Dagger 2 you need to edit build.gradle:

    build.gradle
    apply plugin: 'com.android.application'
    apply plugin: 'com.neenbedankt.android-apt'
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:21.0.3'
        compile 'com.google.dagger:dagger:2.0-SNAPSHOT'
        apt 'com.google.dagger:dagger-compiler:2.0-SNAPSHOT'
        provided 'org.glassfish:javax.annotation:10.0-b28'
    }
    


    It is also highly recommended to install the Dagger IntelliJ Plugin plugin . It will help to navigate where and where the injections come from.

    Dagger IntelliJ Plugin


    Dagger 2 takes the objects for implementation from the methods of modules (methods must be marked with Provides annotation , modules with Module ) or creates them using the constructor of the annotated Inject class . For instance:

    @Module
    public class ModelModule {
       @Provides
       @Singleton
       ApiInterface provideApiInterface() {
           return ApiModule.getApiInterface();
       }
    }
    

    or

    public class RepoBranchesMapper 
       @Inject
       public RepoBranchesMapper() {}
    }
    

    Fields for embedding are indicated by an Inject annotation :

    @Inject
    protected ApiInterface apiInterface;
    

    These two things are connected using components (@Component). They indicate where to get objects from and where to inject them (inject methods). Example:

    @Singleton
    @Component(modules = {ModelModule.class})
    public interface AppComponent {
       void inject(ModelImpl dataRepository);
    }
    

    For Dagger 2 we will use one component (AppComponent) and 3 modules for different layers (Model, Presentation, View).

    AppComponent
    @Singleton
    @Component(modules = {ModelModule.class, PresenterModule.class, ViewModule.class})
    public interface AppComponent {
       void inject(ModelImpl dataRepository);
       void inject(BasePresenter basePresenter);
       void inject(RepoListPresenter repoListPresenter);
       void inject(RepoInfoPresenter repoInfoPresenter);
       void inject(RepoInfoFragment repoInfoFragment);
    }
    


    Model

    For the Model layer, you must provide an ApiInterface and two Schedulers for managing flows. For Scheduler, you must use the Named annotation so that Dagger can figure out the dependency graph.

    ModelModule
    @Provides
    @Singleton
    ApiInterface provideApiInterface() {
       return ApiModule.getApiInterface(Const.BASE_URL);
    }
    @Provides
    @Singleton
    @Named(Const.UI_THREAD)
    Scheduler provideSchedulerUI() {
       return AndroidSchedulers.mainThread();
    }
    @Provides
    @Singleton
    @Named(Const.IO_THREAD)
    Scheduler provideSchedulerIO() {
       return Schedulers.io();
    }
    


    Presenter

    For the presenter layer, we need to provide Model and CompositeSubscription, as well as mappers. We will provide Model and CompositeSubscription through modules, mappers - using an annotated constructor.

    Presenter module
    public class PresenterModule {
       @Provides
       @Singleton
       Model provideDataRepository() {
           return new ModelImpl();
       }
       @Provides
       CompositeSubscription provideCompositeSubscription() {
           return new CompositeSubscription();
       }
    }
    


    An example of a mapper with an annotated constructor
    public class RepoBranchesMapper implements Func1, List> {
       @Inject
       public RepoBranchesMapper() {
       }
       @Override
       public List call(List branchDTOs) {
           List branches = Observable.from(branchDTOs)
                   .map(branchDTO -> new Branch(branchDTO.getName()))
                   .toList()
                   .toBlocking()
                   .first();
           return branches;
       }
    }
    


    View

    With the View layer and the introduction of presenters, the situation is more complicated. When creating a presenter, we pass the View interface in the constructor. Accordingly, Dagger should have a link to the implementation of this interface, i.e. to our fragment. You can go the other way, changing the presenter interface and passing the view link to onCreate. We consider both cases.

    Passing a view link.

    We have a RepoListFragment fragment that implements the RepoListView interface,
    and RepoListPresenter, which accepts this RepoListView as an input to the constructor. We need to implement RepoListPresenter in RepoListFragment. To implement such a scheme, we will have to create a new component and a new module, which in the constructor will accept a link to our RepoListView interface. In this module, we will create a presenter (using a link to the RepoListView interface) and embed it in a fragment.

    Injection in fragment
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       DaggerViewComponent.builder()
               .viewDynamicModule(new ViewDynamicModule(this))
               .build()
               .inject(this);
    }
    


    Component
    @Singleton
    @Component(modules = {ViewDynamicModule.class})
    public interface ViewComponent {
       void inject(RepoListFragment repoListFragment);
    }
    


    Module
    @Module
    public class ViewDynamicModule {
       RepoListView view;
       public ViewDynamicModule(RepoListView view) {
           this.view = view;
       }
       @Provides
       RepoListPresenter provideRepoListPresenter() {
           return new RepoListPresenter(view);
       }
    }
    


    In real applications, you will have many injections and modules, so creating different components for different entities is a great idea to prevent the creation of a god object .

    Change presenter code.

    The above method requires the creation of several files and many actions. In our case, there is a much simpler way, we will change the constructor and we will pass the interface link to onCreate.
    The code:

    Injection in fragment
    @Inject
    RepoInfoPresenter presenter;
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       App.getComponent().inject(this);
       presenter.onCreate(this, getRepositoryVO());
    }
    


    Module
    @Module
    public class ViewModule {
       @Provides
       RepoInfoPresenter provideRepoInfoPresenter() {
           return new RepoInfoPresenter();
       }
    }
    


    Having completed the implementation of Dagger 2, let's move on to testing the application.

    Step 4. Unit Testing


    Testing has long been an integral part of the software development process.
    Wikipedia identifies many types of testing , first of all we will deal with unit testing.

    Unit testing is a programming process that allows you to check the correctness of individual modules of the program source code.
    The idea is to write tests for each non-trivial method. This allows you to quickly check whether the next change in the code has led to regression, that is, errors in the already tested places of the program, and also makes it easier to detect and eliminate such errors.

    We will not be able to write completely isolated tests, because all components interact with each other. By unit tests, we will mean checking the operation of one module surrounded by mokami. We will check the interaction of several real modules in integration tests.

    The interaction scheme of the modules:



    Example of testing the mapper (gray modules are not used, green ones are moki, blue is the module under test):



    Infrastructure

    Tools and frameworks increase the convenience of writing and supporting tests. A CI server that prevents you from merging with red tests dramatically reduces the chances of unexpectedly breaking tests in the master branch. Automatically running tests and nightly builds help identify problems at an early stage. This principle is called fail fast .
    You can read about the test environment in the article Testing on Android: Robolectric + Jenkins + JaСoСo . In the future, we will use Robolecric to write tests, mockito to create mocks and JaСoСo to test the coverage of the code with tests.

    The MVP pattern allows you to quickly and efficiently write tests for our code. With the help of Dagger 2, we can replace real objects with test moki, isolating the code from the outside world. For this we use a test component with test modules. The component is replaced in the test application, which we set using the Config annotation (application = TestApplication.class) in the base test class.

    JaCoCo Code Coverage

    Before you begin, you need to determine what methods to test and how to calculate the percentage of coverage by tests. To do this, we use the JaCoCo library, which generates reports on the results of tests.
    Modern Android Studio supports code coverage out of the box, or you can configure it by adding the following lines to build.gradle:

    build.gradle
    apply plugin: 'jacoco'
    jacoco {
       toolVersion = "0.7.1.201405082137"
    }
    def coverageSourceDirs = [
           '../app/src/main/java'
    ]
    task jacocoTestReport(type: JacocoReport, dependsOn: "testDebugUnitTest") {
       group = "Reporting"
       description = "Generate Jacoco coverage reports"
       classDirectories = fileTree(
               dir: '../app/build/intermediates/classes/debug',
               excludes: ['**/R.class',
                          '**/R$*.class',
                          '**/*$ViewInjector*.*',
                          '**/*$ViewBinder*.*',   //DI
                          '**/*_MembersInjector*.*',  //DI
                          '**/*_Factory*.*',  //DI
                          '**/testrx/model/dto/*.*', //dto model
                          '**/testrx/presenter/vo/*.*', //vo model
                          '**/testrx/other/**',
                          '**/BuildConfig.*',
                          '**/Manifest*.*',
                          '**/Lambda$*.class',
                          '**/Lambda.class',
                          '**/*Lambda.class',
                          '**/*Lambda*.class']
       )
       additionalSourceDirs = files(coverageSourceDirs)
       sourceDirectories = files(coverageSourceDirs)
       executionData = files('../app/build/jacoco/testDebugUnitTest.exec')
       reports {
           xml.enabled = true
           html.enabled = true
       }
    }
    


    Pay attention to the excluded classes: we have removed everything related to Dagger 2 and our DTO and VO models.

    Run jacoco (gradlew jacocoTestReport) and look at the results:



    Now we have a percentage of coverage that ideally matches our number of tests, i.e. 0% =) Let's fix this situation!

    Model

    In the model layer, we need to check the correctness of retrofit (ApiInterface) settings, the correctness of client creation and the operation of ModelImpl.
    Components must be scanned in isolation, so for verification we need to emulate the server, MockWebServer will help us with this . We configure server responses and check retrofit requests.

    Model layer scheme, classes requiring testing are marked in red


    Test module for Dagger 2
    @Module
    public class ModelTestModule {
       @Provides
       @Singleton
       ApiInterface provideApiInterface() {
           return mock(ApiInterface.class);
       }
       @Provides
       @Singleton
       @Named(Const.UI_THREAD)
       Scheduler provideSchedulerUI() {
           return Schedulers.immediate();
       }
       @Provides
       @Singleton
       @Named(Const.IO_THREAD)
       Scheduler provideSchedulerIO() {
           return Schedulers.immediate();
       }
    }
    


    Test examples
    public class ApiInterfaceTest extends BaseTest {
       private MockWebServer server;
       private ApiInterface apiInterface;
       @Before
       public void setUp() throws Exception {
           super.setUp();
           server = new MockWebServer();
           server.start();
           final Dispatcher dispatcher = new Dispatcher() {
               @Override
               public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
                   if (request.getPath().equals("/users/" + TestConst.TEST_OWNER + "/repos")) {
                       return new MockResponse().setResponseCode(200)
                               .setBody(testUtils.readString("json/repos"));
                   } else if (request.getPath().equals("/repos/" + TestConst.TEST_OWNER + "/" + TestConst.TEST_REPO + "/branches")) {
                       return new MockResponse().setResponseCode(200)
                               .setBody(testUtils.readString("json/branches"));
                   } else if (request.getPath().equals("/repos/" + TestConst.TEST_OWNER + "/" + TestConst.TEST_REPO + "/contributors")) {
                       return new MockResponse().setResponseCode(200)
                               .setBody(testUtils.readString("json/contributors"));
                   }
                   return new MockResponse().setResponseCode(404);
               }
           };
           server.setDispatcher(dispatcher);
           HttpUrl baseUrl = server.url("/");
           apiInterface = ApiModule.getApiInterface(baseUrl.toString());
       }
       @Test
       public void testGetRepositories() throws Exception {
           TestSubscriber> testSubscriber = new TestSubscriber<>();
           apiInterface.getRepositories(TestConst.TEST_OWNER).subscribe(testSubscriber);
           testSubscriber.assertNoErrors();
           testSubscriber.assertValueCount(1);
           List actual = testSubscriber.getOnNextEvents().get(0);
           assertEquals(7, actual.size());
           assertEquals("Android-Rate", actual.get(0).getName());
           assertEquals("andrey7mel/Android-Rate", actual.get(0).getFullName());
           assertEquals(26314692, actual.get(0).getId());
       }
      @After
        public void tearDown() throws Exception {
            server.shutdown();
        }
    }
    


    To test the model, we wipe ApiInterface and check the correct operation.

    Sample tests for ModelImpl
    @Test
    public void testGetRepoBranches() {
       BranchDTO[] branchDTOs = testUtils.getGson().fromJson(testUtils.readString("json/branches"), BranchDTO[].class);
       when(apiInterface.getBranches(TestConst.TEST_OWNER, TestConst.TEST_REPO)).thenReturn(Observable.just(Arrays.asList(branchDTOs)));
       TestSubscriber> testSubscriber = new TestSubscriber<>();
       model.getRepoBranches(TestConst.TEST_OWNER, TestConst.TEST_REPO).subscribe(testSubscriber);
       testSubscriber.assertNoErrors();
       testSubscriber.assertValueCount(1);
       List actual = testSubscriber.getOnNextEvents().get(0);
       assertEquals(3, actual.size());
       assertEquals("QuickStart", actual.get(0).getName());
       assertEquals("94870e23f1cfafe7201bf82985b61188f650b245", actual.get(0).getCommit().getSha());
    }
    


    Check coverage in Jacoco:



    Presenter

    In the presenter layer, we need to test the work of the mappers and the work of the presenters.

    Layout of the Presenter layer; classes requiring testing are marked in red


    With mappers, everything is quite simple. Read json from files, convert and check.
    With presenters, we wake up the model and check the calls of the necessary methods to the view. It is also necessary to check the correctness of onSubscribe and onStop, for this we intercept the subscription and check isUnsubscribed

    Test example in presenter layer
        @Before
        public void setUp() throws Exception {
            super.setUp();
            component.inject(this);
            activityCallback = mock(ActivityCallback.class);
            mockView = mock(RepoListView.class);
            repoListPresenter = new RepoListPresenter(mockView, activityCallback);
            doAnswer(invocation -> Observable.just(repositoryDTOs))
                    .when(model)
                    .getRepoList(TestConst.TEST_OWNER);
            doAnswer(invocation -> TestConst.TEST_OWNER)
                    .when(mockView)
                    .getUserName();
        }
        @Test
        public void testLoadData() {
            repoListPresenter.onCreateView(null);
            repoListPresenter.onSearchButtonClick();
            repoListPresenter.onStop();
            verify(mockView).showRepoList(repoList);
        }
        @Test
        public void testSubscribe() {
            repoListPresenter = spy(new RepoListPresenter(mockView, activityCallback)); //for ArgumentCaptor
            repoListPresenter.onCreateView(null);
            repoListPresenter.onSearchButtonClick();
            repoListPresenter.onStop();
            ArgumentCaptor captor = ArgumentCaptor.forClass(Subscription.class);
            verify(repoListPresenter).addSubscription(captor.capture());
            List subscriptions = captor.getAllValues();
            assertEquals(1, subscriptions.size());
            assertTrue(subscriptions.get(0).isUnsubscribed());
        }
    


    See the change in JaCoCo:



    View

    When testing the View layer, we need to check only calls to the presenter life cycle methods from the fragment. All logic is contained in presenters.

    Layer View diagram, classes requiring testing are marked in red


    Fragment Testing Example
    @Test
    public void testOnCreateViewWithBundle() {
       repoInfoFragment.onCreateView(LayoutInflater.from(activity), (ViewGroup) activity.findViewById(R.id.container), bundle);
       verify(repoInfoPresenter).onCreateView(bundle);
    }
    @Test
    public void testOnStop() {
       repoInfoFragment.onStop();
       verify(repoInfoPresenter).onStop();
    }
    @Test
    public void testOnSaveInstanceState() {
       repoInfoFragment.onSaveInstanceState(null);
       verify(repoInfoPresenter).onSaveInstanceState(null);
    }
    


    Final test coverage:



    Conclusion or to be continued ...


    In the second part of the article, we examined the implementation of Dagger 2 and covered unit code with tests. Thanks to the use of MVP and the substitution of injections, we were able to quickly write tests for all parts of the application. All code is available on github . The article was written with the active participation of nnesterov . In the next part, we will consider integration and functional testing, as well as talk about TDD.

    UPDATE
    Building Android Applications Step by Step, Part Three

    Also popular now: