* This article has been published exclusively by guolin_blog, an official wechat account

This article is expected to take 15-20 minutes to read

Introduction to Android Testing

For most Android commercial projects, the basic is in the high-speed iterative development stage, this stage is not only the development efficiency of the project, but also put forward higher requirements for the product quality of the project.

Generally, large projects provide quality assurance through black box testing and other methods. However, the author believes that Android unit testing and UI testing that can run automatically on the Android platform are also required. These tests have the following advantages:

  • Find bugs and other problems in the code earlier and fix bugs in advance;
  • Better design: When refactoring a project, ensure that the new code refactored works correctly, so that the product quality can be better preserved as the business continues to iterate.

Android test code location

When you create a new project in Android Studio, it automatically creates code directories for the two test types:

  • Unit test case: in the module-name/ SRC /test/ Java directory, only depends on the JVM environment, not the Android environment
  • InstrumentTest test/UI test case: is located in the module name/SRC/androidTest/Java directory, under the Android environment to run

Next, I will try to supplement the corresponding unit test cases and UI test cases for my own project (based on MVP architecture development) to preliminarily practice how to write and run relevant test cases on the Android platform.

Android unit testing practices

Create a new use case

To write a new local unit test case, simply open the Java code file you want to test, then click the class name – ⇧⌘T (Windows: Ctrl+Shift+T) – select the method to generate – select the Test folder, corresponding to the local unit test – complete.

Adding dependency libraries

Need JUnit and Mockito framework support, so build. Gradle add:

testImplementation "Junit: junit: 4.12"
testImplementation "Org. Mockito: mockito - core: 2.7.1." "
Copy the code

Write test code

Generally speaking, writing a piece of test code requires three steps:

  • Environment initialization
  • Perform operations
  • Verify the correctness of the results

The author mainly tested the p-layer code in the MVP architecture. In the author’s project, the P layer uses the Dagger2 mechanism to inject a DataManager, or data acquisition source. You also need a proxy for layer V, so that after layer P gets the data from the data source, it can hand it over to Layer V for presentation.

The code calls roughly as follows:

mPresenter = new NewsPresenter(mDataManager);
mPresenter.getNews();
mPresenter.attach(mView);
--> mView.showProgress(); // Load the progress bar before the data is finished loading
--> mView.showNews(news);
--> mView.hideProgress(); // Hide the progress bar after the data is loaded
Copy the code

By contrast, the actual writing of p-layer unit test cases does not require a real data source, just mocks out a DataManager and V-layer proxy for the test using the Mockito framework.

For the Presenter class, the newly created test code looks like this:

/**
 * Created by Xu on 2019/04/05.
 *
 * @author Xu
 */
public class NewsPresenterTest {
    @ClassRule
    public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule();

    @Mock
    private NewsContract.View view;
    @Mock
    protected DataManager mMockDataManager;
    private NewsPresenter newsPresenter;
    
    @Before
    public void setUp(a) {
        MockitoAnnotations.initMocks(this);
        newsPresenter = new NewsPresenter(mMockDataManager);
        newsPresenter.attach(view);
    }
    
    @Test
    public void getNewsAndLoadIntoView(a) {
        TencentNewsResultBean resultBean = new TencentNewsResultBean();
        resultBean.setData(new ArrayList<>());
        when(mMockDataManager.getNews()).thenReturn(Flowable.just(resultBean));

        newsPresenter.getNews();

        // Test whether the model gets data
        verify(mMockDataManager).getNews();

        // Test whether the view calls the corresponding interface
        verify(view).showProgress();
        verify(view).showNews(anyList());
        verify(view).hideProgress();
    }

    @After
    public void tearDown(a) { newsPresenter.detach(); }}Copy the code

Among them:

  1. At the beginning of the code, we declare an @classrule;

What is @classrule? It is almost the same as the @rule annotation, and you can perform some related initial calls before any class method starts. Using this annotation, you can add specific actions during test case execution without affecting the original use case code, effectively reducing coupling.

This is mainly because RxJava2 is used in the project, and RxJava requires support from the Android environment. If you run the JUnit test case directly, you will get an error, so we added @classRule here. Specific reference stackoverflow.com/questions/4…

/**
 * Created by Xu on 2019/04/05.
 *
 * @author Xu
 */
public class RxImmediateSchedulerRule implements TestRule {
    private Scheduler immediate = new Scheduler() {
        @Override
        public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
            // this prevents StackOverflowErrors when scheduling with a delay
            return super.scheduleDirect(run, 0, unit);
        }

        @Override
        public Worker createWorker(a) {
            return newExecutorScheduler.ExecutorWorker(Runnable::run); }};@Override
    public Statement apply(final Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate(a) throws Throwable {
                RxJavaPlugins.setInitIoSchedulerHandler(scheduler -> immediate);
                RxJavaPlugins.setInitComputationSchedulerHandler(scheduler -> immediate);
                RxJavaPlugins.setInitNewThreadSchedulerHandler(scheduler -> immediate);
                RxJavaPlugins.setInitSingleSchedulerHandler(scheduler -> immediate);
                RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> immediate);

                try {
                    base.evaluate();
                } finally{ RxJavaPlugins.reset(); RxAndroidPlugins.reset(); }}}; }}Copy the code
  1. Use the Mockito framework to mock a test with DataManager and V layer proxy newscontract.view. A mock is the creation of a mock object of a class that, in a test environment, replaces the real object with a mock object to verify that the object’s methods are called, or to specify that some of the object’s methods return specific values, etc.
  2. The @before annotation method is executed Before the test case is executed. Here an initialization operation is performed, mainly Mockito framework initialization and Presenter initialization. The @After annotation method is executed After the test case is executed, which does a Presenter detach() operation to prevent memory leaks.
  3. The @test annotated method is the Test method that is actually executed. Based on the previous business code logic:
  • Environment initialization: Since the business logic of NewsPresenter requires DataManager to return a NewsResultBean instance for subsequent operations, and the mock returns only an empty object, So in the first two lines of code, I use Mockito’s when() method to return an empty NewsResultBean instance when the program calls the DataManager#getNews() method.
  • Execute the action: execute NewsPresenter#getNews() for the P layer. In business logic, after this method is executed, DataManager#getNews() is called, and the data is then handed to the V layer agent.
  • Verify that the result is correct: Generally speaking, the easiest way to verify that a method is executed correctly is to see if the output of the executed method matches the expected output. But in this case, NewsPresenter#getNews() is a void method with no return value, so how do you verify? DataManager#getNews() is called and newscontract. View#showNews(news) is called to display the data. DataManager#getNews() and newscontract.view #showProgress(), Newscontract.view #showNews(news) and newscontract.view #hideProgress() are used to verify the Mockito method.

At this point, an Android unit test case is written. Running the unit test case directly through Android Studio results in the following:

It is important to understand that unit testing is only testing a method unit, it is not testing the functional process of an entire APP, that is, unit testing does not involve complex external environment such as database or network. Here, for example, we only test the NewsPresenter#getNews() method. We don’t test whether the entire initialization to display of the NewsFragment is normal or whether the data is wrong. (Such tests are often called integration tests.)

Android UI testing practices

Create a new use case

To contribute a new local UI test case, just open the Java code file you want to test and click on the class name — ⇧⌘T (Windows: Ctrl+Shift+T) — select the method to generate — select the androidTest folder corresponding to the local UI tests — done.

Adding dependency libraries

Need to Espresso framework supports, so in the build. Gradle add (attention androidTestImplementation) :

androidTestImplementation "Androidx. Test: runner: 1.1.0."
androidTestImplementation "Androidx. Test: rules: 1.1.0."
androidTestImplementation "Androidx. Test. Espresso: espresso - core: 3.0.2." "
androidTestImplementation "Androidx. Test. Espresso: espresso - contrib: 3.0.2." "
androidTestImplementation "Androidx. Test. Espresso: espresso - the intents: 3.0.2." "
androidTestImplementation "Androidx. Test. Espresso. Idling: idling - concurrent: 3.0.2." "
androidTestImplementation "Androidx. Test. Espresso: espresso - idling - resource: 3.0.2." "
Copy the code

Write test code

The main test code of the author is NewsDetailActivity. The main function is to load the news title and the original news address delivered by the intent, then display the news title in the Toolbar, and load the news in the Webview.

In contrast, when writing test code, you can construct an intent for testing, add the required test data to the intent, and then start the activity to check whether the data is correct. Here we use the Espresso framework, which has three important components: ViewMatchers (matching a specified View based on its View ID or other properties), ViewActions (executing certain actions of the View, such as clicking events), ViewAssertions (checking some states of the View, For example, specify whether the View is displayed on the screen.

The newly created UI test code is as follows:

/** * Created by Xu on 2019/04/09. */
@RunWith(AndroidJUnit4.class)
@LargeTest
public class NewsDetailActivityTest {

    @Rule
    public ActivityTestRule<NewsDetailActivity> newsDetailActivityActivityTestRule =
            new ActivityTestRule<>(NewsDetailActivity.class, true.false);

    @Before
    public void setUp(a) {
        Intent intent = new Intent(InstrumentationRegistry.getInstrumentation().getTargetContext(), NewsDetailActivity.class);
        intent.putExtra(Constants.NEWS_URL, TestConstants.NEWS_DETAIL_ACTIVITY_TEST_URL);
        intent.putExtra(Constants.NEWS_IMG, TestConstants.NEWS_DETAIL_ACTIVITY_TEST_IMG);
        intent.putExtra(Constants.NEWS_TITLE, TestConstants.NEWS_DETAIL_ACTIVITY_TEST_TITLE);
        newsDetailActivityActivityTestRule.launchActivity(intent);
        IdlingRegistry.getInstance().register(newsDetailActivityActivityTestRule.getActivity().getCountingIdlingResource());
    }

    @Test
    public void showNewsDetail(a) { onView(withId(R.id.toolbar)).check(matches(isDisplayed())); onView(withId(R.id.iv_news_detail_pic)).check(matches(isDisplayed())); onView(withId(R.id.clp_toolbar)).check(matches(isDisplayed())); onView(withId(R.id.clp_toolbar)).check(matches(withCollapsingToolbarLayoutText(is(TestConstants.NEWS_DETAIL_ACTIVITY_TES T_TITLE)))); }@After
    public void tearDown(a) {
        IdlingRegistry.getInstance().unregister(newsDetailActivityActivityTestRule.getActivity().getCountingIdlingResource());
    }
}
Copy the code

Among them:

  1. At the beginning of the class declaration, add two annotations @runwith (Androidjunit4.class) and @largeTest;

The @runwith annotation changes the default execution class of the JUnit test case. Since the Android environment is required and the Espresso framework is used, @runwith selects the AndroidJUnit4 class. @largeTest indicates that this test case uses an external file system or network and takes more than 1000 ms to run.

  1. Declare a variable newsDetailActivityActivityTestRule Rule annotations, and @ newsDetailActivityActivityTestRule ActivityTestRule instantiate the object. ActivityTestRule tests a single Activity that starts Before @test and @before. It contains some basic functions, such as starting the Activity, getting the current Activity instance, etc.
  2. Similarly, the @before annotation method is executed Before the test case is executed. Here we construct an intent for testing, Finally through newsDetailActivityActivityTestRule# launchActivity (intent) method to test the Activity, and make a IdlingResource binding; The @After annotation method is executed After the test case is executed. Here we do an IdlingResource unbinding;

What is IdlingResource?

Generally speaking, most apps have a lot of asynchronous tasks in the process of designing business functions, such as making network requests using Rxjava, but Espresso doesn’t know when your asynchronous tasks end. Using Thread.sleep() alone to wait for the result of an asynchronous callback is too “hardcore”, so you need to resort to the IdlingResource class.

It requires adding the relevant logic to the business code. For example, in the NewsDetailActivity, it receives the address of the news picture delivered by the intent and uses Glide to load the picture asynchronously, with the following code:

public class NewsDetailActivity extends AppCompatActivity {

    @BindView(R.id.iv_news_detail_pic)
    private ImageView ivNewsDetailPic;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_news);
        // omit some code logic
        
        // The App starts to enter the busy state
        EspressoIdlingResource.increment();
        
        // Start loading images
        Glide.with(context).asBitmap().load(imgUrl).into(new GlideDrawableImageViewTarget(mAvatar) {
            @Override
            public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                super.onResourceReady(resource, transition);
                // Set App to idle after asynchronous operation
                if(! EspressoIdlingResource.getIdlingResource().isIdleNow()) { EspressoIdlingResource.decrement(); }}}); }// omit the code
    
    @VisibleForTesting
    public IdlingResource getCountingIdlingResource(a) {
        returnEspressoIdlingResource.getIdlingResource(); }}public class EspressoIdlingResource {
    private static final String RESOURCE = "GLOBAL";

    // Espresso provides a well implemented CountingIdlingResource class
    // If there is no special need, just use it
    private static CountingIdlingResource countingIdlingResource = new CountingIdlingResource(RESOURCE);

    public static void increment(a) {
        countingIdlingResource.increment();
    }

    public static void decrement(a) {
        countingIdlingResource.decrement();
    }

    public static IdlingResource getIdlingResource(a) {
        if (countingIdlingResource == null) {
            countingIdlingResource = new CountingIdlingResource(RESOURCE);
        }
        returncountingIdlingResource; }}Copy the code

Plus we statement in the test code IdlingRegistry. GetInstance (). The register () and IdlingRegistry getInstance (). The unregister () method, According to whether the APP is in a busy state to judge whether the asynchronous task is completed, so Espresso can do the corresponding test for the asynchronous task.

  1. The @test annotated method is the Test method that is actually executed. Based on the previous business code logic:
  • Environment initialization: Simulated intent data for the test
  • Perform an action: Load the data passed by the intent
  • Verify that the result is correct: Check that the corresponding UI style displays the test data properly, using several important APIS of Espresso:
    • OnView () : Get the view, which is searched by the withId() method, that is, get the corresponding view according to its ID
    • Check () : Check the view, you can check whether the view text matches or whether the view is displayed, etc., mainly rely on the match() method to return the corresponding matching class, Espresso also comes with a lot of encapsulated view Matchers for use

Write chained code to verify test results, such as onView(withId(R.I.D.A. toolbar)).check(matches(isDisplayed())); Get the view with id R.id.toolbar and check whether the view is displayed properly.

If the View Matchers that come with Espresso don’t cut it, we can make a custom matcher, For example, onView(withId(R.id.clp_toolbar)).check(matches(withCollapsingToolbarLayoutText(is(TestConstants.NEWS_DETAIL_ACTIVITY_TES T_TITLE)))); CollapsingToolbarLayout view CollapsingToolbarLayout view CollapsingToolbarLayout view CollapsingToolbarLayout view CollapsingToolbarLayout view CollapsingToolbarLayout view CollapsingToolbarLayout view CollapsingToolbarLayout

public static Matcher<View> withCollapsingToolbarLayoutText(Matcher<String> stringMatcher) {
    return new BoundedMatcher<View, CollapsingToolbarLayout>(CollapsingToolbarLayout.class) {
        @Override
        public void describeTo(Description description) {
            description.appendText("with CollapsingToolbarLayout title: ");
            stringMatcher.describeTo(description);
        }

        @Override
        protected boolean matchesSafely(CollapsingToolbarLayout collapsingToolbarLayout) {
            returnstringMatcher.matches(collapsingToolbarLayout.getTitle()); }}; }Copy the code

A Matcher of String Matcher (returned by the is() method) returns a CollapsingToolbarLayout title.

At this point, an Android UI test case is written. Running the use case directly through Android Studio results in the following:

conclusion

This paper mainly starts from two different granularity of test: unit test and UI test, comprehensively refers to the test code in Google Sample project, makes a preliminary practice, analyzes, writes and runs relevant test cases.

The author thinks that the general process of writing Android test cases is as follows:

  1. Determine the granularity of test cases to be written;
  2. Analyze the pages that need to be tested and extract the more important and short business code logic;
  3. Test cases are designed using a three-step (initialization-execution-validation) approach based on this logic, which includes not only business requirements but also other business or common code logic that needs to be maintained;
  4. When doing unit test, I think that the business logic of the test does not need to cross many pages, but can be executed on the current page to avoid increasing the maintenance cost of unit test cases.
  5. Unit test cases do not directly improve code quality, but they can ensure that the refactored new code can run correctly and reduce risks when the project is refactored.

This article will be synchronized to my personal log, if you have any questions, please feel free to raise, thank you!