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

Полный код


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 @Before and @After . Methods marked with annotation @Before will be executed before each test, and methods marked with annotation @Before will be performed @After after each test. There are also annotations @BeforeClass and @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");
  }
}

Полный код


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

Полный код


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

  ...
}

Полный код


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 @BeforeClass and @ClassRule annotations @AfterClass .


When several are used in tests TestRule , it may be necessary that they run in a specific order, for this there is a RuleChain with which you can determine the order in which ours are launched TestRule . Let's 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();
        }
      }
    };
  }
}

Полный код


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

  ...
}

Полный код


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

Полный код


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

Полный код


Now we want to test it NameRepository , but in fact we are testing NameRepository 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 FileReader with its mock.


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


Create a mock FileReader and 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");
  }
}

Полный код


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, so 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)

  • Add the MockitoRule
    • rule to the test :

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

    Полный код


    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 @RunWith annotation, specify Runner which 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 while Robolectric will support the latest Android API, there are problems with 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 Application and other parameters for the emulated Android environment.


    public class MainApplication extends Application {}

    Полный код


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

    Полный код


    Tests run on Android Java VM


    For instrumental tests, the presence of a device or emulator is mandatory, as 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 @RunWith annotation to specify AndroidJUnit4 which 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 run in the background stream, and interactions with UI elements in the 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, 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);
      }
    }

    Полный код


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


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

    Let's write a simple test verifying that the element with id is R.id.container shown on the screen:


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

    Полный код


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

    Полный код


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


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

    Полный код


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

    Полный код


    When opening a fragment, the UI may freeze for a while, 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 the execution of actions . We will write a rule that starts a fragment and expects it to be launched using this library:


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

    Полный код


    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.


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


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

    Полный код


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

    Полный код


    Asynchronous data loading in fragments


    Since operations with a disk, namely, obtaining a name from a file, can take a relatively long time, this operation should be performed asynchronously. To asynchronously get the name from the file, use the RxJava library . We can confidently say that RxJava is now used in most Android applications. Практически каждая задача, которую нужно выполнить асинхронно, выполняется с помощью Rx java , потому что это пожалуй одна из самых удобных и понятных библиотек для асинхронного выполнения кода.


    Change our repository so that it works asynchronously:


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

    Полный код


    To test the RX code, there is a special class TestObserver that automatically subscribes to Observable and instantly receives the result. The repository test will look like this:


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

    Полный код


    Update our fragment using the new reactive repository:


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

    Полный код


    Since now the name is obtained asynchronously, to check the result of work, you need to wait for the completion of the asynchronous action using Awaitility:


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

    Полный код


    When asynchronous actions are performed in a fragment or activity, in this case, reading a name from a file, it must be borne in mind that the fragment can be closed by the user before the asynchronous action is performed. In the current version of the fragment, an error was made, since if the fragment is already closed during the asynchronous operation, it textView will already be deleted and equal null . To prevent Crush applications NullPointerException when accessing textView in subscribe() , stop the asynchronous operation at the close of the fragment:


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

    Полный код


    To test such errors associated with asynchronous actions in a fragment, you need to close the fragment immediately after opening it. This can be done simply by replacing it with another fragment. Then at the end of the asynchronous action onCreateView in a closed fragment textView will be null and if you make a mistake and do not cancel the subscription, the application will crash. We will write a rule for testing this error:


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

    Полный код


    Add this rule to the fragment test class:


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

    Полный код


    Now the test will fail if asynchronous actions will access the fields of the fragment after its completion.


    Unit testing Rx code


    We will create a presenter where we will take out the logic of subscribing to the repository returned Observable from the fragment, and also add timeout to get the name from the file:


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

    Полный код


    In this case, when testing a presenter, you already need to test the end result of a subscription that receives data asynchronously. Let's write a naive version of such a test:


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

    Полный код


    In this test, the presenter will not call any method of the object listener , since the test passes before the asynchronous action is performed. In tests on the emulator, Awaitility solves this problem. In unit tests, testing the asynchronous nature of the code is out of place, and therefore you can replace standard RxJava Schedulers with synchronous ones in them Schedulers . We use TestScheduler for this , which allows you to arbitrarily set the time that supposedly elapsed from the moment of subscribing to Observable , in order to test the correct timeout setting. As usual, we will write a rule for this:


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

    Полный код


    The presenter test with the new rule will look like this:


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

    Полный код


    Testing code using Dagger 2


    To facilitate working with the dependency graph of objects, the Dependency Injection pattern is excellent . Dagger 2 is a library that will help in the implementation of this pattern. Therefore, in most of our Android applications, all components are provided using Dagger. You can write a separate article about the use and advantages of this library, and here we will look at how to test applications that use it.


    To begin with, almost always when using Dagger there exists ApplicationComponent one 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();
    }

    Полный код


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

    Полный код


    We will 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 {}
    }

    Полный код


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

    Полный код


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


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

    Полный код


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


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

    Полный код


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

    Полный код


    Install the test component in the test of our fragment:


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

    Полный код


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

    Полный код


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

    Полный код


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

    Полный код


    Conclusion


    In this article, we figured 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 testing non-obvious problems, reducing code duplication, and generally increasing the readability of tests.


    Full sample application and tests

    A complete sample 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<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;
      }
    }

    Полный код


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

    Полный код


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

    Полный код


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

    Полный код


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

    Полный код


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

    Полный код


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

    Полный код


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

    Полный код


    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.