What are unit tests
Definition: Unit testing is writing test code for the smallest functional unit
The smallest functional unit of a Java program is a method, so unit testing a Java program is testing a single Java method.
What is a JUnit
JUnit is an open source unit testing framework for the Java language, designed specifically for Java and most widely used. JUnit is the de facto standard framework for unit testing, and any Java developer should learn to write unit tests using JUnit.
The benefits of writing unit tests using JUnit are:
- It’s very simple to organize your test code and run it at any time
- JUnit gives successful and failed tests, and generates test reports
- This includes not only the success rate of the tests, but also the code coverage of the tests, which is how much of the code being tested has been tested itself.
- Almost all IDE tools are integrated with JUnit so that we can write and run JUnit tests directly in the IDE
For high-quality code, test coverage should be at least 80%.
Benefits of unit testing
- Unit tests ensure that individual methods perform as expected. If you change the code for a method, just make sure that the corresponding unit test passes and the change is considered correct.
- Can the test code itself be used as sample code to demonstrate how to call the method
When writing unit tests, we follow certain specifications:
- The unit test code itself must be so simple that it can be read at a glance that you should never write tests for the test code;
- Each unit test should be independent of the order in which it is run;
- When testing, you should not only override common test cases, but also pay special attention to test boundary conditions such as 0, NULL, empty string “”, etc.
More of the exception scenarios are exposed online, so it is important to focus on validating the exception logic in unit tests.
How do I write unit tests
Add the dependent
The following dependencies are automatically added to build.gradle for app modules in new Android projects:
TestImplementation junit: junit: '4.12' androidTestImplementation androidx. Test. Ext: junit: 1.1.3 ' AndroidTestImplementation 'androidx. Test. Espresso: espresso - core: 3.4.0'Copy the code
- TestImplementation: Represents the Junit unit test dependency, corresponding to the test directory
- AndroidTestImplementation: said Android integration testing, the corresponding is androidTest directory
When writing unit tests, we can use a mock framework to mock out usable objects that have no real construction at runtime. We need to add the following dependencies:
testImplementation 'org. Mockito: mockito - core: 2.19.0'
Copy the code
Add a use case
First add a test class. Here I add a simple calculation class:
public class Calculate { private int mValue; //+1 public int addOne() { return ++mValue; } //-1 public int reduceOne() { return --mValue; }}Copy the code
Then right-click on the method name, as shown below, and click “Test”:
If the Test class has not been created before, you will be prompted to find the corresponding Test class. Click “Create Test” and the following pop-up box will appear:
- Testing Library: A Library of test cases. Since we rely on Junit4 in build.gradle, we can select Junit4
- Class name: indicates the type of the generated test file. Generally use the default (business class after Test as the Test class name)
- Superclass: base class name. Normally fill in the base class of the business class
- Destination Package: Specifies the target package name of the test class generated in the test directory
- SetUp / @before: Whether to generate a setUp method annotated with @before
- TearDown / @after: Whether to generate the tearDwon method with the @after annotation
- Member: This lists all the public methods provided by the class, where you can choose which methods to add test cases to
Clicking ok will give you the option to create a unit test case or an integration test case, as shown below:
Here we choose the unit test case. Then we will find the corresponding package name and test file in the test directory, as shown below:
annotations
The three most common comments used in unit testing are:
@before: indicates that this method is executed Before all other Test methods. Generally used for initialization.
@after: Indicates that the After method is executed After the execution of each Test method. Generally used to reclaim related resources
@test: Identifies the method as a Test method
Add a use case
We added the following code to the CalculateTest class we just generated:
public class CalculateTest { private Calculate mCalculate; @Before public void setUp() throws Exception { mCalculate = new Calculate(); } @After public void tearDown() throws Exception { mCalculate = null; } @Test public void addOne() { Assert.assertTrue(mCalculate.addOne() == 1); Assert.assertEquals(mCalculate.addOne(), 2); } @Test public void reduceOne() { Assert.assertTrue(mCalculate.reduceOne() == -1); }}Copy the code
- We start by declaring a variable of type Calculate, mCalculate
- We construct an instance of the Calculate object in setUp and assign it to mCalculate
- Reference mCalculate in addOne and reduceOne methods, and verify the corresponding methods
Here we use junit-supported assertions to determine whether a use case passes:
- AssertTrue: Verifies the condition. If the condition is met, the use case can pass. Otherwise, the execution of the use case fails
- AssertEquals: Here assertEquals overload multiple types of implementations, only ints are compared here.
Asynchronous test
public class CalculateTest { private Calculate mCalculate; ExecutorService sSingleExecutorService = Executors.newSingleThreadExecutor(); . @Test public void addOneAsync() { final CountDownLatch signal = new CountDownLatch(1) ; sSingleExecutorService.execute(new Runnable() { @Override public void run() { Assert.assertTrue(mCalculate.addOne() == 1); Assert.assertEquals(mCalculate.addOne(), 2); signal.countDown(); }}); try { signal.await(); } catch (InterruptedException e) { e.printStackTrace() ; }}}Copy the code
As shown in the code above, for asynchronous scenarios, we can use the CountDownLatch class to specifically pause the thread of execution and then wake up the use-case thread until the task is completed.
Note that the above try is at the heart of the pause thread.
The Mock test
We sometimes reference objects from the Android framework, but our unit tests don’t run on a real device. We don’t build real Android objects at run time, but we can mock a fake object and force its interface to return the expected results.
1. Add mock dependency references, as mentioned earlier when adding dependencies:
TestImplementation 'org. Mockito: mockito - core: 2.19.0'Copy the code
2. Importing static makes the code much cleaner, so this step is not necessary:
import static org.mockito.Mockito.*;
Copy the code
3. Create mock objects
TextView mockView = mock(TextView.class);
Copy the code
4. Test and insert piles
when(mockView.getText()).thenReturn("Junit Test");
Copy the code
Let’s look at a simple example. First we’ll add a simple method to the Calculate class to get text information from our TextView:
public String getViewString(TextView view) {
return view.getText().toString();
}
Copy the code
Then we add a new test method to the CalculateTest class:
@Test
public void mockTest() {
TextView mockView = mock(TextView.class);
when(mockView.getText()).thenReturn("Junit Test");
assertEquals(mCalculate.getViewString(mockView), "Junit Test");
}
Copy the code
Finally, run the use case and pass.
Parametric test
When a method has parameters, we can batch verify that different parameter values and corresponding use cases pass without having to write the same code multiple times
1. Parameterize the test first, requiring us to add the following annotations to the test class
@RunWith(Parameterized.class)
Copy the code
2. Define the set of Parameters – the method must be defined as public static – @parameterized.parameters 3 must be added. Define received parameters and expected parameter objects. 4. Add corresponding use cases
Let’s look at the following example: First we add an add method with parameters to Calculate:
public class Calculate { private int mValue; . public int add(int other) { mValue += other; return mValue; }}Copy the code
Then modify the test class
@RunWith(Parameterized.class) //---------@1 public class CalculateTest { private Calculate mCalculate; private Integer mInputNumber; //---------@3 private Integer mExpectedNumber; //---------@4 public CalculateTest(Integer input , Integer output) { mInputNumber = input; mExpectedNumber = output; } @Parameterized.Parameters //---------@2 public static Collection paramsCollection() { return Arrays.asList(new Object[][] { { 2, 2 }, { 6, 6 }, { 19, 19 }, { 22, 22 }, { 23, 23 } }); } @Before public void setUp() throws Exception { mCalculate = new Calculate(); } @After public void tearDown() throws Exception { mCalculate = null; } //---------@5 @Test public void paramsTest() { assertEquals(mExpectedNumber, Integer.valueOf(mCalculate.add(mInputNumber))); }}Copy the code
@1: Annotate the class with RunWith(parameterized.class)
@2: Add the data collection method, decorated with the @parameterized.Parameters annotation
@3: Add input parameters and expected parameters
@4: Adds a constructor that supplies input parameters and expected parameter assignments
@5: Add a test method that validates directly with input and expected parameters
Abnormal test
Exception validation is specified with the @test annotation parameter:
@Test(expected = InvalidParameterException.class)
Copy the code
Here’s a concrete example:
public class Calculate { private int mValue; public int addException(int other) { if (other < 0) { throw new InvalidParameterException(); } return add(other); }}Copy the code
The test classes are as follows:
@RunWith(Parameterized.class) public class CalculateTest { private Calculate mCalculate; @Test(expected = InvalidParameterException.class) public void exceptionTest() { mCalculate.addException(-1); }}Copy the code
Here are a few things to note:
- If the expected exception is a base class that throws an exception, the use case test will also pass
- If no Expected parameter is added, the use case fails
Run the use case
- Run a single use case method
Click the green arrow on the left, the menu as shown in the above picture will pop up, and the single Run can execute the use case.
- Batch execute all use cases of a class
As shown in the figure above, select the test class file, right click “Run class name “, and all the use cases of that class will be executed in batches
- Batch execute all use cases for the project
As shown in the figure above, right-click the package name and Run “Run Test in package name “to execute the use cases for all classes in the package
Exporting a Test Report
After executing the test case, we can export the test report as shown below:
View test coverage
As shown in the figure above: Click the converage button, the following coverage will pop up in the right window, where the test coverage is counted from three aspects:
- class
- method
- Line
Finally, we can export the coverage report.
This article is published by OpenWrite!