Android unit test
Unit test?
What?
Test code written to test a code unit in the project; It is used to execute the objective function in the project and verify its logical state or result. These codes are white box tests, which can detect the accuracy and reliability of the target code. When packaged, the code of unit test will not be compiled into the release apk.
Why?
- Improve confidence
Unit testing can not guarantee 100% that it can run completely and correctly during integration and joint commissioning, but it can ensure that each code unit test passes.
- Convenient code refactoring
Using unit tests, you can write unit tests while refactoring to ensure that the refactoring does not destroy the accuracy of the original code logic.
- Writing unit tests is itself a small-scale code refactoring
In this process, you can find problems in logic design and code writing, such as boundary condition processing, protection of empty objects, dependence on parameters during class construction, and dependence on Android Context.
- Save development time
If there is no unit test, in order to debug a function and see whether the interface is displayed correctly, we run the APP. If there is any error, change something and run it again... With unit testing, you can run the APP less in the development process. You can wait a minute or two to verify whether the code logic is correct. The speed is much faster. In addition, unit testing can help us reduce bugs, thus reducing the time for debugging bugs and fix bug s
- Find bug s faster
If there is no unit test, we need to find the problem of our module in the final integration and joint commissioning. Once the problem is serious, we may feel stressed and panicked. However, through unit testing, you can have one more layer of assurance, find and solve problems earlier, and make yourself more idle during integration.
1, Existing Android testing framework and its advantages and disadvantages
-
JUnit4
- Test object and environment: no complex dependency; Testing of non private, non static and non global methods and classes;
- The most used test framework, JUnit4 is integrated by default in the project created by Android Studio, and dependencies are added;
- The corresponding unit test code is placed in the src/test/java directory,
- Writing: similar to IDEA,
@Test public void testadd(int num1, int num2) { MainActivity mainActivity = new MainActivity(); //before; Create the object of the class to be tested int sum = mainActivity.add(1,2); //on: call the method to be tested Assert.assertEquals(3,sum); //after: detect the result through assertion }
-
Run: select a method and right-click run;
-
Main test object
- There is a clear return value; When doing unit testing, you only need to call this function to verify whether its return value meets the expected results.
- This function only changes some properties or states inside its object. If the function itself does not return a value, verify the properties and states it changes.
- Some functions do not return a value or directly change the state of which value, so it is necessary to verify their behavior, such as click events.???
- For b and c cases, it is often to verify whether a method of an object in the target method has been called, or whether a state of an object in the target method has changed, so as to verify whether the target method is called according to the logic we want;
-
Problem: it is difficult to deal with the complex situation of relying on resources in the project; For the comparison of data structures such as objects, collections and maps (the readability of assertions is not friendly), you can use Hamcest Frame comparison;
-
Mockito
-
Test object and environment: for complex dependency situations; Testing of non private, non static and non global methods and classes;
-
In the test environment, other dependent objects are identified by Mockito mock to replace the real objects, so that the target methods to be tested are isolated,
Avoid the influence and dependence of external factors
, can be executed in our preset environment to achieve two purposes:
- Verify the call of the specified method, how many times it has been called, what the parameters are, etc;
- Specify the behavior of some methods of the object, return specific values, or perform specific actions;
-
Problem: the principle of generating mock objects has its limitations. For example, methods of final type, private type and static type cannot mock;
-
-
PowerMockito
- Test object and environment: for complex dependency situations; Testing of private, static and global methods and classes;
- Extending other mock frameworks; PowerMock basically cover s case s that cannot be supported by Mockito;
- Most java tests can be completed using JUnit4+Mockito+PowerMockito
- Powermock provides mock And spy, method simulation is usually required for the call verification of private methods of Activity. Both mock and spy can be implemented.
- Problem: for objects that rely on the Android SDK (such as Activity, Button, etc.), testing is a headache;
-
Robolectric
- Test object and environment: Test for objects that depend on Android SDK (such as Activity, Button, etc.)
- By implementing a set of Android code that the JVM can run, we can test without Android environment.
- It can realize UI test, query a control, simulate click and verify logic;
- JUnit4+Mockito+PowerMockito+Robolectric
- Question: because our projects are written in Activity and some business logic uses private methods, it is very complex to verify the correctness of the logic by querying the control and displaying the UI. Powermock is required.
- Note: there is a compatibility problem between Powermock and Robolectric. If the problem cannot be solved, You can try to give upRobolectric
-
UIAutomator And Espresso;
Google's open source testing framework, based on Instrumentation, is more inclined to self-test in UI.
Espresso is powerful and fast, but it has an important limitation: it can only operate in the Context of the App under test. This means that the following App automation test requirements cannot be realized: push messages of applications, synchronize contacts, and enter the tested App from another application.
UIAutomator 2.0 The above functions can be realized after publishing; Written in script language, integration failed... UIAutomator integration test failed;
-
robotium; UI test case
-
ActivityUnitTestCase,ActivityInstrumentationTestCase2
The Instrumentation system provided by Android, for example, runs the unit test code on the simulator or real machine. Although this method can work, it is very slow.
2, Determine Android test framework
JUnit4 | Mockito | PowerMockito | Robolectric | UIAuutomator / Espresso | robotium | ActivityUnitTestCase / ActivityInstrumentationTestCase2 | |
---|---|---|---|---|---|---|---|
Reliance on complex situations in the project | ✅ | ✅ | ✅ | ||||
Dependence on Android SDK | ✅ | ||||||
UI test | ✅ | ✅ | ✅ | ||||
Test private, global, static | ✅ |
JUnit4 + Mockito + PowerMockito + Robolectric
Usage: JUnit4 + PowerMockito
3, How to write unit tests, and how to write unit tests for non interface logical parts
In Android studio project, the source code is placed under src/main/java by default,
The corresponding Java related unit test code is placed in the src/test/java directory;
Src / Android test / Java stores Android related unit test code
Dependency: Yes by default: testImplementation "junit:junit:$junit_version"
Take tosee SDK as an example; src/main/java/tech/tosee/combine/models/TSPeer.kt
- unit testing
- Non interface unit test
- Create a Test class. Right click the class name, select GOTO, and select Test
- Select JUnit4; Check the method or attribute to be tested (private methods are not displayed); Note that after clicking OK, you will be prompted to save the directory: Java related codes are placed in the src/test/java directory; Android related unit test code is in Src / Android test / Java
- Code completion
@Test fun testEquals() { //before instantiates the target class to be tested and designs some preconditions: val tsPeer: TSPeer = TSPeer("userId", "gmsUserId", "nickname", "merchantId", "rtcUserId") //on executes the operation, calls the method to be tested, and obtains the results: val equals = tsPeer.equals(TSPeer("userid", "gmsUserId", "nickName", "merchantid", "rtcUserid")) print("================================" + equals)//This one won't be beaten Log.i(TAG, "testEquals: ") //after verification result: verify whether the result is the same as expected through assertion: assertTrue(equals) }
- Run the test method. Right click the method name, run
Notes:
@BeforeClass: Perform only once for all tests and must be static void @Before: The initialization method is executed once for each test method (note and BeforeClass The difference is that the latter is executed once for all methods) @Test: Test method, where you can test the expected exception and timeout @Test(expected=ArithmeticException.class)Check whether the tested method throws ArithmeticException abnormal @Ignore: Ignored test method @After: Release resources and execute each test method once (note and AfterClass The difference is that the latter is executed once for all methods) @AfterClass: Perform only once for all tests and must be static void
- Dependency:
testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0" // PowerMock brings in the mockito dependency testImplementation "org.powermock:powermock-module-junit4:2.0.4" testImplementation "org.powermock:powermock-module-junit4-rule:2.0.4" testImplementation "org.powermock:powermock-api-mockito2:2.0.4" testImplementation "org.powermock:powermock-classloading-xstream:2.0.4"
In case of problems, it is recommended to read: PowerMock for Android unit test
- Notes:
@RunWith(PowerMockRunner.class) @PrepareForTest({YourClassWithEgStaticMethod.class}) @PowerMockIgnore("javax.management.*")
If the test case does not use @ PrepareForTest, @ RunWith(PowerMockRunner.class) is not added, and vice versa.
When you need to use PowerMock for Mock static, final, private methods, etc., you need to add @ PrepareForTest. Or use this annotation:
@RunWith(PowerMockRunner.class) @PrepareForTest({YourClassWithEgStaticMethod.class})
Or use annotation and code
@PrepareForTest({YourClassWithEgStaticMethod.class}) MockitoAnnotations.initMocks(this);
Note: @ PrepareForTest({Example1.class, Example2.class,...})// Declare multiple static classes
-
Ordinary Mock: the object passed by the Mock parameter (no annotation is required)
- Code to be tested
class CommonExampleKotlin { fun callArgumentInstance(file: File): Boolean { return file.exists() } }
- Test code
class CommonExampleKotlinTest { @Test fun testCallArgumentInstance() { //1. Create a mock object val file = PowerMockito.mock(File::class.java) val commonExample = CommonExample() //2. Specifies the specific behavior of the mock object PowerMockito.`when`(file.exists()).thenReturn(true) //3. Pass the mock object as a parameter to the test method to execute the test method. assertTrue(commonExample.callArgumentInstance(file)) } }
-
The new object inside the Mock method
When using powermockito When using the whennew method, you must annotate @ PrepareForTest and @ RunWith@ The class written in PrepareForTest is the class where the new object code of mock needs to be located.
- Code to be tested 1
class CommonExampleKotlin { /** * 2) Mock new object inside method */ fun callArgumentInstance(path: String?): Boolean { val file = File(path) return file.exists() } }
- Test code 1
@Test //@RunWith(PowerMockRunner::class.java) @PrepareForTest(CommonExampleKotlin::class) //2) The new object inside the mock method fun testCallArgumentInstance2() { //Code implementation RunWith MockitoAnnotations.initMocks(this); //1. Create a mock object val file = PowerMockito.mock(File::class.java) val commonExampleKotlin = CommonExampleKotlin() //2. Specifies that when a File object is created with the parameter "fileName", the File object that has been mock is returned. PowerMockito.whenNew(File::class.java).withArguments("fileName").thenReturn(file) //3. Specifies the specific behavior of the mock object PowerMockito.`when`(file.exists()).thenReturn(true) //In fact, this method did not successfully create the file here, assertTrue(!commonExampleKotlin.callArgumentInstance("fileName")) val newFile = Mockito.mock(File::class.java) newFile.exists() Mockito.verify(newFile).exists() }
- Code to be tested 2
public class EmployeeService { public int getTotalEmployee() { EmployeeDao employeeDao = new EmployeeDao(); return employeeDao.getTotal(); } }
- Test code 2
getTotalEmployee()Because we can't modify this method when testing. No matter what is inside, we can't touch local variables, so! Intelligent simulation cannot be changed. This correction is likely to cause defects in the system. If you don't pay attention, it's over. Very careful, as a tester! /** * Test with PowerMock */ @Test public void testGetTotalEmployeeWithMock() { EmployeeDao employeeDao = PowerMockito.mock(EmployeeDao.class); try { //No parameters are enough! Return simulated local variables! PowerMockito.whenNew(EmployeeDao.class).withNoArguments() .thenReturn(employeeDao); PowerMockito.when(employeeDao.getTotal()).thenReturn(10); EmployeeService service = new EmployeeService(); int total = service.getTotalEmployee(); assertEquals(10, total); } catch (Exception e) { fail("Test failed."); } } }
-
final method of Mock ordinary object
- Code to be tested
class CommonDependency { /** * 3) Mock final method of common object */ final fun isAlive(): Boolean { // do something return false } } class CommonExampleKotlin { /** * 3) Mock final method of common object */ open fun callFinalMethod(example: CommonDependency): Boolean { return example.isAlive() } }
- Test code
@Test //The class of the final method. @PrepareForTest(CommonDependency::class) fun testCallFinalMethod() { val mock: CommonDependency = PowerMockito.mock(CommonDependency::class.java) val commonExampleKotlin = CommonExampleKotlin() PowerMockito.`when`(mock.isAlive()).thenReturn(true) assertTrue(commonExampleKotlin.callFinalMethod(mock)) }
- Note: if using powermockito Mock error ockito cannot mock / Spy because: - final class
Add dependency:
//Mockito testImplementation "org.mockito:mockito-inline:3.3.3" androidTestImplementation 'org.mockito:mockito-android:3.3.3'
4. Static method of mock static class
- Code to be tested
class CommonExampleKotlin { /** * 4) Mock Static methods of common classes */ fun printUUID(): String { return Utils.generateNewUUId() } }
- Static class
object MUtils { @JvmStatic open fun generateNewUUId(): String { return UUID.randomUUID().toString() } }
- Test code
//Statement tells JUnit to use PowerMockRunner to execute the test. @RunWith(PowerMockRunner::class) //Statement tells PowerMock to prepare the Employee class for testing. It is suitable for simulating final classes or classes with final, private, static and native methods. @PrepareForTest(Utils::class) class CommonExampleKotlinTest { //4) ??????? Static method of mock ordinary class val res = "Return UUID" @Test fun testPrintUUID() { val commonExampleKotlin = CommonExampleKotlin() PowerMockito.mockStatic(MUtils::class.java) PowerMockito.`when`(MUtils.generateNewUUId()).thenReturn(res) print(commonExampleKotlin.printUUID()) assert(res.equals(commonExampleKotlin.printUUID())) } }
-
Mock private method
- Method to be tested
class CommonExampleKotlin { /** * 5) Mock Private method */ fun callPrivateMethod(): Boolean { return isExist() } private fun isExist(): Boolean { return false } }
- Test code
//Statement tells JUnit to use PowerMockRunner to execute the test. @RunWith(PowerMockRunner::class) //Statement tells PowerMock to prepare the Employee class for testing. It is suitable for simulating final classes or classes with final, private, static and native methods. @PrepareForTest(CommonExampleKotlin::class) class CommonExampleKotlinTest { //5) mock private method @Test fun testCallPrivateMethod() { val mock:CommonExampleKotlin = PowerMockito.mock(CommonExampleKotlin::class.java) PowerMockito.`when`(mock.callPrivateMethod()).thenCallRealMethod() PowerMockito.`when`<Boolean>(mock, "isExist").thenReturn(true) print(mock.callPrivateMethod()) assertTrue(mock.callPrivateMethod()) } }
6. Static and final methods of mock system class
- Code to be tested
class CommonExampleKotlin { /** * 6) Mock Static and final methods of system classes */ fun callSystemStaticMethod(str: String?): String { return System.getProperty(str) } }
- Test code
//Statement tells JUnit to use PowerMockRunner to execute the test. @RunWith(PowerMockRunner::class) //Statement tells PowerMock to prepare the Employee class for testing. It is suitable for simulating final classes or classes with final, private, static and native methods. //@PrepareForTest(Utils::class) @PrepareForTest(CommonExampleKotlin::class) class CommonExampleKotlinTest { //6) Static and final methods of mock system classes @Test fun testcallSystemStaticMethod() { val commonExample = CommonExampleKotlin(); PowerMockito.mockStatic(System::class.java); PowerMockito.`when`<String>(System.getProperty("aaa")).thenReturn("bbb"); assertEquals("bbb", commonExample.callSystemStaticMethod("aaa")); } }
7. Private variables of mock common class
- Code to be tested
class CommonExampleKotlin { /** * 7) Mock Private variables of ordinary classes */ private val STATE_NOT_READY: Int = 0 private val STATE_READY = 1 private val mState = STATE_NOT_READY fun doSomethingIfStateReady(): Boolean { return if (mState == STATE_READY) { // DO some thing true } else { false } } }
- Test code
class CommonExampleKotlinTest { //7) Private variable of mock ordinary class @Test fun testdoSomethingIfStateReady() { val sample = CommonExampleKotlin() Whitebox.setInternalState(sample, "mState", 1) assertTrue(sample.doSomethingIfStateReady()) } }
Note: when you need to mock the private variable mState, you do not need to annotate @ PrepareForTest and @ RunWith, but use Whitebox to mock the private variable mState and inject your preset variable value.
IV Coverage of unit tests How to do statistics
-
Concept:
The proportion and degree of source code being tested in the program is called code coverage.
-
index
- Branch (C1 Coverage): calculate branch coverage for all if and switch statements. Count the number of branches executed and the number of branches not executed in the method. Note, however, that exception handling is not included in this calculation.
- Lines: a line may contain one or more instructions. If at least one instruction is executed, the line is considered to be executed.
- Methods: each non abstract method contains at least one instruction. If at least one instruction is executed, the method is considered to be executed.
- Classes: if at least one method in a class is executed, the class is considered to be executed.
- Instruction (C0 Coverage): the smallest unit of JaCoCo count is a single Java bytecode instruction. Instruction coverage provides information about the number of bytecodes executed and not executed.
- Cyclomatic Complexity: calculate the Cyclomatic Complexity of non abstract methods and summarize the (cyclomatic) complexity of classes, packages and groups. This value can be used as a reference for whether the unit test case is completely covered.
-
usage method
- Click "Edit Configurations..." on the upper side of the IDE:
- Select JaCoCo in the Choose coverage runner:
- function; Right click the class name and select run '...' with Coverage
- After running, indicators such as Classes, Methods and Lines will be displayed
- Click "Generate Coverage Report" in the left column to generate an html report of all indicators:
-
Reference documents
-
Android unit test framework - design ideas
-
Jacob: there is a compatibility problem with Powermock.
-
Cobertra:
-
Android uses JaCoco to analyze unit code and test coverage
-
Android unit test / Ui test + Jacobo coverage statistics
-
Code coverage of Android UI automation test
-
Attached:
- ExoPlayer
- It is an application layer media player built on Android low-level media API;
- ExoPlayer is easy to use
- Official documents
- ExoPlayer Introduction (official document)
- Cycle complexity
- Other directions
If there is infringement, please contact to delete the post.