Testing Android Applications

    Testing is one of the most important parts of developing quality software products. Today we will talk about some methodologies and libraries developed and used by our team to write tests of Android applications.



    Let's start with the most basic things, because more experienced developers can go directly to the section on tools for UI testing. For those who want to learn or refresh basic things - enjoy reading.


    Create the first test


    Let's create a small component, which we will test. It parses the file with the JSON object containing the name, and returns the resulting string:


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

    Here and in the future I will provide an abridged version of the code. The full version can be viewed in the repository . A link to the full code will be attached to each snippet .


    Now write the first JUnit test. JUnit is a Java library for writing tests. In order for JUnit to know that the method is a test, you need to add an annotation to it @Test. JUnit contains a class Assertthat allows you to compare the actual values ​​with the expected ones and generates an error if the values ​​do not match. This test will test the correctness of our component, namely reading the file, parsing JSON and getting the correct field:


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

    Full code


    Libraries for writing tests


    Tests are also code that needs to be supported. Moreover, the test code should be easy to understand so that it can be verified in the mind. Therefore, it makes sense to invest in simplifying the test code, getting rid of duplication, and increasing readability. Let's look at the widely used libraries that will help us in this matter.


    In order not to duplicate the preparation code in each test, there are annotations @Beforeand @After. Methods marked with annotation @Beforewill be executed before each test, and methods marked with annotation will be performed @Afterafter each test. There are also annotations @BeforeClassand @AfterClass, which are executed respectively before and after all tests in the class. Let's redo our test using the following methods:


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

    Full code


    We were able to remove duplication of the setup code for each test. However, many different classes with tests may require the creation of a file, and this duplication would also be desirable to remove. To do this, there is a library of test rules ( TestRule ). The test rule performs a function similar to @Beforeand @After. In the apply () method of this class, we can perform the actions we need before and after each or all tests are completed. In addition to reducing code duplication, the advantage of this method is that the code is taken out of the test class, which reduces the amount of code in the test and makes it easier to read. Let's write a rule for creating a file:


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

    Full code


    We use this rule in our test. In order for actions to be TestRuleperformed for each test, you need to mark it with TestRuleannotation @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");
      }
      ...
    }
    

    Full code


    If the rule is marked with annotation @ClassRule, then actions will not be called before each test, but once before all tests in the class, similarly to @BeforeClassand annotations @AfterClass.


    When several are used in tests TestRule, it may be necessary that they run in a certain order, for this there is a RuleChain with which you can determine the order of launch of ours TestRule. Create a rule that should create a folder before the file is created:


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

    Full code


    With this rule, the test class will look like this:


    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");
      }
      ...
    }

    Full code


    Now, in each test, a directory will be created before creating the file and deleted after deleting the file.


    Google Truth is a library for improving the readability of test code. It contains assert methods (similar to JUnit Assert ), but more human-readable, and also includes much more options for checking parameters. This is what the previous test using Truth looks like:


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

    Full code


    It can be seen that the code reads almost like text in spoken English.


    Our component does two different things: it reads a file and parses it. To adhere to the principle of sole responsibility, let's separate the logic for reading a file into a separate component:


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

    Full code


    Now we want to test it NameRepository, but in fact we are testing reading the file in FileReader. To avoid this and thereby increase the insulation, reliability and speed of the test, we can replace the real one FileReaderwith its mock.


    Mockito is a library for creating stubs (mocks) instead of real objects for use in tests. Some actions that can be performed using Mockito:
    create stubs for classes and interfaces;
    check method calls and values ​​passed to this method;
    connection to a real “spy” object spyto control method calls.


    Create a mock FileReaderand configure it so that the method readFile()returns the string we need:


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

    Full code


    Now no reading of the file occurs. Instead, the mock gives the value configured in the test.


    The use of mok has its advantages:


    • tests check only the tested class for errors, errors of other classes do not affect the testing of the tested class in any way
    • sometimes shorter and more readable code
    • it is possible to check method calls and passed values ​​to methods of a frozen object

    and disadvantages:


    • By default, unconfigured methods return null, because all methods used must be explicitly configured.
    • if the real object has a state, then each time it is supposed to change, you need to reconfigure its mock, which is why the test code is sometimes bloated.

    There is an easier and more convenient way to create mok - use a special annotation @Mock:


    @Mock File file;

    There are three ways to initialize such mokas:



    @Before
    public void setUp() {
      MockitoAnnotations.initMocks(this);
    }


    @RunWith(MockitoJUnitRunner.class)


    @Rule public final MockitoRule rule = MockitoJUnit.rule();

    The second option is the most declarative and compact, but requires the use of a special test runner, which is not always convenient. The latter option is devoid of this drawback and is more declarative than using the method initMocks().


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

    Full code


    Host Java VM vs Android Java VM


    Android tests can be divided into two types: those that can be run on a regular Java VM, and those that need to be run on an Android Java VM. Let's look at both types of tests.


    Tests run on a regular Java VM


    Tests for code that does not require the operation of Android API components that require an Android emulator or a real device can be run directly on your computer and on any Java machine. Mostly these are business logic unit tests that test in isolation of a single class. Integration tests are written much less often, since it is far from always possible to create real class objects with which the tested class interacts.


    To write a class with Host Java tests, you need the java file to have a path ${moduleName}/src/test/java/.... Also, using the @RunWithannotation, specify Runnerwhich is responsible for running the tests, the correct call and processing of all methods:


    @RunWith(MockitoJUnitRunner.class)
    public class TestClass {...}

    Using these tests has many advantages:


    • do not require running an emulator or a real device, this is especially important when passing tests in Continuous integration , where the emulator can work very slowly and there is no real device
    • pass very quickly, since you don’t need to run the application, display the UI, etc.
    • stable, since there are no problems associated with the fact that the emulator may freeze, etc.

    on the other hand, with these tests:


    • you cannot fully test the interaction of classes with the operating system
    • in particular, you cannot test clicks on UI elements and gestures

    In order to be able to use the Android API classes in Host Java tests, there is a Robolectric library that emulates the Android environment and gives access to its main functions. However, testing Android classes with Roboelectric often works unstable: it takes time for Robolectric to support the latest Android API, there are problems getting resources, etc. Therefore, real classes are almost never used, and their moki are used for unit testing.


    To run tests using Roboelectric, you need to install a custom TestRunner . In it, you can configure the SDK version (the latest stable version is 23), indicate the main class Applicationand other parameters for the emulated Android environment.


    public class MainApplication extends Application {}

    Full code


    @RunWith(RobolectricTestRunner.class)
    @Config(sdk = 21, application = MainApplication.class)
    public class MainApplicationTest {
      @Test
      public void packageName() {
        assertThat(RuntimeEnvironment.application)
            .isInstanceOf(MainApplication.class);
      }
    }
    

    Full code


    Tests run on Android Java VM


    For instrumental tests, the presence of a device or an emulator is mandatory, since we will test button presses, text input, and other actions.


    To write a test for Android Java VM, you need to put the java file along the path ${moduleName}/src/androidTest/java/..., and also use the @RunWithannotation to specify AndroidJUnit4which will allow you to run tests on your Android device.


    @RunWith(AndroidJUnit4.class)
    public class TestClass {...}

    UI tests


    To test the UI, the Espresso framework is used , which provides an API for testing the user interface of the program. In Espresso, tests work in a background stream, and interaction with UI elements in a UI stream. Espresso has several main classes for testing:


    • Espresso is the main class. It contains static methods, such as pressing the system buttons (Back, Home), call / hide the keyboard, open the menu, and access the component.
    • ViewMatchers - allows you to find the component on the screen in the current hierarchy.
    • ViewActions - allows you to interact with the component (click, longClick, doubleClick, swipe, scroll, etc.).
    • ViewAssertions - allows you to check the status of the component.

    First UI test


    We will write the simplest Android application, which we will test:


    public class MainActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
      }
    }

    Full code


    Testing our application. When testing the UI, you first need to start the Activity. To do this, there is an ActivityTestRule that starts an Activity before each test and closes after:


    @Rule public ActivityTestRule activityTestRule =
        new ActivityTestRule<>(MainActivity.class);

    Let's write a simple test that checks that the element with id is R.id.containershown on the screen:


    @RunWith(AndroidJUnit4.class)
    public class MainActivityTest {
      @Rule
      public ActivityTestRule activityTestRule =
          new ActivityTestRule<>(MainActivity.class);
      @Test
      public void checkContainerIsDisplayed() {
        onView(ViewMatchers.withId(R.id.container))
            .check(matches(isDisplayed()));
      }
    }

    Full code


    Unlock and turn on the screen


    The emulator on slow or busy machines can run slowly. Therefore, between the launch of the emulator and the end of the build with installing the application on the emulator, enough time may pass so that the screen is blocked from inactivity. Thus, the test can be run with the screen locked, which will cause an error java.lang.RuntimeException: Could not launch activity within 45 seconds. Therefore, before starting the Activity, you need to unlock and turn on the screen. Since this needs to be done in each UI test, in order to avoid code duplication, we will create a rule that will unlock and turn on the screen before the test:


    class UnlockScreenRule implements TestRule {
      ActivityTestRule activityRule;
      UnlockScreenRule(ActivityTestRule 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();
          }
        };
      }
    }

    Full code


    Let's write a custom ActivityTestRuleone that unlocks the emulator screen and launches activity before running the tests:



    Using this rule instead of the standard one can greatly reduce the number of random crashes of UI tests in CI.


    Fragment Testing


    Typically, the layout and logic of the application UI is not put all into activity, but is divided into windows, for each of which a fragment is created. Let's create a simple snippet to display the name with 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;
      }
    }

    Full code


    When opening a fragment, the UI may freeze for some time, and if animations of transitions between fragments are used, the test can begin before the fragment appears. Therefore, you need not just open the fragment, but wait until it is launched. The Awaitility library , which has a very simple and clear syntax , is excellent for waiting for the result of actions . We will write a rule that starts a fragment and expects it to be launched using this library:


    class OpenFragmentRule implements TestRule {
      private final ActivityTestRule activityRule;
      private final Fragment fragment;
      OpenFragmentRule(ActivityTestRule 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();
          }
        };
      }
    }

    Full code


    In this case, the expression means that if the fragment does not start within five seconds, the test will not pass. It should be noted that as soon as the fragment starts, the test will immediately continue execution and will not wait all five seconds.


    Similar to the rule that launches the activity, it is logical to create a rule that launches the fragment:


    public class FragmentTestRule
        implements TestRule {
      private ActivityTestRule activityRule;
      private F fragment;
      private RuleChain ruleChain;
      public FragmentTestRule(Class activityClass, F fragment) {
        this.fragment = fragment;
        this.activityRule = new ActivityTestRule<>(activityClass);
        ruleChain = RuleChain
          .outerRule(activityRule)
          .around(new OpenFragmentRule<>(activityRule, fragment));
      }
      public ActivityTestRule 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);
      }
    }

    Full code


    A fragment test using this rule will look like this:


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

    Full code


    Asynchronous data loading in fragments


    Так как операции с диском, а именно получение имени из файла, может выполняться сравнительно долго, то следует эту операцию выполнять асинхронно. Для асинхронного получения имени из файла используем библиотеку RxJava. Можно уверенно сказать, что RxJava сейчас используется в большинстве Android приложений. Практически каждая задача, которую нужно выполнить асинхронно, выполняется с помощью RxJava, потому что это пожалуй одна из самых удобных и понятных библиотек для асинхронного выполнения кода.


    Изменим наш репозиторий так, чтобы он работал асинхронно:


    public class NameRepository {
      ...
      public Single getName() {
        return Single.create(
            emitter -> {
              Gson gson = new Gson();
              emitter.onSuccess(
                  gson.fromJson(fileReader.readFile(), User.class).getName());
            });
      }
    }

    Полный код


    Для тестирования RX-кода существует специальный класс TestObserver, который автоматически подпишется на Observable и мгновенно получит результат. Тест репозитория будет выглядеть следующим образом:


    @RunWith(MockitoJUnitRunner.class)
    public class NameRepositoryTest {
     ...
     @Test
     public void getName() {
       TestObserver observer = nameRepository.getName().test();
       observer.assertValue("Sasha");
     }
    }

    Полный код


    Обновим наш фрагмент, используя новый реактивный репозиторий:


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

    Полный код


    Так как теперь имя получается асинхронно, то для проверки результата работы нужно дождаться завершения асинхронного действия с помощью Awaitility:


    @RunWith(AndroidJUnit4.class)
    public class UserFragmentTest {
      ...
      @Test
      public void nameDisplayed() {
        await()
            .atMost(5, SECONDS)
            .ignoreExceptions()
            .untilAsserted(
                () ->
                    onView(ViewMatchers.withText("Sasha"))
                        .check(matches(isDisplayed())));
      }
    }
    

    Полный код


    Когда во фрагменте или активити выполняются асинхронные действия, в данном случае — чтение имени из файла, нужно иметь ввиду, что фрагмент может быть закрыт пользователем до того, как асинхронное действие выполнится. В текущей версии фрагмента допущена ошибка, так как если при выполнении асинхронной операции фрагмент будет уже закрыт, то 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;
      }
    }

    Полный код


    Для тестирования подобных ошибок, связанных с асинхронными действиям во фрагменте, нужно закрыть фрагмент сразу же после его открытия. Это можно сделать просто заменив его на другой фрагмент. Тогда при завершении асинхронного действия onCreateView в закрытом фрагменте textView будет null и если допустить ошибку и не отменить подписку, приложение упадет. Напишем правило для тестирования на эту ошибку:


    public class FragmentAsyncTestRule
        implements TestRule {
      private final ActivityTestRule activityRule;
      private final Fragment fragment;
      public FragmentAsyncTestRule(Class 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());
            }
          }
        };
      }
    }

    Полный код


    Добавим это правило в класс тестов фрагмента:


    @RunWith(AndroidJUnit4.class)
    public class UserFragmentTest {
      @ClassRule
      public static TestRule asyncRule =
          new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment());
      ...
    }

    Полный код


    Теперь тест упадет, если асинхронные действия будут обращаться к полям фрагмента после его завершения.


    Юнит-тестирование Rx кода


    Создадим презентер, куда мы вынесем логику подписки на возвращаемый репозиторием 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()));
      }
    }

    Полный код


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


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

    Полный код


    В данном тесте презентер не вызовет никакой метод объекта listener, так как тест проходит прежде, чем выполняется асинхронное действие. В тестах на эмуляторе Awaitility решает эту проблему. В юнит-тестах тестирование асинхронной природы кода не совсем к месту, а потому в них можно заменить стандартные RxJava Schedulers на синхронные. Используем для этого TestScheduler, который позволяет произвольно установить время, которое якобы прошло с момента подписки на Observable, чтобы протестировать корректную установку таймаута. Как обычно, напишем для этого правило:


    RxImmediateSchedulerRule
    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;
      }
    }

    Полный код


    Тест презентера с новым правилом будет выглядеть следующим образом:


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

    Полный код


    Тестирование кода, использующего Dagger 2


    Для облегчения работы с графом зависимостей объектов отлично подходит паттерн Dependency Injection. Dagger 2 — это библиотека, которая поможет в реализации этого паттерна. Поэтому в большинстве наших Android приложений все компоненты предоставляются с помощью Dagger. Об использовании и преимуществах этой библиотеки можно написать отдельную статью, а тут мы рассмотрим, как тестировать приложения, её использующие.


    To begin with, almost always when using Dagger, there exists ApplicationComponentone that provides all the main dependencies of the application, and is initialized in the application class Application, which, in turn, has a method for obtaining this component.


    ApplicationComponent
    @Singleton
    @Component(modules = {ContextModule.class})
    public interface ApplicationComponent {
      UserComponent createUserComponent();
    }

    Full code


    Mainapplication
    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;
      }
    }

    Full code


    We also create a Dagger module that will provide the repository:


    Usermodule
    @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 {}
    }

    Full code


    Change the fragment as follows to get the repository using 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;
      }
    }

    Full code


    In addition to functional UI tests, it is good to have unit tests with locked dependencies. In order to provide moored objects with Dagger, you need to replace it with ApplicationComponenta specially created component for tests. First of all, create a method for replacing the main component in Application:


    public void setComponentForTest(ApplicationComponent component) {
      this.component = component;
    }

    Full code


    In order not to replace the component in each class with fragment tests, we create a rule for this:


    class TestDaggerComponentRule implements TestRule {
      private final ActivityTestRule activityRule;
      private final ApplicationComponent component;
      TestDaggerComponentRule(
          ActivityTestRule 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);
            }
          }
        };
      }
    }

    Full code


    Note that you need to return the original component after the test, since Application is created one for all tests and it is worth returning it to its default state after each. Now create a rule that will carry out all the preparations for testing the fragment described above. Before each test, the screen will be unlocked, activity will be launched, the fragment we need will be opened, and the test Dagger component will be installed, which will provide dependency mobs.


    public class FragmentTestRule
        implements TestRule {
      private ActivityTestRule activityRule;
      private F fragment;
      private RuleChain ruleChain;
      public FragmentTestRule(
          Class 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));
      }
      ...
    }

    Full code


    Install the test component in the test of our fragment:


    @RunWith(AndroidJUnit4.class)
    public class UserFragmentTest {
      ...
      @Rule
      public final FragmentTestRule 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;
        }
      }
    }

    Full code


    Tests run only for Debug applications


    It happens that you need to add logic or go to the UI elements that developers need for more convenient testing and should only be displayed if the application is being built in debug mode. For example, let's make the presenter not only pass the name to the subscriber in the debug assembly, but also output it to the log:


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

    Full code


    This logic also needs to be tested, but tests should only be run with the appropriate type of application assembly. We will write a rule DebugTestRulethat will check the type of application assembly and run tests only for debug version:


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

    Full code


    A test with this rule will look like this:


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

    Full code


    Conclusion


    In this article, we sorted out the basic libraries for writing tests and developed a set of tools based on TestRule and designed to solve the problems of activating activity and fragments, working with asynchronous code, a dagger, debugging code and an android emulator. The use of these tools allowed us to test non-obvious problems, reduce duplication of code, and generally improve readability of tests.


    Full sample application and tests

    A complete example of an application and tests using all of the above libraries and utilities.


    public class NameRepository {
      private final FileReader fileReader;
      public NameRepository(FileReader fileReader) {
        this.fileReader = fileReader;
      }
      public Single 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;
      }
    }

    Full code


    @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 observer = nameRepository.getName().test();
        observer.assertValue("Sasha");
      }
    }

    Full code


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

    Full code


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

    Full code


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

    Full code


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

    Full code


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

    Full code


    @RunWith(AndroidJUnit4.class)
    public class UserFragmentTest {
      @ClassRule
      public static TestRule asyncRule =
          new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment());
      @Rule
      public final FragmentTestRule 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);
        }
      }
    }

    Full code


    Acknowledgments


    This article was written in collaboration with Evgeny Aseev . He wrote a significant part of the code of our libraries. Thank you for the review of the article text and code - Andrei Tarashkevich , Ruslan Login . Thanks to the sponsor of the project, AURA Devices.


    Also popular now: