- PVSM.RU - https://www.pvsm.ru -
Тестирование — одна из важнейших частей разработки качественных программных продуктов. Сегодня мы поговорим о некоторых методологиях и библиотеках, разработанных и используемых нашей командой для написания тестов Android приложений.
Начнем с самых базовых вещей, потому более опытные разработчики могут перейти сразу к разделу об инструментах для UI тестирования. Для тех, кому хочется узнать или освежить базовые вещи — приятного чтения.
Создадим небольшой компонент, который и будем тестировать. Он парсит файл с JSON объектом, содержащим имя, и возвращает полученную строку:
public class NameRepository {
private final File file;
public NameRepository(File file) {
this.file = file;
}
public String getName() throws IOException {
Gson gson = new Gson();
User user = gson.fromJson(readFile(), User.class);
return user.name;
}
public String readFile() throws IOException {
byte[] bytes = new byte[(int) file.length()];
try (FileInputStream in = new FileInputStream(file)) {
in.read(bytes);
}
return new String(bytes, Charset.defaultCharset());
}
private static final class User {
String name;
}
}
Тут и в дальнейшем я буду приводить сокращенную версию кода. Полную версию можно посмотреть в репозитории [1]. К каждому сниппету будет приложена ссылка на полный код [2].
Теперь напишем первый JUnit [3] тест. JUnit — это Java библиотека для написания тестов. Для того, чтобы JUnit знал, что метод является тестом, нужно добавить к нему аннотацию @Test
. JUnit содержит в себе класс Assert
, который позволяет сравнивать фактические значения с ожидаемыми и выводит ошибку, если значения не совпадают. Этот тест будет тестировать корректность нашего компонента, а именно чтения файла, парсинга JSON и получения верного поля:
public class NameRepositoryTest {
private static final File FILE = new File("test_file");
NameRepository nameRepository = new NameRepository(FILE);
@Test
public void getName_isSasha() throws Exception {
PrintWriter writer = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(FILE), UTF_8)), true);
writer.println("{name : Sasha}");
writer.close();
String name = nameRepository.getName();
Assert.assertEquals(name, "Sasha");
FILE.delete();
}
@Test
public void getName_notMia() throws Exception {
PrintWriter writer = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(FILE), UTF_8)), true);
writer.println("{name : Sasha}");
writer.close();
String name = nameRepository.getName();
Assert.assertNotEquals(name, "Mia");
FILE.delete();
}
}
Полный код [4]
Тесты — это тоже код, который надо поддерживать. Более того, код тестов должен быть прост для понимания, чтобы его можно было верифицировать в уме. Потому есть смысл инвестировать в упрощение кода тестов, избавление от дублирования и повышение читабельности. Посмотрим на широко используемые библиотеки, которые помогут нам в этом деле.
Чтобы не дублировать код подготовки в каждом тесте, существуют аннотации @Before
и @After
. Методы, помеченные аннотацией @Before
, будут выполняться перед каждым тестом, а помеченные аннотацией @After
— после каждого теста. Также есть аннотации @BeforeClass
и @AfterClass
, которые выполняются соответственно перед и после всех тестов в классе. Давайте переделаем наш тест, используя такие методы:
public class NameRepositoryTest {
private static final File FILE = new File("test_file");
NameRepository nameRepository = new NameRepository(FILE);
@Before
public void setUp() throws Exception {
PrintWriter writer = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(FILE), UTF_8)), true);
writer.println("{name : Sasha}");
writer.close();
}
@After
public void tearDown() {
FILE.delete();
}
@Test
public void getName_isSasha() throws Exception {
String name = nameRepository.getName();
Assert.assertEquals(name, "Sasha");
}
@Test
public void getName_notMia() throws Exception {
String name = nameRepository.getName();
Assert.assertNotEquals(name, "Mia");
}
}
Полный код [5]
Мы смогли убрать дублирование кода настройки каждого теста. Однако, много разных классов с тестами могут потребовать создания файла, и это дублирование тоже хотелось бы убрать. Для этого есть библиотека тестовых правил (TestRule [6]). Тестовое правило выполняет функцию схожую с @Before
и @After
. В методе apply() этого класса мы можем выполнить нужные нам действия до и после выполнения каждого или всех тестов. Помимо уменьшения дублирования кода, преимущество такого метода заключается еще и в том, что код выносится из класса тестов, что уменьшает количество кода в тесте и облегчает его чтение. Напишем правило для создания файла:
public class CreateFileRule implements TestRule {
private final File file;
private final String text;
public CreateFileRule(File file, String text) {
this.file = file;
this.text = text;
}
@Override
public Statement apply(final Statement s, Description d) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
PrintWriter writer =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(FILE), UTF_8)), true);
writer.println(text);
writer.close();
try {
s.evaluate();
} finally {
file.delete();
}
}
};
}
}
Полный код [7]
Используем это правило в нашем тесте. Для того, чтобы действия TestRule
исполнялись для каждого теста, нужно пометить TestRule
аннотацией @Rule
.
public class NameRepositoryTest {
static final File FILE = new File("test_file");
@Rule public final CreateFileRule fileRule =
new CreateFileRule(FILE, "{name : Sasha}");
NameRepository nameRepository = new NameRepository(new FileReader(FILE));
@Test
public void getName_isSasha() throws Exception {
String name = nameRepository.getName();
Assert.assertEquals(name, "Sasha");
}
...
}
Полный код [8]
Если правило отметить аннотацией @ClassRule
, то действия будут вызываться не перед каждым тестом, а один раз перед всеми тестами в классе, аналогично аннотациям @BeforeClass
и @AfterClass
.
Когда в тестах используется несколько TestRule
, может понадобиться, чтобы они запускались в определенном порядке, для этого существует RuleChain [9] с помощью которого можно определить порядок запуска наших TestRule
. Создадим правило, которое должно создать папку до того, как будет создан файл:
public class CreateDirRule implements TestRule {
private final File dir;
public CreateDirRule(File dir) {
this.dir = dir;
}
@Override
public Statement apply(final Statement s, Description d) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
dir.mkdir();
try {
s.evaluate();
} finally {
dir.delete();
}
}
};
}
}
Полный код [10]
С этим правилом класс с тестом будет выглядеть следующим образом:
public class NameRepositoryTest {
static final File DIR = new File("test_dir");
static final File FILE = Paths.get(DIR.toString(), "test_file").toFile();
@Rule
public final RuleChain chain = RuleChain
.outerRule(new CreateDirRule(DIR))
.around(new CreateFileRule(FILE, "{name : Sasha}"));
@Test
public void getName_isSasha() throws Exception {
String name = nameRepository.getName();
Assert.assertEquals(name, "Sasha");
}
...
}
Полный код [11]
Теперь в каждом тесте директория будет создаваться перед созданием файла и удаляться после удаления файла.
Google Truth [12] — это библиотека для улучшения читабельности кода тестов. Содержит методы assert (аналогично JUnit Assert [13]), но более читабельные для человека, а также включает гораздо больше вариантов для проверки параметров. Так выглядит предыдущий тест с использование Truth:
@Test
public void getName_isSasha() throws Exception {
String name = nameRepository.getName();
assertThat(name).isEqualTo("Sasha");
}
@Test
public void getName_notMia() throws Exception {
String name = nameRepository.getName();
assertThat(name).isNotEqualTo("Mia");
}
Полный код [14]
Видно, что код читается почти как текст на разговорном английском языке.
Наш компонент делает две разных работы: читает файл и парсит его. Чтобы придерживаться принципа единственной ответственности, давайте выделим логику чтения файла в отдельный компонент:
public class FileReader {
private final File file;
public FileReader(File file) {
this.file = file;
}
public String readFile() throws IOException {
byte[] bytes = new byte[(int) file.length()];
try (FileInputStream in = new FileInputStream(file)) {
in.read(bytes);
}
return new String(bytes, Charset.defaultCharset());
}
}
Полный код [15]
Сейчас мы хотим тестировать именно NameRepository
, а фактически тестируем и чтение файла в FileReader
. Чтобы этого избежать и тем самым повысить изоляцию, надежность и скорость выполнения теста, мы можем заменить реальный FileReader
на его мок.
Mockito [16] — библиотека для для создания заглушек (моков) вместо реальных объектов для использования их в тестах. Некоторые действия, которые можно выполнять с помощью Mockito:
создавать заглушки для классов и интерфейсов;
проверять вызовы метода и значения передаваемые этому методу;
подключение к реальному объекту «шпиона» spy
для контроля вызова методов.
Создадим мок FileReader
и настроим его так, чтобы метод readFile()
возвращал нужную нам строку:
public class NameRepositoryTest {
FileReader fileReader = mock(FileReader.class);
NameRepository nameRepository = new NameRepository(fileReader);
@Before
public void setUp() throws IOException {
when(fileReader.readFile()).thenReturn("{name : Sasha}");
}
@Test
public void getName_isSasha() throws Exception {
String name = nameRepository.getName();
assertThat(name).isEqualTo("Sasha");
}
}
Полный код [17]
Теперь не происходит никакого чтения файла. Вместо этого, мок отдает настроенное в тесте значение.
Использование моков имеет свои преимущества:
и недостатки:
Существует более простой и удобный способ создания моков — использовать специальную аннотацию @Mock
:
@Mock File file;
Есть три способа инициализировать такие моки:
@Before
public void setUp() {
Mockito.initMocks(this);
}
@RunWith(MockitoJUnitRunner.class)
@Rule public final MockitoRule rule = MockitoJUnit.rule();
Второй вариант максимально декларативен и компактен, но требует использования специального раннера тестов, что не всегда удобно. Последний вариант лишен этого недостатка и более декларативен, чем использование метода initMocks()
.
@RunWith(MockitoJUnitRunner.class)
public class NameRepositoryTest {
@Mock FileReader fileReader;
NameRepository nameRepository;
@Before
public void setUp() throws IOException {
when(fileReader.readFile()).thenReturn("{name : Sasha}");
nameRepository = new NameRepository(fileReader);
}
@Test
public void getName_isSasha() throws Exception {
String name = nameRepository.getName();
assertThat(name).isEqualTo("Sasha");
}
}
Полный код [21]
Android тесты можно поделить на два типа: те, что можно запускать на обычной Java VM, и те, что необходимо запускать на Android Java VM. Давайте посмотрим на оба типа тестов.
Тесты для кода, не требующего работы компонентов Android API, для работы которых нужен Android-эмулятор или реальное устройство, можно запускать прямо на вашем компьютере и на любой Java-машине. Преимущественно это юнит-тесты бизнес-логики, которые тестируют изолированно отдельно взятый класс. Гораздо реже пишутся интеграционные тесты, так как далеко не всегда есть возможность создать реальные объекты классов, с которыми взаимодействует тестируемый класс.
Чтобы написать класс с Host Java тестами нужно, чтобы java файл имел путь ${moduleName}/src/test/java/...
. Также с помощью @RunWith
аннотации указать Runner
, который отвечает за запуск тестов, корректный вызов и обработку всех методов:
@RunWith(MockitoJUnitRunner.class)
public class TestClass {...}
Использование этих тестов имеет множество преимуществ:
с другой стороны, этими тестами:
Для того, чтобы была возможность использовать Android API классы в Host Java тестах, существует библиотека Robolectric [23], которая эмулирует среду Android и дает доступ к ее основным функциям. Однако, тестирование классов Android с Roboelectric часто работает нестабильно: нужно время, пока Robolectric будет поддерживать последнее API Android, существуют проблемы с получением ресурсов и т.д. Поэтому реальные классы почти не используются, а используются их моки для юнит-тестирования.
Для запуска тестов с помощью Roboelectric нужно установить кастомный TestRunner [24]. В нем можно настроить версию SDK (самая последняя стабильная версия — 23), обозначить основной класс Application
и другие параметры для эмулированной среды Android.
public class MainApplication extends Application {}
Полный код [25]
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 21, application = MainApplication.class)
public class MainApplicationTest {
@Test
public void packageName() {
assertThat(RuntimeEnvironment.application)
.isInstanceOf(MainApplication.class);
}
}
Полный код [26]
Для инструментальных тестов наличие устройства или эмулятора обязательно, так как мы будем тестировать нажатие кнопок, ввод текста, и другие действия.
Чтобы написать тест для Android Java VM нужно положить java файл по пути ${moduleName}/src/androidTest/java/...
, а также с помощью @RunWith
аннотации указать AndroidJUnit4
, который позволит запускать тесты на устройстве Android.
@RunWith(AndroidJUnit4.class)
public class TestClass {...}
Для тестирования UI используется фреймворк Espresso [27], который предоставляет API для тестирования пользовательского интерфейса программы. В Espresso тесты работают в бэкграунд потоке, а взаимодействие с UI элементами в потоке UI. Espresso имеет несколько основных классов для тестирования:
Напишем простейшее Android-приложение, которое и будем тестировать:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
}
}
Полный код [28]
Протестируем наше приложение. При тестировании UI прежде всего нужно запустить Activity. Для этого существует ActivityTestRule [29], которое запускает Activity перед каждым тестом и закрывает после:
@Rule public ActivityTestRule<MainActivity> activityTestRule =
new ActivityTestRule<>(MainActivity.class);
Напишем простой тест, проверяющий, что элемент с id R.id.container
показан на экране:
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule<MainActivity> activityTestRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void checkContainerIsDisplayed() {
onView(ViewMatchers.withId(R.id.container))
.check(matches(isDisplayed()));
}
}
Полный код [30]
Эмулятор на слабых или загруженных машинах может работать медленно. Поэтому между запуском эмулятора и окончанием билда с установкой приложения на эмулятор может пройти достаточно времени для того, чтобы экран заблокировался от бездействия. Таким образом тест может быть запущен при заблокированном экране, что вызовет ошибку java.lang.RuntimeException: Could not launch activity within 45 seconds
. Поэтому перед запуском Activity нужно разблокировать и включить экран. Раз это нужно делать в каждом UI тесте, для избежания дублирования кода создадим правило, которое будет разблокировать и включать экран перед тестом:
class UnlockScreenRule<A extends AppCompatActivity> implements TestRule {
ActivityTestRule<A> activityRule;
UnlockScreenRule(ActivityTestRule<A> activityRule) {
this.activityRule = activityRule;
}
@Override
public Statement apply(Statement statement, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
activityRule.runOnUiThread(() -> activityRule
.getActivity()
.getWindow()
.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON));
statement.evaluate();
}
};
}
}
Полный код [31]
Напишем кастомное ActivityTestRule
, которое разблокирует экран эмулятора и запустит активити перед запуском тестов:
public class ActivityTestRule<A extends AppCompatActivity> implements TestRule {
private final android.support.test.rule.ActivityTestRule<A> activityRule;
private final RuleChain ruleChain;
public ActivityTestRule(Class<A> activityClass) {
this.activityRule = new ActivityTestRule<>(activityClass, true, true);
ruleChain = RuleChain
.outerRule(activityRule)
.around(new UnlockScreenRule(activityRule));
}
public android.support.test.rule.ActivityTestRule<A> getActivityRule() {
return activityRule;
}
public void runOnUiThread(Runnable runnable) throws Throwable {
activityRule.runOnUiThread(runnable);
}
public A getActivity() {
return activityRule.getActivity();
}
@Override
public Statement apply(Statement statement, Description description) {
return ruleChain.apply(statement, description);
}
}
Полный код [32]
Используя это правило вместо стандартного можно сильно снизить число случайных падений UI тестов в CI.
Обычно верстка и логика UI приложения не кладется вся в активити, а разбивается на окна, для каждого из которых создается фрагмент. Давайте создадим простой фрагмент для вывода на экран имени с помощью NameRepository
:
public class UserFragment extends Fragment {
private TextView textView;
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
textView = new TextView(getActivity());
try {
textView.setText(createNameRepository().getName());
} catch (IOException exception) {
throw new RuntimeException(exception);
}
return textView;
}
private NameRepository createNameRepository() {
return new NameRepository(
new FileReader(
new File(
getContext().getFilesDir().getAbsoluteFile()
+ File.separator
+ "test_file")));
}
@Override
public void onDestroyView() {
super.onDestroyView();
textView = null;
}
}
Полный код [33]
При открытии фрагмента UI может зависнуть на некоторое время, а если используются анимации переходов между фрагментами, тест может начаться до появления фрагмента. Поэтому нужно не просто открыть фрагмент, а дождаться, когда он будет запущен. Для ожидания результата выполнения действий отлично подходит библиотека Awaitility [34], которая имеет очень простой и понятный синтаксис. Напишем правило, запускающее фрагмент и ожидающее его запуска с помощью этой библиотеки:
class OpenFragmentRule<A extends AppCompatActivity> implements TestRule {
private final ActivityTestRule<A> activityRule;
private final Fragment fragment;
OpenFragmentRule(ActivityTestRule<A> activityRule, Fragment fragment) {
this.activityRule = activityRule;
this.fragment = fragment;
}
@Override
public Statement apply(Statement statement, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
openFragment(fragment);
await().atMost(5, SECONDS).until(fragment::isResumed);
statement.evaluate();
}
};
}
}
Полный код [35]
В данном случае выражение означает, что если в течении пяти секунд фрагмент не запустится, то тест не будет пройден. Нужно отметить, что как только фрагмент запустится, тест сразу же продолжит выполнение и не будет ждать все пять секунд.
Аналогично правилу, которое запускает активити, логично создать правило, которое запускает фрагмент:
public class FragmentTestRule<A extends AppCompatActivity, F extends Fragment>
implements TestRule {
private ActivityTestRule<A> activityRule;
private F fragment;
private RuleChain ruleChain;
public FragmentTestRule(Class<A> activityClass, F fragment) {
this.fragment = fragment;
this.activityRule = new ActivityTestRule<>(activityClass);
ruleChain = RuleChain
.outerRule(activityRule)
.around(new OpenFragmentRule<>(activityRule, fragment));
}
public ActivityTestRule<A> getActivityRule() {
return activityRule;
}
public F getFragment() {
return fragment;
}
public void runOnUiThread(Runnable runnable) throws Throwable {
activityRule.runOnUiThread(runnable);
}
public A getActivity() {
return activityRule.getActivity();
}
@Override
public Statement apply(Statement statement, Description description) {
return ruleChain.apply(statement, description);
}
}
Полный код [36]
Тест фрагмента с использованием этого правила будет выглядеть следующим образом:
@RunWith(AndroidJUnit4.class)
public class UserFragmentTest {
@Rule
public final RuleChain rules = RuleChain
.outerRule(new CreateFileRule(getTestFile(), "{name : Sasha}"))
.around(new FragmentTestRule<>(MainActivity.class, new UserFragment()));
@Test
public void nameDisplayed() {
onView(withText("Sasha")).check(matches(isDisplayed()));
}
private File getTestFile() {
return new File(
InstrumentationRegistry.getTargetContext()
.getFilesDir()
.getAbsoluteFile() + File.separator + "test_file");
}
}
Полный код [37]
Так как операции с диском, а именно получение имени из файла, может выполняться сравнительно долго, то следует эту операцию выполнять асинхронно. Для асинхронного получения имени из файла используем библиотеку RxJava [38]. Можно уверенно сказать, что RxJava сейчас используется в большинстве Android приложений. Практически каждая задача, которую нужно выполнить асинхронно, выполняется с помощью RxJava, потому что это пожалуй одна из самых удобных и понятных библиотек для асинхронного выполнения кода.
Изменим наш репозиторий так, чтобы он работал асинхронно:
public class NameRepository {
...
public Single<String> getName() {
return Single.create(
emitter -> {
Gson gson = new Gson();
emitter.onSuccess(
gson.fromJson(fileReader.readFile(), User.class).getName());
});
}
}
Полный код [39]
Для тестирования RX-кода существует специальный класс TestObserver
, который автоматически подпишется на Observable
и мгновенно получит результат. Тест репозитория будет выглядеть следующим образом:
@RunWith(MockitoJUnitRunner.class)
public class NameRepositoryTest {
...
@Test
public void getName() {
TestObserver<String> observer = nameRepository.getName().test();
observer.assertValue("Sasha");
}
}
Полный код [40]
Обновим наш фрагмент, используя новый реактивный репозиторий:
public class UserFragment extends Fragment {
...
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
textView = new TextView(getActivity());
createNameRepository()
.getName()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(name -> textView.setText(name));
return textView;
}
}
Полный код [41]
Так как теперь имя получается асинхронно, то для проверки результата работы нужно дождаться завершения асинхронного действия с помощью Awaitility:
@RunWith(AndroidJUnit4.class)
public class UserFragmentTest {
...
@Test
public void nameDisplayed() {
await()
.atMost(5, SECONDS)
.ignoreExceptions()
.untilAsserted(
() ->
onView(ViewMatchers.withText("Sasha"))
.check(matches(isDisplayed())));
}
}
Полный код [42]
Когда во фрагменте или активити выполняются асинхронные действия, в данном случае — чтение имени из файла, нужно иметь ввиду, что фрагмент может быть закрыт пользователем до того, как асинхронное действие выполнится. В текущей версии фрагмента допущена ошибка, так как если при выполнении асинхронной операции фрагмент будет уже закрыт, то textView
будет уже удален и равен null
. Чтобы не допустить краша приложения с NullPointerException
при доступе к textView
в subscribe()
, остановим асинхронное действие при закрытии фрагмента:
public class UserFragment extends Fragment {
private TextView textView;
private Disposable disposable;
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
textView = new TextView(getActivity());
disposable =
createNameRepository()
.getName()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(name -> textView.setText(name));
return textView;
}
@Override
public void onDestroyView() {
super.onDestroyView();
disposable.dispose();
textView = null;
}
}
Полный код [43]
Для тестирования подобных ошибок, связанных с асинхронными действиям во фрагменте, нужно закрыть фрагмент сразу же после его открытия. Это можно сделать просто заменив его на другой фрагмент. Тогда при завершении асинхронного действия onCreateView
в закрытом фрагменте textView
будет null
и если допустить ошибку и не отменить подписку, приложение упадет. Напишем правило для тестирования на эту ошибку:
public class FragmentAsyncTestRule<A extends AppCompatActivity>
implements TestRule {
private final ActivityTestRule<A> activityRule;
private final Fragment fragment;
public FragmentAsyncTestRule(Class<A> activityClass, Fragment fragment) {
this.activityRule = new ActivityTestRule<>(activityClass);
this.fragment = fragment;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
base.evaluate();
} finally {
activityRule.launchActivity(new Intent());
openFragment(fragment);
openFragment(new Fragment());
}
}
};
}
}
Полный код [44]
Добавим это правило в класс тестов фрагмента:
@RunWith(AndroidJUnit4.class)
public class UserFragmentTest {
@ClassRule
public static TestRule asyncRule =
new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment());
...
}
Полный код [45]
Теперь тест упадет, если асинхронные действия будут обращаться к полям фрагмента после его завершения.
Создадим презентер, куда мы вынесем логику подписки на возвращаемый репозиторием Observable
из фрагмента, а также добавим timeout
для получения имени из файла:
public class UserPresenter {
public interface Listener {
void onUserNameLoaded(String name);
void onGettingUserNameError(String message);
}
private final Listener listener;
private final NameRepository nameRepository;
public UserPresenter(Listener listener, NameRepository nameRepository) {
this.listener = listener;
this.nameRepository = nameRepository;
}
public void getUserName() {
nameRepository
.getName()
.timeout(2, SECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
listener::onUserNameLoaded,
error -> listener.onGettingUserNameError(error.getMessage()));
}
}
Полный код [46]
В данном случае при тестировании презентера уже нужно протестировать конечный результат подписки, которая получает данные асинхронно. Напишем наивную версию такого теста:
@RunWith(RobolectricTestRunner.class)
public class UserPresenterTest {
@Rule public final MockitoRule rule = MockitoJUnit.rule();
@Mock UserPresenter.Listener listener;
@Mock NameRepository nameRepository;
UserPresenter presenter;
@Before
public void setUp() {
when(nameRepository.getName()).thenReturn(Observable.just("Sasha"));
presenter = new UserPresenter(listener, nameRepository);
}
@Test
public void getUserName() {
presenter.getUserName();
verifyNoMoreInteractions(listener);
}
}
Полный код [47]
В данном тесте презентер не вызовет никакой метод объекта listener
, так как тест проходит прежде, чем выполняется асинхронное действие. В тестах на эмуляторе Awaitility решает эту проблему. В юнит-тестах тестирование асинхронной природы кода не совсем к месту, а потому в них можно заменить стандартные RxJava Schedulers
на синхронные. Используем для этого TestScheduler [48], который позволяет произвольно установить время, которое якобы прошло с момента подписки на Observable
, чтобы протестировать корректную установку таймаута. Как обычно, напишем для этого правило:
public class RxImmediateSchedulerRule implements TestRule {
private static final TestScheduler TEST_SCHEDULER = new TestScheduler();
private static final Scheduler IMMEDIATE_SCHEDULER = new Scheduler() {
@Override
public Disposable scheduleDirect(Runnable run, long delay, TimeUnit unit) {
return super.scheduleDirect(run, 0, unit);
}
@Override
public Worker createWorker() {
return new ExecutorScheduler.ExecutorWorker(Runnable::run);
}
};
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
RxJavaPlugins.setIoSchedulerHandler(scheduler -> TEST_SCHEDULER);
RxJavaPlugins.setComputationSchedulerHandler(
scheduler -> TEST_SCHEDULER);
RxJavaPlugins.setNewThreadSchedulerHandler(
scheduler -> TEST_SCHEDULER);
RxAndroidPlugins.setMainThreadSchedulerHandler(
scheduler -> IMMEDIATE_SCHEDULER);
try {
base.evaluate();
} finally {
RxJavaPlugins.reset();
RxAndroidPlugins.reset();
}
}
};
}
public TestScheduler getTestScheduler() {
return TEST_SCHEDULER;
}
}
Полный код [49]
Тест презентера с новым правилом будет выглядеть следующим образом:
@RunWith(RobolectricTestRunner.class)
public class UserPresenterTest {
static final int TIMEOUT_SEC = 2;
static final String NAME = "Sasha";
@Rule public final MockitoRule rule = MockitoJUnit.rule();
@Rule public final RxImmediateSchedulerRule timeoutRule =
new RxImmediateSchedulerRule();
@Mock UserPresenter.Listener listener;
@Mock NameRepository nameRepository;
PublishSubject<String> nameObservable = PublishSubject.create();
UserPresenter presenter;
@Before
public void setUp() {
when(nameRepository.getName()).thenReturn(nameObservable.firstOrError());
presenter = new UserPresenter(listener, nameRepository);
}
@Test
public void getUserName() {
presenter.getUserName();
timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC - 1, SECONDS);
nameObservable.onNext(NAME);
verify(listener).onUserNameLoaded(NAME);
}
@Test
public void getUserName_timeout() {
presenter.getUserName();
timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC + 1, SECONDS);
nameObservable.onNext(NAME);
verify(listener).onGettingUserNameError(any());
}
}
Полный код [50]
Для облегчения работы с графом зависимостей объектов отлично подходит паттерн Dependency Injection. Dagger 2 [51] — это библиотека, которая поможет в реализации этого паттерна. Поэтому в большинстве наших Android приложений все компоненты предоставляются с помощью Dagger. Об использовании и преимуществах этой библиотеки можно написать отдельную статью, а тут мы рассмотрим, как тестировать приложения, её использующие.
Начнем с того, что практически всегда при использовании Dagger существует ApplicationComponent
, который предоставляет все основные зависимости приложения, и инициализируется в классе приложения Application
, который, в свою очередь, имеет метод для получения этого компонента.
@Singleton
@Component(modules = {ContextModule.class})
public interface ApplicationComponent {
UserComponent createUserComponent();
}
Полный код [52]
public class MainApplication extends Application {
private ApplicationComponent component;
@Override
public void onCreate() {
super.onCreate();
component = DaggerApplicationComponent.builder()
.contextModule(new ContextModule(this))
.build();
}
public ApplicationComponent getComponent() {
return component;
}
}
Полный код [53]
Также создадим Dagger модуль, который будет предоставлять репозиторий:
@Module
public class UserModule {
@Provides
NameRepository provideNameRepository(@Private FileReader fileReader) {
return new NameRepository(fileReader);
}
@Private
@Provides
FileReader provideFileReader(@Private File file) {
return new FileReader(file);
}
@Private
@Provides
File provideFile(Context context) {
return new File(context.getFilesDir().getAbsoluteFile()
+ File.separator
+ "test_file");
}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
private @interface Private {}
}
Полный код [54]
Изменим фрагмент следующим образом, чтобы репозиторий получать с помощью Dagger:
public class UserFragment extends Fragment {
...
@Inject NameRepository nameRepository;
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
((MainApplication) getActivity().getApplication())
.getComponent()
.createUserComponent()
.injectsUserFragment(this);
textView = new TextView(getActivity());
disposable = nameRepository
.getName()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(name -> textView.setText(name));
return textView;
}
}
Полный код [55]
Помимо функциональных тестов UI хорошо иметь и unit-тесты с замоканными зависимостями. Чтобы предоставлять мокированные объекты с помощью Dagger, нужно заменить ApplicationComponent
на специально созданный компонент для тестов. В первую очередь создадим метод для подмены основного компонента в Application
:
public void setComponentForTest(ApplicationComponent component) {
this.component = component;
}
Полный код [53]
Чтобы не заменять компонент в каждом классе с тестами фрагментов, создадим для этого правило:
class TestDaggerComponentRule<A extends AppCompatActivity> implements TestRule {
private final ActivityTestRule<A> activityRule;
private final ApplicationComponent component;
TestDaggerComponentRule(
ActivityTestRule<A> activityRule, ApplicationComponent component) {
this.activityRule = activityRule;
this.component = component;
}
@Override
public Statement apply(Statement statement, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
MainApplication application =
((MainApplication) activityRule.getActivity().getApplication());
ApplicationComponent originalComponent = application.getComponent();
application.setComponentForTest(component);
try {
statement.evaluate();
} finally {
application.setComponentForTest(originalComponent);
}
}
};
}
}
Полный код [56]
Отметим, что нужно вернуть оригинальный компонент после теста, так как Application создается один для всех тестов и стоит возвращать его к дефолтному состоянию после каждого. Теперь создадим правило, которое будет проводить все подготовки к тестированию фрагмента описанные выше. Перед каждым тестом будет разблокирован экран, запущено активити, открыт нужный нам фрагмент и установлен тестовый Dagger компонент, предоставляющий моки зависимостей.
public class FragmentTestRule<A extends AppCompatActivity, F extends Fragment>
implements TestRule {
private ActivityTestRule<A> activityRule;
private F fragment;
private RuleChain ruleChain;
public FragmentTestRule(
Class<A> activityClass, F fragment, ApplicationComponent component) {
this.fragment = fragment;
this.activityRule = new ActivityTestRule<>(activityClass);
ruleChain = RuleChain
.outerRule(activityRule)
.around(new TestDaggerComponentRule<>(activityRule, component))
.around(new OpenFragmentRule<>(activityRule, fragment));
}
...
}
Полный код [36]
Установим тестовый компонент в тесте нашего фрагмента:
@RunWith(AndroidJUnit4.class)
public class UserFragmentTest {
...
@Rule
public final FragmentTestRule<MainActivity, UserFragment> fragmentRule =
new FragmentTestRule<>(
MainActivity.class,
new UserFragment(),
createTestApplicationComponent());
private ApplicationComponent createTestApplicationComponent() {
ApplicationComponent component = mock(ApplicationComponent.class);
when(component.createUserComponent())
.thenReturn(DaggerUserFragmentTest_TestUserComponent.create());
return component;
}
@Singleton
@Component(modules = {TestUserModule.class})
interface TestUserComponent extends UserComponent {}
@Module
static class TestUserModule {
@Provides
public NameRepository provideNameRepository() {
NameRepository nameRepository = mock(NameRepository.class);
when(nameRepository.getName()).thenReturn(
Single.fromCallable(() -> "Sasha"));
return nameRepository;
}
}
}
Полный код [57]
Бывает, что необходимо добавить логику иди элементы UI, которые нужны разработчикам для более удобного тестирования и должны отображаться только если приложение собирается в режиме debug. Давайте для примера сделаем, чтобы в debug сборке презентер не только передавал имя подписчику, но и выводил его в лог:
class UserPresenter {
...
public void getUserName() {
nameRepository
.getName()
.timeout(TIMEOUT_SEC, SECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
name -> {
listener.onUserNameLoaded(name);
if (BuildConfig.DEBUG) {
logger.info(String.format("Name loaded: %s", name));
}
}, error -> listener.onGettingUserNameError(error.getMessage()));
}
}
Полный код [58]
Эту логику тоже нужно тестировать, но тесты должны запускаться только при соответствующем типе сборки приложения. Напишем правило DebugTestRule
, которое будет проверять тип сборки приложения и запускать тесты только для дебаг версии:
public class DebugRule implements TestRule {
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
if (BuildConfig.DEBUG) {
base.evaluate();
}
}
};
}
}
Полный код [59]
Тест с этим правилом будет выглядеть следующим образом:
class UserPresenterDebugTest {
...
@Rule public final DebugTestsRule debugRule = new DebugTestsRule();
@Test
public void userNameLogged() {
presenter.getUserName();
timeoutRule.getTestScheduler().triggerActions();
nameObservable.onNext(NAME);
verify(logger).info(contains(NAME));
}
}
Полный код [60]
В этой статье мы разобрались с базовыми библиотеками для написания тестов и разработали набор инструментов, основанных на TestRule [6] и предназначенных для решения проблем запуска активити и фрагментов, работой с асинхронным кодом, даггером, отладочным кодом и эмулятором андроида. Применение этих инструментов позволило протестировать неочевидные проблемы, снизить дублирование кода и в целом повысить читабельность тестов.
Полный пример приложения и тестов, использующих все вышеперечисленные библиотеки и утилиты.
public class NameRepository {
private final FileReader fileReader;
public NameRepository(FileReader fileReader) {
this.fileReader = fileReader;
}
public Single<String> getName() {
return Single.create(
emitter -> {
Gson gson = new Gson();
emitter.onSuccess(
gson.fromJson(fileReader.readFile(), User.class).name);
});
}
private static final class User {
String name;
}
}
Полный код [61]
@RunWith(MockitoJUnitRunner.class)
public class NameRepositoryTest {
@Mock FileReader fileReader;
NameRepository nameRepository;
@Before
public void setUp() throws IOException {
when(fileReader.readFile()).thenReturn("{name : Sasha}");
nameRepository = new NameRepository(fileReader);
}
@Test
public void getName() {
TestObserver<String> observer = nameRepository.getName().test();
observer.assertValue("Sasha");
}
}
Полный код [62]
public class UserPresenter {
public interface Listener {
void onUserNameLoaded(String name);
void onGettingUserNameError(String message);
}
private final Listener listener;
private final NameRepository nameRepository;
private final Logger logger;
private Disposable disposable;
public UserPresenter(
Listener listener, NameRepository nameRepository, Logger logger) {
this.listener = listener;
this.nameRepository = nameRepository;
this.logger = logger;
}
public void getUserName() {
disposable =
nameRepository
.getName()
.timeout(2, SECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
name -> {
listener.onUserNameLoaded(name);
if (BuildConfig.DEBUG) {
logger.info(String.format("Name loaded: %s", name));
}
},
error -> listener.onGettingUserNameError(error.getMessage()));
}
public void stopLoading() {
disposable.dispose();
}
}
Полный код [63]
@RunWith(RobolectricTestRunner.class)
public class UserPresenterTest {
static final int TIMEOUT_SEC = 2;
static final String NAME = "Sasha";
@Rule public final MockitoRule rule = MockitoJUnit.rule();
@Rule public final RxImmediateSchedulerRule timeoutRule =
new RxImmediateSchedulerRule();
@Mock UserPresenter.Listener listener;
@Mock NameRepository nameRepository;
@Mock Logger logger;
PublishSubject<String> nameObservable = PublishSubject.create();
UserPresenter presenter;
@Before
public void setUp() {
when(nameRepository.getName()).thenReturn(nameObservable.firstOrError());
presenter = new UserPresenter(listener, nameRepository, logger);
}
@Test
public void getUserName() {
presenter.getUserName();
timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC - 1, SECONDS);
nameObservable.onNext(NAME);
verify(listener).onUserNameLoaded(NAME);
}
@Test
public void getUserName_timeout() {
presenter.getUserName();
timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC + 1, SECONDS);
nameObservable.onNext(NAME);
verify(listener).onGettingUserNameError(any());
}
}
Полный код [64]
@RunWith(RobolectricTestRunner.class)
public class UserPresenterDebugTest {
private static final String NAME = "Sasha";
@Rule public final DebugRule debugRule = new DebugRule();
@Rule public final MockitoRule mockitoRule = MockitoJUnit.rule();
@Rule public final RxImmediateSchedulerRule timeoutRule =
new RxImmediateSchedulerRule();
@Mock UserPresenter.Listener listener;
@Mock NameRepository nameRepository;
@Mock Logger logger;
PublishSubject<String> nameObservable = PublishSubject.create();
UserPresenter presenter;
@Before
public void setUp() {
when(nameRepository.getName()).thenReturn(nameObservable.firstOrError());
presenter = new UserPresenter(listener, nameRepository, logger);
}
@Test
public void userNameLogged() {
presenter.getUserName();
timeoutRule.getTestScheduler().triggerActions();
nameObservable.onNext(NAME);
verify(logger).info(contains(NAME));
}
}
Полный код [65]
public class UserFragment extends Fragment implements UserPresenter.Listener {
private TextView textView;
@Inject UserPresenter userPresenter;
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
((MainApplication) getActivity().getApplication())
.getComponent()
.createUserComponent(new UserModule(this))
.injectsUserFragment(this);
textView = new TextView(getActivity());
userPresenter.getUserName();
return textView;
}
@Override
public void onUserNameLoaded(String name) {
textView.setText(name);
}
@Override
public void onGettingUserNameError(String message) {
textView.setText(message);
}
@Override
public void onDestroyView() {
super.onDestroyView();
userPresenter.stopLoading();
textView = null;
}
}
Полный код [66]
@RunWith(AndroidJUnit4.class)
public class UserFragmentIntegrationTest {
@ClassRule
public static TestRule asyncRule =
new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment());
@Rule
public final RuleChain rules = RuleChain
.outerRule(new CreateFileRule(getTestFile(), "{name : Sasha}"))
.around(new FragmentTestRule<>(MainActivity.class, new UserFragment()));
@Test
public void nameDisplayed() {
await()
.atMost(5, SECONDS)
.ignoreExceptions()
.untilAsserted(
() ->
onView(ViewMatchers.withText("Sasha"))
.check(matches(isDisplayed())));
}
private static File getTestFile() {
return new File(
InstrumentationRegistry.getTargetContext()
.getFilesDir()
.getAbsoluteFile() + File.separator + "test_file");
}
}
Полный код [67]
@RunWith(AndroidJUnit4.class)
public class UserFragmentTest {
@ClassRule
public static TestRule asyncRule =
new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment());
@Rule
public final FragmentTestRule<MainActivity, UserFragment> fragmentRule =
new FragmentTestRule<>(
MainActivity.class,
new UserFragment(),
createTestApplicationComponent());
@Test
public void getNameMethodCalledOnCreate() {
verify(fragmentRule.getFragment().userPresenter).getUserName();
}
private ApplicationComponent createTestApplicationComponent() {
ApplicationComponent component = mock(ApplicationComponent.class);
when(component.createUserComponent(any(UserModule.class)))
.thenReturn(DaggerUserFragmentTest_TestUserComponent.create());
return component;
}
@Singleton
@Component(modules = {TestUserModule.class})
interface TestUserComponent extends UserComponent {}
@Module
static class TestUserModule {
@Provides
public UserPresenter provideUserPresenter() {
return mock(UserPresenter.class);
}
}
}
Полный код [68]
Статья написана в коллаборации с Evgeny Aseev [69]. Он же написал значительную часть кода наших библиотек. Спасибо за ревью текста статьи и кода — Andrei Tarashkevich [70], Ruslan Login [71]. Спасибо спонсору проекта, компании AURA Devices.
Автор: Monnoroch
Источник [72]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/276500
Ссылки в тексте:
[1] репозитории: https://github.com/Monnoroch/android-testing
[2] полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/user/NameRepository.java
[3] JUnit: http://junit.org/junit4/javadoc/4.12/overview-summary.html
[4] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/baseline/NameRepositoryTest.java
[5] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/beforeafter/NameRepositoryTest.java
[6] TestRule: https://developer.android.com/reference/android/support/test/rule/package-summary.html
[7] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/rules/CreateFileRule.java
[8] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/filerule/NameRepositoryTest.java
[9] RuleChain: http://junit.org/junit4/javadoc/4.12/org/junit/rules/RuleChain.html
[10] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/rules/CreateDirRule.java
[11] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/rulechain/NameRepositoryTest.java
[12] Google Truth: https://github.com/google/truth
[13] JUnit Assert: http://junit.sourceforge.net/javadoc/org/junit/Assert.html
[14] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/truth/NameRepositoryTest.java
[15] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/common/FileReader.java
[16] Mockito: http://site.mockito.org
[17] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/mockito/NameRepositoryTest.java
[18] Mockito.initMocks(): https://static.javadoc.io/org.mockito/mockito-core/2.2.28/org/mockito/MockitoAnnotations.html#initMocks(java.lang.Object)
[19] MockitoJUnitRunner: https://static.javadoc.io/org.mockito/mockito-core/2.2.28/org/mockito/junit/MockitoJUnitRunner.html
[20] MockitoRule: https://static.javadoc.io/org.mockito/mockito-core/2.6.5/org/mockito/junit/MockitoRule.html
[21] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/mockitorunner/NameRepositoryTest.java
[22] Continuous integration: https://en.wikipedia.org/wiki/Continuous_integration
[23] Robolectric: http://robolectric.org/getting-started/
[24] TestRunner: http://junit.sourceforge.net/junit3.8.1/javadoc/junit/textui/TestRunner.html
[25] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/robolectric/MainApplication.java
[26] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/MainApplicationTest.java
[27] Espresso: https://developer.android.com/training/testing/ui-testing/espresso-testing.html
[28] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/MainActivity.java
[29] ActivityTestRule: https://developer.android.com/reference/android/support/test/rule/ActivityTestRule.html
[30] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/activity/activityrule/MainActivityTest.java
[31] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/rules/UnlockScreenRule.java
[32] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/rules/ActivityTestRule.java
[33] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/user/UserFragment.java
[34] Awaitility: https://github.com/awaitility/awaitility
[35] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/rules/OpenFragmentRule.java
[36] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/rules/FragmentTestRule.java
[37] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/user/UserFragmentTest.java
[38] RxJava: https://github.com/ReactiveX/RxJava
[39] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/user/rx/NameRepository.java
[40] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/rx/NameRepositoryTest.java
[41] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/user/awaitility/UserFragment.java
[42] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/user/awaitility/UserFragmentTest.java
[43] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/user/async/UserFragment.java
[44] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/rules/FragmentAsyncTestRule.java
[45] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/user/async/UserFragmentTest.java
[46] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/user/rx/timeout/UserPresenter.java
[47] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/rx/timeout/withoutrule/UserPresenterTest.java
[48] TestScheduler: http://reactivex.io/RxJava/javadoc/io/reactivex/schedulers/TestScheduler.html
[49] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/rules/RxImmediateSchedulerRule.java
[50] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/rx/timeout/UserPresenterTest.java
[51] Dagger 2: https://google.github.io/dagger/
[52] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/ApplicationComponent.java
[53] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/MainApplication.java
[54] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/user/UserModule.java
[55] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/user/dagger/UserFragment.java
[56] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/rules/TestDaggerComponentRule.java
[57] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/androidTest/java/com/testing/user/dagger/UserFragmentTest.java
[58] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/main/java/com/testing/user/debug/UserPresenter.java
[59] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/rules/DebugRule.java
[60] Полный код: https://github.com/Monnoroch/android-testing/blob/master/testing/src/test/java/com/testing/user/debug/UserPresenterDebugTest.java
[61] Полный код: https://github.com/Monnoroch/android-testing/blob/master/example/src/main/java/com/example/user/NameRepository.java
[62] Полный код: https://github.com/Monnoroch/android-testing/blob/master/example/src/test/java/com/example/user/NameRepositoryTest.java
[63] Полный код: https://github.com/Monnoroch/android-testing/blob/master/example/src/main/java/com/example/user/UserPresenter.java
[64] Полный код: https://github.com/Monnoroch/android-testing/blob/master/example/src/test/java/com/example/user/UserPresenterTest.java
[65] Полный код: https://github.com/Monnoroch/android-testing/blob/master/example/src/test/java/com/example/user/UserPresenterDebugTest.java
[66] Полный код: https://github.com/Monnoroch/android-testing/blob/master/example/src/main/java/com/example/user/UserFragment.java
[67] Полный код: https://github.com/Monnoroch/android-testing/blob/master/example/src/androidTest/java/com/example/user/UserFragmentIntegrationTest.java
[68] Полный код: https://github.com/Monnoroch/android-testing/blob/master/example/src/androidTest/java/com/example/user/UserFragmentTest.java
[69] Evgeny Aseev: https://github.com/AseevEIDev
[70] Andrei Tarashkevich: https://github.com/andrewtar
[71] Ruslan Login: https://www.linkedin.com/in/ruslan-login-68bb2676/
[72] Источник: https://habrahabr.ru/post/352334/?utm_campaign=352334
Нажмите здесь для печати.