Source: Colin-Phang /AndroidUnitTest

Next article: Landing on Android Unit Tests in Complex Projects (PowerMock Practice)

In recent years, there has been a lot of discussion about implementing unit testing in Android projects, and Google has also provided some official solutions. However, many articles on the Internet are usually only the simplest project demo, which has almost no practical reference value for complex projects. So this article summarizes recent research in an attempt to find a way to land unit tests on an Android project.

This paper is mainly divided into two parts: investigation and practice. This article focuses on Android unit testing research.

0 revenue

Unit testing refers to the examination and verification of the smallest testable units in software. What is a minimum measurable unit – this is an artificial division and can be a class, function, or visualized function. Importantly, unit testing emphasizes that the individual units under test are tested in isolation from the rest of the program.

So what does unit testing do for us — or why do we spend so much time and effort doing it? The main points are as follows:

1. Ensure the quality of business delivery.

Unit testing tests the project at the functional level. On the one hand, it can test the robustness of the code under various boundary and limit conditions, and at the same time, it can check the unstable factors caused by code changes in the process of iteration and reduce the risk of high-speed iteration.

2. Unit testing forces us to encapsulate and remodel the project so that it can be more gracefully tested.

You will consciously keep each class small, and you will clearly assign each function responsibility, rather than trying to cram a bunch of code into a single class (such as an Activity). You automatically prefer to write code in a composition rather than inheritance style.

3. The case of unit test is similar to the technical document and has the ability of business guidance.

Unit test cases are often tied to the specific business processes and code logic of a page, so they can be as business-oriented as documents.

At the very least, unit testing provides a quick and convenient way to test whether the logic of a module/function is robust, rather than intrusively adding test code to the project business. The benefits of introducing a framework for unit testing are large enough.

1. Framework Research

1.0 Scope of unit tests

Which functions need to be unit tested? It can be briefly summarized as the following three:

  1. With an explicit return value, simply call the function and verify that the return value of the function matches the expected result.
  2. The function itself returns no value, validating the properties and states it changes.
  3. Some functions return no value and do not directly change the state of any value, so you need to verify their behavior, such as a page jump or pulling up the camera.
  4. Functions that return no value, change no state, and trigger no behavior are untestable and should not exist in a project.

The ** framework was investigated to make it as easy as possible to cover the above three functions in Android projects. ** Let’s talk about unit testing in Android development.

When creating a new project, you can see that there are two directories in the SRC directory: androitTest and test. Both are directories for Android test code, but they are different:

  1. / SRC /androidTest code needs to run on the real machine/emulator, mainly to test a function is normal, similar to UI automation test.
  2. The/SRC /test code can run directly on the JVM and validate function-level logic, which is commonly referred to as unit test code.

So there are two types of Android test code running on real machines and JVMS. Here are some related frameworks:

  1. JUnit, the root of Java unit testing, basically validates the state of a value/object returned by a function with assertions.
  2. Espresso, Google’s official UI test automation framework, needs to run on mobile phones/emulators, similar to Appium.
  3. Robolectric, which implements a set of Android code that runs on the JVM.
  4. Mockito, if the business under test depends on a complex context, mocks can be used to simulate the objects that the code under test depends on to ensure unit testing.

Let’s talk about the investigation and selection of several frameworks. In a hurry, we can directly see the conclusion at the end of the article.

JUnit 1.1

JUnit is the foundation of Java unit testing, on which test cases are run and validated. Android is developed using the Java language, and Android unit testing is naturally dependent on JUnit. JUnit is mainly used for:

  1. Several annotations are provided to make it easy to organize and run tests.
  2. Various assertion apis are provided to verify that code works as expected.

The assertion API will not be introduced, but check the official wiki.

Here are a few common notes:

  1. @Test

Mark the method as a test method. The test method must be public void and can throw an exception. 2. @runwith specifies a Runner to provide the context in which the test code runs. 3. @rule defines the behavior of each test method in a test class, such as specifying a Acitivity as the context in which the test is run. 4. The @before initialization method is usually used to set the test preconditions. 5. @after frees the resource, which is called once After each test method executes.

@RunWith(JUnit4.class)
public class JUnitSample {
    Object object;

    // Initializers, usually preconditions/dependencies used for testing
    @Before
    public void setUp(a) throws Exception {
        object = new Object();
    }

    // The test method must be public void
    @Test
    public void test(a) { Assert.assertNotNull(object); }}Copy the code

Ps: A Test class unit Test is executed in the following order: @beforeClass – > @before – > @test – > @after – > @AfterClass

Conclusion: JUnit is the foundation of unit testing.

1.2 Espresso

Google’s official UI automated testing framework, the test code written with Espresso must run on emulator or device, and in the process of running the test code, it will actually pull up the page, UI interaction, file reading and writing, network request, etc., and finally check the UI state through various assertions. The framework provides three types of apis:

  1. ViewMatchers, finding the View object under test, implements findViewById in the test code.
  2. ViewActions, which sends interaction events that simulate UI touch interactions in the test code.
  3. ViewAssertions, which verify the state of the UI, are assertions of the STATE of the UI that are checked as expected after the test code has run.

Without further ado, let’s go to a simple demo:

// run the test code using AndroidJUnit4 provided by Espresso
@RunWith(AndroidJUnit4.class)
public class EspressoSample {

    // Use ActivityTestRule provided by Espresso to pull MainActivity
    @Rule
    public ActivityTestRule<MainActivity> mIntentsRule = new IntentsTestRule<>(MainActivity.class);

    @Test
    public void testNoContentView(a) throws Exception {
        // the withId function returns a ViewMatchers object to look for the view withId r.i.b.tn_get
        onView(withId(R.id.btn_get))
                // The click function returns a ViewActions object that emits the click event
                .perform(click());  

        // Check whether the asynchronous network request is completed by polling periodically whether loadingView is displayed
        View loadingView = mIntentsRule.getActivity().findViewById(R.id.loading_view);
        while (true) {
            Thread.sleep(1000);
            if (loadingView.getVisibility() == View.GONE) {
                break; }}After the request is complete, check the UI state
        // find the view from r.i.idmg_result
        onView(withId(R.id.img_result))
                // The matches function returns a ViewAssertions object that checks whether a state of the view is as expected.check(matches(isDisplayed())); }}Copy the code

The above test code needs to be run on the real machine/emulator. During the running process, MainActivity is automatically pulled up and the button with the ID of BTN_GET is automatically clicked. Then after the loading is finished, the id of IMg_result is displayed.

You can feel that Espresso is really powerful, with the API it provides, you can basically test common UI logic. But in complex projects, Espreeso’s shortcomings are also obvious:

1. Coarse granularity.

Espresso is essentially a UI automated test scheme. It is difficult to verify function-level logic. If you only want to verify whether a function is normal or not, the test results are not controllable due to network conditions, device conditions and even user accounts.

2. Complex logic.

General page UI elements are large and complex, so it is impossible to write test code to verify the interaction logic of every View. Only some key interactions can be verified selectively.

3. Running speed is slow.

Writing test code with Espresso must run on emulator or device. Running test cases becomes a lengthy process as you package them, upload them to the machine, and then run the UI one by one. The nice thing about this is that it’s intuitive on the phone, but it’s really slow to debug and run, and it’s certainly not as efficient and convenient as manual testing.

Conclusion: Writing Espresso use cases is like doing a reverse implementation of business code. In practice, it is better to run the project code directly for manual self-testing, so I feel Espresso is a powerful UI automation testing tool, rather than a solution for unit testing.

1.3 Robolectric

The Espresso issue is obvious, but is it possible to get Android code off the phone/emulator and run directly on the JVM? We need a unit test solution that isolates Android dependencies and can be run directly from the IDE to see the results.

Jar contains all the declarations of the Android Framework’s classes, functions, and variables, but it doesn’t have any concrete implementation. Android.jar is only used for JAVA code compilation, and isn’t really packaged into APK. The real implementation of the Android Framework is on the device/emulator. Calling Android SDK functions on the JVM throws RuntimeException directly.

So a big pain point for Android unit testing is how to isolate the entire Android SDK dependency.

Robolectric, Google’s officially recommended open source testing framework, is one such tool. It simply implements a set of Android code that runs on the JVM. One such tool is Robolectric, an open-source testing framework officially recommended by Google that implements a set of Android code that runs on the JVM.

Shadow is the core of Robolectric, a framework that provides Shadow objects (Activity and ShadowActivity, TextView and ShadowTextView, etc.) for objects in the Android SDK. The essence of Robolectric is to simulate and test Android components using Shadow in a Java runtime environment to achieve Android unit testing. For components that Robolectirc does not currently support, you can extend Robolectric’s functionality by using custom shadows.

Robolectric tric tric tric tric tric tric tric tric tric tric tric tric tric tric Tric Tric Tric:

  1. The Robolectric version is strongly dependent on the Android SDK version. Robolectric will shadow most of Android code

2. Robolectric fails to download maven dependencies for the first time. This dependency file is too large and the download logic is written in the Robolectric framework and cannot be solved by network proxy. There are several online solutions that have failed in the latest version of Robolectric. 3. Incompatible with third-party libraries. A large number of third-party libraries do not have shadow classes and will fail at startup/runtime. 4. Static code blocks are prone to errors. We often load the SO library or perform some initialization logic in a static block of code, and almost always get an error and it’s hard to resolve. Changing the logic in reverse for the sake of this unit test would be a bit of an inversion.

Abroad about Robolectri also has a lot of discussion: www.philosophicalhacker.com/post/why-i-…

Conclusion: When the code under test (Presenter, Model layer, etc.) inevitably relies on Android SDK code (TextUtils, Looper, etc.),Robolectric can easily make the test code run on the JVM, which is probably the most significant aspect of Robolectric. But because of the above, I don’t think such a unit testing framework can ever land on a project when even running the code successfully becomes a fantasy.

1.4 the Mock

Having said that Espresso needs to run on a real machine, Robolectric had too many problems to work on complex projects. Consider using a Mock framework to isolate the entire Android SDK and project business dependencies and focus unit testing on function-level code logic.

A Mock is defined as creating Mock objects/data to test the behavior of your program. Mock Servers are the ones we see most often, where the Mock interface returns data for front-end debugging.

In unit testing, however, if the business under test depends on a more complex context, you can use mocks to create objects in the Mock code to enable the unit test to proceed. Car designers, for example, use crash test dummies to simulate injuries to people in car crashes.

Mock frameworks are basically the following two:

  1. Mockito
    • Simulate objects and make them perform/return as we expect (similar to code piling)
    • Verify that the mock object executes/returns as expected
  2. PowerMockito
    • Based on the Mockito
    • Supports simulation of static functions, constructors, private functions, final functions, and system functions

PowerMockito is very powerful, but the more PowerMock is used, the lower the abstraction level of the code being tested, and the worse the quality and structure of the code. Because PowerMockito is an extension based on Mockito, the apis for both are very similar. The common apis are the following:

  1. Simulates the object and specifies the execution/return values of some functions
when(...) .thenReturn(...)Copy the code
  1. Verify that the mock object executes/returns as expected
verify(...) .invoke(...)Copy the code

Here’s how to isolate the Android SDK and project business dependencies with PowerMockito in unit testing:

  1. Mock complex objects that are relied on
  2. Execute the code under test
  3. Verify that the logic executes/returns as expected
public class PowerMockitoSample {
    private MainActivity activity;
    private ImageView mockImg;
    private TextView mockTv;

    @Before
    public void setUp(a) {
        activity = new MainActivity();
        // 1. Mock the dependent complex object.
        // MainActivity relies on some Views. Here is how to Mock out the dependent complex objects and make them private variables of MainActivity
        mockImg = PowerMockito.mock(ImageView.class);
        Whitebox.setInternalState(activity, "resultImg", mockImg);
        mockTv = PowerMockito.mock(TextView.class);
        Whitebox.setInternalState(activity, "resultTv", mockTv);
        Whitebox.setInternalState(activity, "loadingView", PowerMockito.mock(ProgressBar.class));
    }

    @Test
    public void test_onFail(a) throws Exception {
        // 2. Execute code under test.
        // Verify the activity.onfail () function
        String errorMessage = "test";
        activity.onFail(errorMessage);
        // 3. Verify that the logic executes/returns as expected.
        // Verify that the UI state of resultImg and resultTv is changed as expectedverify(mockImg).setImageResource(R.drawable.ic_error); verify(mockTv).setText(errorMessage); }}Copy the code

After we mock the various View objects MainActivity relies on, the rest is basically a matter of workload.

As you can see, the Mock framework does a good job of isolating complex dependent objects (such as Views), ensuring that the individual units under test are isolated from the rest of the program, and then focusing on verifying that the logic of a particular function/module is sound and robust.

It is important to note that in real projects there is a lot of code (logs, other statistical code, etc.) that is commonly used but does not affect business logic, and some static code blocks also call the Android SDK API directly. Because unit test code runs on the JVM and needs to be suppressed/quarantined, PowerMockito provides good support (more on that in the next article).

Conclusion: PowerMockito, a powerful Mock framework, proxies out complex objects on which the classes under test depend without requiring intrusive changes to the business code and ensures that the unit test code runs quickly and efficiently on the JVM.

2 the conclusion

  1. JUnit is the foundation.
  2. Espresso needs to run on a real machine and can be used for android-based functional testing rather than unit testing.
  3. Roboelctric had too many problems in complex projects and was abandoned.
  4. Android unit testing focuses on fine-granularity (functional level) code logic by isolating the entire Android SDK and project business dependencies through PowerMockito.