This article is sponsored by Yu Gang Said Writing Platform

Original author: Jdqm

Copyright notice: The copyright of this article belongs to the wechat public account Yu Gangshuo

Unit tests are the basic tests in your application’s testing strategy. By unit testing your code, you can easily verify that the logic of individual units is correct. Running unit tests after each build can help you quickly catch and fix regression problems caused by code changes (refactoring, optimization, and so on). This article focuses on unit testing in Android.

The purpose and content of unit testing

Why unit tests?

  • Improved stability, the ability to know if the development is done correctly;
  • Quickly feedback bugs, run through unit test cases, locate bugs;
  • Check bugs through unit tests as early as possible in the development cycle to minimize technical debt. The later the bug repair may cost more, and in serious cases, the project schedule will be affected;
  • Provide security for code refactoring, optimize code without worrying about regression problems, run test cases after refactoring, fail to show that refactoring may be problematic, easier to maintain.

What do unit tests measure?

  • List the normal and abnormal conditions that you want to test coverage for test verification;
  • Performance tests, such as the time taken by an algorithm, and so on.

Classification of unit tests

  1. Local tests: Run only on the local machine JVM to minimize execution time. This unit test does not depend on the Android framework, or if there are dependencies, it is convenient to simulate the dependencies to achieve isolation of Android dependencies, such as Google recommended [Mockito][1].

  2. Instrumented Tests: Unit tests run on a real machine or emulator that are slow to run on the device. These tests can access Instrumented information, such as the context of the application being tested.

JUnit annotations

Knowing some JUnit annotations will help you understand what follows.

Annotation describe
@Test public void method() Define the method as the unit test method
@Test (expected = Exception.class) public void method() A test method that does not throw an Exception type of Annotation (subclasses are fine)-> fails
@Test(timeout=100) public void method() Performance test, if method takes more than 100 ms -> fails
@Before public void method() This method is executed before each Test and is used to prepare the Test environment (e.g., initialize the class, read the input stream, etc.). In a Test class, each execution of the @test method triggers a call.
@After public void method() This method is executed after each Test and is used to clean up the Test environment data. In a Test class, each execution of the @test method triggers a call.
@BeforeClass public static void method() This method is executed once before all tests start to do some time-consuming initialization work (such as connecting to a database) and must be static
@AfterClass public static void method() This method is executed once after all tests are completed to clean up data (such as disconnecting data) and must be static
@ignore or @ignore (” too long “) public void method() Ignore the current test method, usually when the test method is not ready, or too time consuming
@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class TestClass{} Causes all test methods in the test class to execute in alphabetical order of method names, specifying three values: DEFAULT, JVM, and NAME_ASCENDING

Local test

Local tests can be divided into two categories based on whether the unit has external dependencies (e.g. Android dependencies, other unit dependencies).

1. Add dependencies, Google official recommendation:
Dependencies {// Required -- JUnit 4 framework testImplementation 'JUnit: JUnit :4.12' // Optional -- Mockito Framework (optional, for simulating some dependency objects to isolate dependencies) testImplementation 'org.mockito:mockito-core:2.19.0'}Copy the code
2. Storage location of unit test code:

In fact, AS already created the test code store directory for us.

├─ ├─ Class exercises, ├─ Class exercises, ├─ Class Exercises, ├─ Class Exercises, ├─ Class Exercises, ├─ Class Exercises, Class ExercisesCopy the code
3. Create test class:

You can manually Create a Test class in the corresponding directory. AS also provides a shortcut: Select the corresponding class -> hover the cursor over the class name -> press ALT + ENTER-> select Create Test in the pop-up window

Note: Checking setUp/ @before produces an empty setUp() method with an @before annotation, and tearDown/ @after produces an empty method with @after.

import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; public class EmailValidatorTest { @Test public void isValidEmail() { assertThat(EmailValidator.isValidEmail("[email protected]"), is(true)); }}Copy the code
4. Run test cases:
  1. To Run a single Test method: select the @test annotation or method name, right-click Run;
  2. Run all test methods in a test class: open the class file and right-click Run in the scope of the class, or right-click Run directly from the class file.
  3. Run all test classes in a directory: select the directory and right-click Run.

The example of running the previous test to verify the mailbox format is displayed in the Run window, as shown below:

The result clearly shows that the test method is isValidEmail() in EmailValidatorTest class. The test status is passed and the test takes 12 milliseconds.

To modify the previous example, pass in an invalid email address:

@Test
public void isValidEmail() {
    assertThat(EmailValidator.isValidEmail("#[email protected]"), is(true));
}
Copy the code

The test status is failed, which takes 14 milliseconds, and also gives detailed error information: an assertion error occurs in line 15 because Expected is true but Actual is false.

You can also run all test cases using the gradlew test command. This way, you can add the following configuration to output the various tests during the unit test:

android {
    ...
    testOptions.unitTests.all {
        testLogging {
            events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
            outputs.upToDateWhen { false }
            showStandardStreams = true
        }
    }
}
Copy the code

Gradlew test:

Out or system. err prints in unit tests are also printed.

5. Simulate and isolate dependencies through the simulation framework:

In the previous example of validating the mail format, the local JVM virtual machine provides an adequate environment to run, but if the unit being tested relies on the Android framework, such as using methods of the Android Context class, the local JVM will not provide such an environment. This is where the Mockito [1] framework comes in.

Here is a Context#getString(int) test case

import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class MockUnitTest { private static final String FAKE_STRING = "AndroidUnitTest"; @Mock Context mMockContext; @test public void readStringFromContext_LocalizedString() { When (mmockContext.getString (r.string.app_name)).thenReturn(FAKE_STRING); assertThat(mMockContext.getString(R.string.app_name), is(FAKE_STRING)); when(mMockContext.getPackageName()).thenReturn("com.jdqm.androidunittest"); System.out.println(mMockContext.getPackageName()); }}Copy the code

Isolation of dependencies is achieved by specifying the return value of calling context.getString(int) method through the simulation framework [Mockito][1], where [Mockito][1] uses [cglib][2] dynamic proxy technology.

Instrumental testing

In some cases, it is possible to isolate Android dependencies by means of emulation, but at great cost. In such cases, instrumented unit testing can help reduce the effort required to write and maintain the emulation code.

Instrumented tests are tests that run on a real machine or emulator and can take advantage of the Android Framework APIs and supporting APIs. Instrumentation unit tests should be created if the test case requires access to instrumentation information (such as the application Context), or if the actual implementation of an Android framework component (such as a Parcelable or SharedPreferences object) is required. It will be slower because you have to run to a real machine or emulator.

Configuration:
Dependencies {androidTestImplementation 'com. Android. Support: support - annotations: 27.1.1' androidTestImplementation 'com. Android. Support. Test: runner: 1.0.2' androidTestImplementation 'com. Android. Support. Test: rules: 1.0.2'}Copy the code
android {
    ...
    defaultConfig {
        ...
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}
Copy the code
Example:

Here is an example of operating SharedPreference. This example needs to access the Context class and the concrete implementation of SharedPreference. Using simulation to isolate dependencies will cost a lot, so instrumented testing is more appropriate.

This is the implementation of the operation SharedPreference in the business code

public class SharedPreferenceDao { private SharedPreferences sp; public SharedPreferenceDao(SharedPreferences sp) { this.sp = sp; } public SharedPreferenceDao(Context context) { this(context.getSharedPreferences("config", Context.MODE_PRIVATE)); } public void put(String key, String value) { SharedPreferences.Editor editor = sp.edit(); editor.putString(key, value); editor.apply(); } public String get(String key) { return sp.getString(key, null); }}Copy the code

Create instrumented Test class (app/ SRC /androidTest/ Java)

// @runwith is only required to mix JUnit3 with JUnit4. @runwith (androidjunit4.class) public class SharedPreferenceDaoTest {public static final String TEST_KEY = "instrumentedTest"; Public static final String TEST_STRING = "TEST_STRING "; SharedPreferenceDao spDao; @Before public void setUp() { spDao = new SharedPreferenceDao(App.getContext()); } @Test public void sharedPreferenceDaoWriteRead() { spDao.put(TEST_KEY, TEST_STRING); Assert.assertEquals(TEST_STRING, spDao.get(TEST_KEY)); }}Copy the code

Running like a local unit test, this procedure installs APK on the connected device and the test results are displayed in the Run window as shown below:

The status passed can be clearly seen in the test result. If you look at the printed log carefully, it can be found that this process installs two APK files to the simulator, respectively app-debug.apk and app-debug-androidtest. apk. Instrumented Tests are instrumented in app-debug-androidtest.apk. PM install:

// install apK //-t: allow install test APK //-r: Reinstalling an existing application, preserving its data, More similar to replace installation / / please refer to the https://developer.android.com/studio/command-line/adb?hl=zh-cn adb shell PM to install - t - r filePathCopy the code

After installing the two APKs, run the AM instrument command to run the instrumented test case. The general format of the command is as follows:

am instrument [flags] <test_package>/<runner_class>
Copy the code

For example, the actual execution command in this example:

adb shell am instrument -w -r -e debug false -e class 'com.jdqm.androidunittest.SharedPreferenceDaoTest#sharedPreferenceDaoWriteRead' com.jdqm.androidunittest.test/android.support.test.runner.AndroidJUnitRunner
Copy the code
-w: Forces the AM instrument command to wait for the completion of the instrument test to ensure that the COMMAND line window is not closed during the test to view the log of the test process. -r: outputs the result in raw format. -e: In the form of key-value pairs provide test options, for example - e debug false please refer to https://developer.android.com/studio/test/command-line?hl=zh-cn for more information on this commandCopy the code

If you can’t handle the instrumented test time, there is a readily available solution [Robolectric][3], which will be used locally when we talk about the open Source box library in the next section.

Common open source library for unit testing

1. Mocktio

Github.com/mockito/moc…

Mock objects, which control the return value of their methods, monitor the invocation of their methods, and so on.

Add the dependent

TestImplementation 'org. Mockito: mockito - core: 2.19.0'Copy the code

example

import static org.hamcrest.core.Is.is; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.*; import static org.mockito.internal.verification.VerificationModeFactory.atLeast; @RunWith(MockitoJUnitRunner.class) public class MyClassTest { @Mock MyClass test; @test public void mockitoTestExample() throws Exception {//MyClass Test = Mock (myclass.class); Test.getuniqueid () returns 43 when(test.getUniqueId()).thenReturn(18); Test.com pareTo() returns 43 when(test.compareTo(anyInt())).thenReturn(18); DoThrow (new NullPointerException()).when(test).close(); DoNothing ().when(test).execute(); // doNothing when test.execute() is called. assertThat(test.getUniqueId(), is(18)); Test.getuniqueid () verify(test, times(1)).getUniqueId(); Test.getuniqueid () verify(test, never()).getUniqueId(); // Verify that test.getUniqueId() verify(test, atLeast(2)).getUniqueId(); Test.getuniqueid () verify(test, atMost(3)).getUniqueId(); Query ("test string") verify(test).query("test string"); // Wrap the List object with mockito.spy () and return the spy object that mocks it. List List = new LinkedList(); List spy = spy(list); / / specified spy. Get (0) returns "Jdqm doReturn" (" Jdqm "). The when (spy). Get (0); assertEquals("Jdqm", spy.get(0)); }}Copy the code
2. powermock

Github.com/powermock/p…

Mock for static methods

Add the dependent

TestImplementation 'org. Powermock: powermock - API - mockito2:1.7.4' testImplementation 'org. Powermock: powermock - module - junit 4:1.7.4'Copy the code

Note: If using Mockito, you need to use compatible versions of both, see github.com/powermock/p…

example

@RunWith(PowerMockRunner.class) @PrepareForTest({StaticClass1.class, StaticClass2. Class}) public class StaticMockTest {@test public void testSomething() throws Exception{ By default all methods do nothing mockStatic(Staticclass.class); when(StaticClass1.getStaticMethod()).thenReturn("Jdqm"); StaticClass1.getStaticMethod(); / / verify StaticClass1. GetStaticMethod () this method is called a verifyStatic (StaticClass1. Class, times (1)); }}Copy the code

Or encapsulate as non-static and then use [Mockito][1]:

class StaticClass1Wraper{
  void someMethod() {
    StaticClass1.someStaticMethod();
  }
Copy the code
3. Robolectric

robolectric.org

It is mainly to solve the defects of time-consuming instrumentalization testing. Instrumentalization testing needs to be installed and run on Android system, that is, on Android VIRTUAL machine or real machine, so it is very time-consuming. Basically, it takes several minutes to go back and forth every time. The industry already has a solution to this problem: Robolectric from Pivotal LABS uses Robolectrict to emulate the Shadow Classes of the Android core library, making it easy to write tests like native tests and run them directly on the JVM in your workplace.

Add the configuration

TestImplementation "org. Robolectric: robolectric: 3.8" android {... testOptions { unitTests { includeAndroidResources = true } } }Copy the code

Example simulation open MainActivity, click the Button above the interface, read TextView text information.

MainActivity.java

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView tvResult = findViewById(R.id.tvResult); Button button = findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { tvResult.setText("Robolectric Rocks!" ); }}); }}Copy the code

The test class (app/SRC/test/Java /)

@RunWith(RobolectricTestRunner.class) public class MyActivityTest { @Test public void clickingButton_shouldChangeResultsViewText() throws Exception { MainActivity activity = Robolectric.setupActivity(MainActivity.class); Button button = activity.findViewById(R.id.button); TextView results = activity.findViewById(R.id.tvResult); // To simulate clicking a button, call OnClickListener#onClick button.performClick(); Assert.assertEquals("Robolectric Rocks!" , results.getText().toString()); }}Copy the code

The test results

At 917 milliseconds, this is slower than a purely local test. This example is very similar to running directly to a real machine or emulator, but it only needs to run on the local JVM, thanks to Robolectric’s Shadow.

Note: The first run will require downloading some dependencies, which may take a little longer, but the subsequent tests will certainly be faster than instrumenting the process of packing two APKs and installing them.

In the sixth section, the author introduced running to the real computer to test the SharedPreferences operation by means of instrumentation test. Perhaps the joke point is that it takes too long. Now, Robolectric is adapted to local test to try to reduce some time.

In a real project, the Application might be created to initialize some other dependent libraries, which is not convenient for unit testing. Instead, create an additional Application class, which does not need to be registered in the manifest file, and write directly to the local test directory.

public class RoboApp extends Application {}
Copy the code

When writing a test class to @ Config (application = RoboApp. Class) to configure the application, when introduced into the Context needs to call RuntimeEnvironment. Application for:

app/src/test/java/

@RunWith(RobolectricTestRunner.class) @Config(application = RoboApp.class) public class SharedPreferenceDaoTest { public  static final String TEST_KEY = "instrumentedTest"; Public static final String TEST_STRING = "TEST_STRING "; SharedPreferenceDao spDao; @ Before public void setUp () {/ / the Context using RuntimeEnvironment here. The application to replace the application Context spDao = new SharedPreferenceDao(RuntimeEnvironment.application); } @Test public void sharedPreferenceDaoWriteRead() { spDao.put(TEST_KEY, TEST_STRING); Assert.assertEquals(TEST_STRING, spDao.get(TEST_KEY)); }}Copy the code

Just run it like it’s local.

Practical experience

1. How to test textutil.isEmpty () in your code
public static boolean isValidEmail(CharSequence email) {
    if (TextUtils.isEmpty(email)) {
        return false;
    }
    return EMAIL_PATTERN.matcher(email).matches();
}
Copy the code

When you try to test such code locally, you will receive the following exception:

java.lang.RuntimeException: Method isEmpty in android.text.TextUtils not mocked.
Copy the code

In this case, add the implementation of the TextUtils class directly under the local test directory (app/ SRC /test/ Java), but make sure the package name is the same.

package android.text; public class TextUtils { public static boolean isEmpty(CharSequence str) { return str == null || str.length() == 0; }}Copy the code
2. Isolate native methods
public class Model {
    public native boolean nativeMethod();
}
Copy the code
public class ModelTest { Model model; @Before public void setUp() throws Exception { model = mock(Model.class); } @Test public void testNativeMethod() throws Exception { when(model.nativeMethod()).thenReturn(true); Assert.assertTrue(model.nativeMethod()); }}Copy the code
3. In internal new, it is inconvenient to Mock
public class Presenter { Model model; public Presenter() { model = new Model(); } public boolean getBoolean() { return model.getBoolean()); }}Copy the code

In this case, you need to change the way the code is written. Instead of internal new, you need to pass it as an argument.

public class Presenter { Model model; public Presenter(Model model) { this.model = model; } public boolean getBoolean() { return model.getBoolean(); }}Copy the code

This is done to facilitate Mock Model objects.

public class PresenterTest { Model model; Presenter presenter; @before public void setUp() throws Exception {// Mock Model object Model = mock(model.class); presenter = new Presenter(model); } @Test public void testGetBoolean() throws Exception { when(model.getBoolean()).thenReturn(true); Assert.assertTrue(presenter.getBoolean()); }}Copy the code

As you can see from this example, whether the framework of your code is unit test-friendly is also a factor in advancing unit testing.

4. Local unit tests – File operations

In some involves the App file reading and writing, often called at runtime Environment. External.getexternalstoragedirectory () get machine peripheral storage paths, typically run to the real machine or debug on the simulator, are time consuming, can through the way of simulation, Complete the file operation on the local JVM.

// Keep the package name the same. Public class Environment {public static File external.getexternalstoragedirectory () {return new File (" local File system directory "); }}Copy the code

Debug directly in the local unit test, no longer need to run to the real machine, and then pull out the file to view.

public class FileDaoTest { public static final String TEST_STRING = "Hello Android Unit Test."; FileDao fileDao; @Before public void setUp() throws Exception { fileDao = new FileDao(); } @Test public void testWrite() throws Exception { String name = "readme.md"; fileDao.write(name, TEST_STRING); String content = fileDao.read(name); Assert.assertEquals(TEST_STRING, content); }}Copy the code
5. Some test tips
  • Consider readability: Use expressive method names for method names, and consider using a specification such as Rspec-style for test paradigms. The method name can take one format, such as: [Method to test][Test conditions][Meets expected results].
  • Do not use logical flow keywords: for example (If/else, for, do/while, switch/case) within a test method, break them into separate test methods If necessary.
  • What the test really needs to test: the cases that need to be overridden, generally considering only the validation output (such as what is displayed and what is the value after an operation).
  • Don’t worry about testing private methods: treat private methods as black-box internal components and test public methods that reference them; Don’t worry about testing trivial code such as getters or setters.
  • Each unit Test method should be sequential: Decouple as much as possible. There should be no timing between Test A and Test B for different Test methods.

In the sample code snippets given in this article, some class code is not posted. If necessary, you can go to the following address to obtain the complete code:

Github.com/jdqm/Androi…

The resources

Developer.android.com/training/te… Developer.android.com/training/te… Developer.android.com/training/te… Blog. Dreamtobe. Cn / 2016/05/15 /… www.jianshu.com/p/bc99678b1… Developer.android.com/studio/test… Developer.android.com/studio/comm…