- PVSM.RU - https://www.pvsm.ru -

Построение Android приложений шаг за шагом, часть вторая

Построение Android приложений шаг за шагом, часть вторая - 1

В первой части статьи [1] мы разработали приложение для работы с github, состоящее из двух экранов, разделенное по слоям с применением паттерна MVP. Мы использовали RxJava для упрощения взаимодействия с сервером и две модели данных для разных слоев. Во второй части мы внедрим Dagger 2, напишем unit тесты, посмотрим на MockWebServer, JaCoCo и Robolectric.

Содержание:

Введение

В первой части статьи мы в два этапа создали простое приложение для работы с github.

Условная схема приложения

Построение Android приложений шаг за шагом, часть вторая - 2

Диаграмма классов

Построение Android приложений шаг за шагом, часть вторая - 3 [19]

Все исходники вы можете найти на Github [20]. Ветки в репозитории соответствуют шагам в статье: Step 3 Dependency injection [21] — третий шаг, Step 4 Unit tests [22] — четвертый шаг.

Шаг 3. Dependency Injection

Перед тем, как использовать Dagger 2, необходимо понять принцип Dependency injection (Внедрение зависимости) [23].

Представим, что то у нас есть объект A, который включает объект B. Без использования DI мы должны создавать объект B в коде класса A. Например так:

public class A {
   B b;

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

Такой код сразу же нарушает SRP [24] и DRP [25] из принципов SOLID [26]. Самым простым решением является передача объекта B в конструктор класса A, тем самым мы реализуем Dependency Injection “вручную”:

public class A {
   B b;

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

Обычно DI реализуется с помощью сторонних библиотек, где благодаря аннотациям, происходит автоматическая подстановка объекта.

public class A {
   @Inject
   B b;

   public A() {
       inject();
   }
}

Подробнее об этом механизме и его применении на Android можно прочитать в этой статье: Знакомимся с Dependency Injection на примере Dagger [27]

Dagger 2

Dagger 2 — библиотека созданная Google для реализации DI. Ее основное преимущество в кодогенерации, т.е. все ошибки будут видны на этапе компиляции. На хабре есть хорошая статья про Dagger 2 [28], также можно почитать официальную страницу [29] или хорошую инструкцию на codepath [30]

Для установки Dagger 2 необходимо отредактировать 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'
}

Также очень рекомендуется поставить плагин Dagger IntelliJ Plugin [31]. Он поможет ориентироваться откуда и куда происходят инжекции.

Dagger IntelliJ Plugin

Построение Android приложений шаг за шагом, часть вторая - 4

Сами объекты для внедрения Dagger 2 берет из методов модулей (методы должны помечаться аннотацией Provides [32], модули — Module [33]) или создает их с помощью конструктора класса аннотированного Inject [34]. Например:

@Module
public class ModelModule {

   @Provides
   @Singleton
   ApiInterface provideApiInterface() {
       return ApiModule.getApiInterface();
   }
}

или

public class RepoBranchesMapper 

   @Inject
   public RepoBranchesMapper() {}
}

Поля для внедрения обозначаются аннотацией Inject [34]:

@Inject
protected ApiInterface apiInterface;

Связываются эти две вещи с помощью компонентов (@Component). В них указывается откуда брать объекты и куда их внедрять (методы inject). Пример:

@Singleton
@Component(modules = {ModelModule.class})
public interface AppComponent {

   void inject(ModelImpl dataRepository);
}

Для работы Dagger 2 мы будем использовать один компонент (AppComponent) и 3 модуля для разных слоев (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

Для Model — слоя необходимо необходимо предоставлять ApiInterface и два Scheduler для управления потоками. Для Scheduler необходимо использовать аннотацию Named [35], чтобы Dagger разобрался с графом зависимостей.

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

Для presenter слоя нам необходимо предоставлять Model и CompositeSubscription, а также мапперы. Model и CompositeSubscription будем предоставлять через модули, мапперы — с помощью аннотированного конструктора.

Presenter Module

public class PresenterModule {

   @Provides
   @Singleton
   Model provideDataRepository() {
       return new ModelImpl();
   }

   @Provides
   CompositeSubscription provideCompositeSubscription() {
       return new CompositeSubscription();
   }
}

Пример маппера с аннотированным конструктором

public class RepoBranchesMapper implements Func1<List<BranchDTO>, List<Branch>> {

   @Inject
   public RepoBranchesMapper() {
   }

   @Override
   public List<Branch> call(List<BranchDTO> branchDTOs) {
       List<Branch> branches = Observable.from(branchDTOs)
               .map(branchDTO -> new Branch(branchDTO.getName()))
               .toList()
               .toBlocking()
               .first();
       return branches;
   }
}

View

Со View слоем и внедрением презентеров ситуация сложнее. При создании презентера мы в конструкторе передаем интерфейс View. Соответственно, Dagger должен иметь ссылку на реализацию этого интерфейса, т.е на наш фрагмент. Можно пойти и другим путем, изменив интерфейс презентера и передавая ссылку на view в onCreate. Рассмотрим оба случая.

Передача ссылки на view.

У нас есть фрагмент RepoListFragment, реализующий интерфейс RepoListView,
и RepoListPresenter, принимающий на вход в конструкторе этот RepoListView. Нам необходимо внедрить RepoListPresenter в RepoListFragment. Для реализации такой схемы нам придется создать новый компонент и новый модуль, который в конструкторе будет принимать ссылку на наш интерфейс RepoListView. В этом модуле мы будем создавать презентер (с использованием ссылки на интрефейс RepoListView) и внедрять его в фрагмент.

Внедрение во фрагменте

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

Компонент

@Singleton
@Component(modules = {ViewDynamicModule.class})
public interface ViewComponent {

   void inject(RepoListFragment repoListFragment);
}

Модуль

@Module
public class ViewDynamicModule {

   RepoListView view;

   public ViewDynamicModule(RepoListView view) {
       this.view = view;
   }

   @Provides
   RepoListPresenter provideRepoListPresenter() {
       return new RepoListPresenter(view);
   }
}

В реальных приложениях у вас будет множество инжекций и модулей, поэтому создание различных компонентов для различных сущностей — отличная идея для предотвращения создания god object [36].

Изменение кода презентера.

Приведенный выше метод требует создания нескольких файлов и множества действий. В нашем случае, есть гораздо более простой способ, изменим конструктор и будем передавать ссылку на интерфейс в onCreate.
Код:

Внедрение во фрагменте

@Inject
RepoInfoPresenter presenter;

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

Модуль

@Module
public class ViewModule {

   @Provides
   RepoInfoPresenter provideRepoInfoPresenter() {
       return new RepoInfoPresenter();
   }
}

Завершив внедрение Dagger 2, перейдем к тестированию приложения.

Шаг 4.Тестирование, Unit test

Тестирование давно стало неотъемлемой частью процесса разработки ПО.
Википедия выделяет множество видов тестирования [37], в первую очередь разберемся с модульным (unit) тестированием.

Модульное тестирование [38] процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.
Идея состоит в том, чтобы писать тесты для каждого нетривиального метода. Это позволяет достаточно быстро проверить, не привело ли очередное изменение кода к регрессии, то есть к появлению ошибок в уже оттестированных местах программы, а также облегчает обнаружение и устранение таких ошибок.

Написать полностью изолированные тесты у нас не получится, потому что все компоненты взаимодействуют друг с другом. Под unit тестами, мы будем понимать проверку работы одного модуля окруженного моками. Взаимодействие нескольких реальных модулей будем проверять в интеграционных тестах.

Схема взаимодействия модулей:

Построение Android приложений шаг за шагом, часть вторая - 5

Пример тестирования маппера (серые модули — не используются, зеленые — моки, синий — тестируемый модуль):

Построение Android приложений шаг за шагом, часть вторая - 6

Инфраструктура

Инструменты и фреймворки повышают удобство написания и поддержки тестов. CI сервер, который не даст вам сделать merge при красных тестах, резко уменьшает шансы неожиданной поломки тестов в master branch. Автоматический запуск тестов и ночные сборки помогают выявить проблемы на самом раннем этапе. Этот принцип получил название fail fast [39].
Про тестовое окружение вы можете почитать в статье Тестирование на Android: Robolectric + Jenkins + JaСoСo [40]. В дальнейшем мы будем использовать Robolecric [41] для написания тестов, mockito [42] для создания моков и JaСoСo [43] для проверки покрытия кода тестами.

Паттерн MVP позволяет быстро и эффективно писать тесты на наш код. С помощью Dagger 2 мы сможем подменить настоящие объекты на тестовые моки, изолировав код от внешнего мира. Для этого используем тестовый компонент с тестовыми модулями. Подмена компонента происходит в тестовом application, который мы задаем с помощью аннотации Config [44](application = TestApplication.class) в базовом тестовом классе.

JaCoCo Code Coverage

Перед началом работы, нужно определить какие методы тестировать и как считать процент покрытия тестами. Для этого используем библиотеку JaCoCo, которая генерирует отчеты по результатам выполнения тестов.
Современная Android Studio поддерживает code coverage из коробки [45] или можно настроить его, добавив в 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
   }
}

Обратите внимание на исключенные классы: мы удалили все что связано с Dagger 2 и нашими моделями DTO и VO.

Запустим jacoco (gradlew jacocoTestReport) и посмотрим на результаты:

Построение Android приложений шаг за шагом, часть вторая - 7

Сейчас у нас процент покрытия идеально совпадает с нашим количеством тестов, т.е 0% =) Давайте исправим эту ситуацию!

Model

В model слое нам необходимо проверить правильность настройки retrofit (ApiInterface), корректность создания клиента и работу ModelImpl.
Компоненты должны проверяться изолированно, поэтому для проверки нам нужно эмулировать сервер, в этом нам поможет MockWebServer [46]. Настраиваем ответы сервера и проверяем запросы retrofit.

Схема Model слоя, классы требующие тестирования помечены красным

Построение Android приложений шаг за шагом, часть вторая - 8

Тестовый модуль для 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();
   }
}

Примеры тестов

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<List<RepositoryDTO>> testSubscriber = new TestSubscriber<>();
       apiInterface.getRepositories(TestConst.TEST_OWNER).subscribe(testSubscriber);

       testSubscriber.assertNoErrors();
       testSubscriber.assertValueCount(1);

       List<RepositoryDTO> 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();
    }
}

Для проверки модели мокаем ApiInterface и проверяем корректность работы.

Пример тестов для 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<List<BranchDTO>> testSubscriber = new TestSubscriber<>();
   model.getRepoBranches(TestConst.TEST_OWNER, TestConst.TEST_REPO).subscribe(testSubscriber);

   testSubscriber.assertNoErrors();
   testSubscriber.assertValueCount(1);

   List<BranchDTO> actual = testSubscriber.getOnNextEvents().get(0);

   assertEquals(3, actual.size());
   assertEquals("QuickStart", actual.get(0).getName());
   assertEquals("94870e23f1cfafe7201bf82985b61188f650b245", actual.get(0).getCommit().getSha());
}

Проверим покрытие в Jacoco:

Построение Android приложений шаг за шагом, часть вторая - 9

Presenter

В presenter слое нам необходимо протестировать работу мапперов и работу презентеров.

Схема Presenter слоя, классы требующие тестирования помечены красным

Построение Android приложений шаг за шагом, часть вторая - 10

С мапперами все достаточно просто. Считываем json из файлов, преобразуем и проверяем.
С презентерами — мокаем model и проверяем вызовы необходимых методов у view. Также необходимо проверить корректность onSubscribe и onStop, для этого перехватываем подписку (Subscription) и проверяем isUnsubscribed

Пример тестов в presenter слое

    @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<Subscription> captor = ArgumentCaptor.forClass(Subscription.class);
        verify(repoListPresenter).addSubscription(captor.capture());
        List<Subscription> subscriptions = captor.getAllValues();
        assertEquals(1, subscriptions.size());
        assertTrue(subscriptions.get(0).isUnsubscribed());
    }

Смотрим изменение в JaCoCo:

Построение Android приложений шаг за шагом, часть вторая - 11

View

При тестирование View слоя, нам необходимо проверить только вызовы методов жизненного цикла презентера из фрагмента. Вся логика содержится в презентерах.

Схема View слоя, классы требующие тестирования помечены красным

Построение Android приложений шаг за шагом, часть вторая - 12

Пример тестирования фрагмента

@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);
}

Финальное покрытие тестами:

Построение Android приложений шаг за шагом, часть вторая - 13

Заключение или to be continued…

Во второй части статьи мы рассмотрели внедрение Dagger 2 и покрыли код unit тестами. Благодаря использованию MVP и подмене инжекций мы смогли быстро написать тесты на все части приложения. Весь код доступен на github [20]. Статья написана при активном участии nnesterov [47]. В следующей части рассмотрим интеграционное и функциональное тестирование, а также поговорим про TDD.

Автор: Rambler&Co

Источник [48]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android/112695

Ссылки в тексте:

[1] первой части статьи: https://habrahabr.ru/company/rambler-co/blog/275943/

[2] Введение: #20

[3] Шаг 3. Dependency Injection: #21

[4] Dagger 2: #22

[5] Model: #23

[6] Presenter: #24

[7] View: #25

[8] Шаг 4.Тестирование, Unit test: #26

[9] Инфраструктура: #27

[10] JaCoCo Code Coverage : #28

[11] Model: #29

[12] Presenter: #211

[13] View: #212

[14] Заключение или to be continued: #213

[15] Введение: https://habrahabr.ru/company/rambler-co/blog/275943/#1

[16] Шаг 1. Простая архитектура: https://habrahabr.ru/company/rambler-co/blog/275943/#2

[17] Шаг 2. Усложненная архитектура: https://habrahabr.ru/company/rambler-co/blog/275943/#9

[18] Шаг 3. Dependency Injection: https://habrahabr.ru/company/rambler-co/blog/277343/#2

[19] Image: https://habrastorage.org/files/4e0/9f7/802/4e09f78028274958819a59445c3065fd.png

[20] Github: https://github.com/andrey7mel/android-step-by-step

[21] Step 3 Dependency injection: https://github.com/andrey7mel/android-step-by-step/tree/Step_3_Dependency_injection

[22] Step 4 Unit tests: https://github.com/andrey7mel/android-step-by-step/tree/Step_4_Unit_tests

[23] Dependency injection (Внедрение зависимости): https://ru.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8

[24] SRP: https://en.wikipedia.org/wiki/Single_responsibility_principle

[25] DRP: https://en.wikipedia.org/wiki/Dependency_inversion_principle

[26] SOLID: https://ru.wikipedia.org/wiki/SOLID_(%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)

[27] Знакомимся с Dependency Injection на примере Dagger: https://habrahabr.ru/post/202866/

[28] хорошая статья про Dagger 2: https://habrahabr.ru/company/ncloudtech/blog/274025/

[29] официальную страницу: http://google.github.io/dagger/

[30] хорошую инструкцию на codepath : https://github.com/codepath/android_guides/wiki/Dependency-Injection-with-Dagger-2

[31] Dagger IntelliJ Plugin: https://github.com/square/dagger-intellij-plugin

[32] Provides: https://habrahabr.ru/users/provides/

[33] Module: https://habrahabr.ru/users/module/

[34] Inject: https://habrahabr.ru/users/inject/

[35] Named: https://habrahabr.ru/users/named/

[36] god object: https://ru.wikipedia.org/wiki/%D0%91%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82

[37] Википедия выделяет множество видов тестирования: https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%BD%D0%BE%D0%B3%D0%BE_%D0%BE%D0%B1%D0%B5%D1%81%D0%BF%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F

[38] Модульное тестирование: https://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%B4%D1%83%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5

[39] fail fast: https://habrahabr.ru/post/218325/

[40] Тестирование на Android: Robolectric + Jenkins + JaСoСo: https://habrahabr.ru/company/rambler-co/blog/266837/

[41] Robolecric: http://robolectric.org/

[42] mockito: http://mockito.org/

[43] JaСoСo: http://eclemma.org/jacoco/

[44] Config: https://habrahabr.ru/users/config/

[45] поддерживает code coverage из коробки: https://codelabs.developers.google.com/codelabs/android-testing/index.html#10

[46] MockWebServer: https://github.com/square/okhttp/tree/master/mockwebserver

[47] nnesterov: https://habrahabr.ru/users/nnesterov/

[48] Источник: https://habrahabr.ru/post/277343/