SoFunction
Updated on 2025-04-09

In-depth discussion of Unit Testing in Android

1. Testing for ContentProvider
Before you start writing Case for Provider, you should carefully read the instructions for Provider testing in the SDK documentation. But just reading those instructions, you still can't write the correct Case, because you also know that Android documents are relatively poor, and there are some key things that are not specified in the documentation, and you also know that this is not uncommon in Android.
You write a Provider Case, as follows:
Copy the codeThe code is as follows:

public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
}

There is an error in the compilation, and it says that ProviderTestCase2 does not have an implicit construct. It seems that we need a constructor and write a standard JUnit constructor!
Copy the codeThe code is as follows:

public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
    public FeedProviderTest(String name) {
        super(name);
    }
}

WTF, there are still compilation errors, and it is even more serious! Is ProviderTestCase2 not inherited from TestCase, using Eclipse's suggestion, it creates a construct with two parameters:
Copy the codeThe code is as follows:

public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
    public FeedProviderTest(String name) {
        super(name);
    }

    public DemoProviderTest(Class<FeedProvider> providerClass,
            String providerAuthority) {
        super(providerClass, providerAuthority);
        // TODO Auto-generated constructor stub
    }
}

However, there is still an error in FeedProviderTest(String name) with only one name. If you try it without parameters, it still doesn't work. This shows that ProviderTestCase2 does not have such a constructor, but it makes no sense, because it is inherited from TestCase after all! Very magical and weird!
Since ProviderTestCase2 does not have a single parameter construction, then you can only remove the construct with one parameter!
Copy the codeThe code is as follows:

public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
    public DemoProviderTest(Class<FeedProvider> providerClass,
            String providerAuthority) {
        super(providerClass, providerAuthority);
    }

    public void testConstructor() throws Throwable {
        assertNotNull("can construct resolver", getMockContentResolver());
        ContentProvider provider = getProvider();
        assertNotNull("can instantiate provider", provider);
    }
}

I wrote a basic test, ran it, and got a Warning, which was reported by JUnit Framework. DemoProviderTest does not define the public constructor TestCase(name) or TestCase(). What's the case? It's not that I don't define it, but that there is a compilation error, because the damn ProviderTestCase2 does not have these two constructs! Damn it, I can only add this structure back! But because the parent class does not have it, you can only refer to the double parameter construction of the parent class!
Copy the codeThe code is as follows:

public class DemoProviderTest extends ProviderTestCase2<FeedProvider> { 
    public DemoProviderTest() {
        super(null, null);
    }

    public DemoProviderTest(Class<FeedProvider> providerClass,
            String providerAuthority) {
        super(providerClass, providerAuthority);
    }

    public void testConstructor() throws Throwable {
        assertNotNull("can construct resolver", getMockContentResolver());
        ContentProvider provider = getProvider();
        assertNotNull("can instantiate provider", provider);
    }
}

But what are the parameters passed? Let's try it with Null first! There is a complete error. NPE appears when the parent class's construct is initialized, which means that it is definitely wrong to pass Null! After looking at the construct DemoProviderTest(Class<FeedProvider> providerClass, String providerAuthority) imposed with two parameters, it is also said that a Class object should be passed and the Provider's Authority should be passed, and try it again!
Copy the codeThe code is as follows:

public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
    public DemoProviderTest() {
        super(, AUTHORITY);
    }

    public DemoProviderTest(Class<FeedProvider> providerClass,
            String providerAuthority) {
        super(providerClass, providerAuthority);
    }

    public void testConstructor() throws Throwable {
        assertNotNull("can construct resolver", getMockContentResolver());
        ContentProvider provider = getProvider();
        assertNotNull("can instantiate provider", provider);
    }
}

This time, Okay, but the construction of two parameters is meaningless, so one parameter calls two parameters:
Copy the codeThe code is as follows:

    public DemoProviderTest() {
        this(, AUTHORITY);
    }

Or Okay, which means that our Case must provide the correct construction parameters to ProviderTestCase2!
Add setUp and tearDown:
Copy the codeThe code is as follows:

    @Override
    public void setUp() throws Exception {
        mContentResolver = getMockContentResolver();
    }

    @Override
    public void tearDown() throws Exception {
        mContentResolver = null;
    }

After running, I found that the testConstructor was dead and said that getMockContentResolver() returns Null. How is this possible? It's so weird! Thinking about it, it might be that the initialization is not correct, so I added a call to the parent class to setUp:
Copy the codeThe code is as follows:

    @Override
    public void setUp() throws Exception {
        ();
        mContentResolver = getMockContentResolver();
    }

    @Override
    public void tearDown() throws Exception {
        ();
        mContentResolver = null;
    }

Now that I run again, I will all be OK, which means that any method involving override (Override) the parent class must be called in order to initialize correctly! Here is the correct full version:
Copy the codeThe code is as follows:

public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
    private ContentResolver mContentResolver;

    public DemoProviderTest() {
        this(, AUTHORITY);
    }

    public DemoProviderTest(Class<FeedProvider> providerClass,
            String providerAuthority) {
        super(providerClass, providerAuthority);
    }

    @Override
    public void setUp() throws Exception {
        ();
        mContentResolver = getMockContentResolver();
    }

    @Override
    public void tearDown() throws Exception {
        ();
        mContentResolver = null;
    }

    public void testConstructor() throws Throwable {
        assertNotNull("can construct resolver", getMockContentResolver());
        ContentProvider provider = getProvider();
        assertNotNull("can instantiate provider", provider);
    }
}

Let's summarizeThe experience gained from this example is that for component testing, all components must be inherited from the .* component testing framework below, but the correct parameters need to be passed to these component testing frameworks, otherwise Case cannot be tested:
Two constructors
Copy the codeThe code is as follows:

    public DemoProviderTest() {
        this(, AUTHORITY);
    }

    public DemoProviderTest(Class<FeedProvider> providerClass,
            String providerAuthority) {
        super(providerClass, providerAuthority);
    }

None of them is missing, and it is the specified constructor of JUnit (with a String, or without parameters) that needs to call the construct specified by the test architecture to pass the correct parameters to the test framework!
Also, when rewritten the parent class method, you must also call the parent class method, otherwise it will still not be initialized correctly!
But here I have to say that the test framework of these components is really not easy to use. First of all, the name is puzzling, why is there a 2? Android is really 2! Also, since as a framework, the initialization work should be done thoroughly and thoroughly, so that we can be called a framework. Users should only need to inherit and finish their own tasks, and they should be able to work. Just like the component Activity or ContentProvider, when you get into your code, the initialization work in the framework has been completed, so you, the inheritor, just need to care about your own initialization work! But the test framework is bad. The inheritor not only needs to care about his initialization but also ensures that the correct parameters are passed to the parent class!
2. Testing for Activity
Also, you should pay attention to the initialization part when testing Activity, but it doesn’t matter if you don’t adjust super for setUp and tearDown!
Copy the codeThe code is as follows:

public class ExplorerActivityTester extends
        ActivityInstrumentationTestCase2<ExplorerActivity> {
    public ExplorerActivityTester() {
        this(TARGET_PACKAGE_NAME, );
    }

    public ExplorerActivityTester(String pkg, Class<ExplorerActivity> class1) {
        super(pkg, class1);
    }

    @Override
    public void setUp() {
        mInstrumentation = getInstrumentation();
    }
}

3. Obstacles to unit testing
In Android, due to the characteristics of its system architecture, it is particularly difficult to write unit test cases and verify test cases for Android.
a. Activity reuse
The reason is that every test package is also an Apk. Each package can only inject one target Apk, which means that it can only test the content in one Apk. Once an operation jumps outside the Apk, it will be beyond the control scope of the test framework. However, the component reuse mechanism is very common in Android. It is very common to jump to other applications (apks) through Intent and call other applications' components to complete a certain operation. This is the feature of Android, and it is the most common! But this lays an insurmountable obstacle for unit test cases. The test framework itself is weaker, and once a component is popped up, Instrumentation cannot control it. The open source testing framework robutium-solo solves this problem to a certain extent. Solo can operate any component in a package, especially it can solve the problem of multiple Activity jumps. However, as mentioned earlier, because a test Apk can only inject one target Apk, once the Activity jumps outside the application, Solo has no choice. This is an unsolvable question. Therefore, when doing testing in Android, you can only focus on some logic layers, API layers, data, Providers, Services and other codes that are far away from the surface operation! For the case where the surface activity jumps around, you can only do some tests or solve them with MockObject, but this usually loses the meaning of the test itself, because it takes a lot of time to create a MockObject, which is not worth it!
b. ActionBar is not clickable
Another very disgusting problem is that the ActionBar of Activity cannot be clicked directly. I really don’t understand what Google is doing. I have created a new thing, but the test framework does not support operations! Thinking that clicking ActionBar can only click on screen coordinates through Solo, it is very difficult to port and maintain!
Speaking of operations, it has to be said that the native framework Instrumentation supports very few operations and is not easy to use. It can only distribute KeyEvent events, which are not easy to use in many cases. For example, if there is a dialog box, it is very troublesome to click on Okay or Cancel. If you want to click on an item in a ListView, it is also very troublesome! Similarly, the third-party robotium-solo framework is much easier to use. It is well encapsulated, and you can easily click on the View with this text on the screen through (). Its internal implementation is to use the View's display tree, find the relevant View based on the Tag (text), and then send a click event to it! This also explains why Solo cannot click ActionBar, because ActionBar is not in the View of Activity, it is something like StatusBar, at the system level!
c. StatusBar belongs to
It is hard to imagine that the Statusbar that can be seen everywhere actually belongs to Settings. Only packages injected with Settings can operate on Statusbar. So although Statusbar has something related to your Apk (such as tips), you still can't operate it directly unless you write a specially injected test package!
4. Security Concern
The code for the test (Instrumentation and TestRunner) also exists in the form of an Apk, which can inject any target Apk, and then operate on it, even obtain its resources and data. This brings about security issues! Apk with test code can be regarded as an application, once it is run on a certain phone, but can operate any application.
In fact, this is not a problem at all. If the application market can strictly test and review the applications uploaded by developers. But the problem now is that neither Google Play nor other markets are tested much, so it will give bad people an opportunity to take advantage of it!
In fact, the key problem here is that Android manufacturers should not blindly pursue quantity! Apple is the idea that centrally selling apps is, and Apple's App Store is also the best one! Android is just an imitator, so it is normal for you to develop slowly, have a small quantity, have insufficient quality, and have poor income, because you are a follower and you start late! For manufacturers, you cannot control the quantity and cannot create tens of thousands of applications at once. This takes time, but at least, you can strictly control the quality! You can conduct strict testing of uploaded applications, which is responsible to users and yourself! So whether it is a device or an application, Apple's must be better quality, and Android always needs to be less severe, so when you see Apple's things are higher, Android is cheaper, of course the price is also Android's only advantage! In today's society, you get what you pay for, and there will naturally be no good products if you are cheap!