Android Unit Testing Practice

what is unit testing

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 a test for a single Java method.

What is JUnit

JUnit is an open source unit testing framework for the Java language, designed specifically for Java, and is the most widely used. JUnit is the de facto standard framework for unit testing, and any Java developer should learn and use JUnit to write unit tests.

The benefits of writing unit tests with JUnit are:

  • Very simple to organize test code and run them at any time
  • JUnit will give successful tests and failed tests, and can also generate test reports
  • It not only includes the success rate of the test, but also the code coverage of the test, that is, how much of the tested code itself has been tested.
  • Almost all IDE tools integrate JUnit so that we can write and run JUnit tests directly in the IDE

For high-quality code, test coverage should be above 80%.

The benefits of unit testing

  • Unit tests ensure that individual methods behave as expected. If you modify the code of a method, you only need to ensure that the corresponding unit test passes, and the modification is considered correct.
  • The test code itself can be used as sample code to demonstrate how to call the method

When writing unit tests, we have to follow certain rules:

  • The unit test code itself must be very simple, can be understood at a glance, and must not write tests for the test code;
  • Each unit test should be independent of each other and not depend on the order in which it is run;
  • When testing, not only should the common test cases be covered, but also special attention should be paid to the test boundary conditions, such as the input is 0, null, empty string "" and so on.

Most of the online exposures are abnormal scenarios, so it is necessary to focus on verifying the relevant abnormal logic in unit testing.

How to write unit tests

add dependencies

The following dependencies are automatically added to the build.gradle of the app module in the new Android project:

testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
复制代码
  • testImplementation : Indicates Junit unit test dependencies, corresponding to the test directory
  • androidTestImplementation :表示Android集成测试,对应的是androidTest目录

在写单元测试的时候,有些对象在运行时是没有真实构造的,这个时候我们可以使用mock框架来模拟出一个可用的对象,需要添加如下依赖:

testImplementation 'org.mockito:mockito-core:2.19.0'
复制代码

添加用例

首先添加一个测试类,这里我添加一个简单的计算类:

public class Calculate {

    private int mValue;

    //+1
    public int addOne() {
        return ++mValue;
    }

    //-1
    public int reduceOne() {
        return --mValue;
    }
}
复制代码

然后在方法名上右键鼠标,如下图所示,点击"Test":

goto test

如果之前该类没有创建过Test类,则会提示你没有找到对应的测试类,点击“create Test”即会出现如下弹框:

create test

  • Testing Library:测试用例库,因为我们build.gradle中依赖的是Junit4,所以这里选择Junit4即可
  • Class name:表示生成的测试文件类型。一般用默认的即可(业务类后面加上Test作为测试类名)
  • Superclass:基类名称。一般正常填业务类的基类即可
  • Destination package:test目录下生成的Test类的目标包名
  • setUp/@Before : 是否生成setUp方法,并且加上@Before注解
  • tearDown/@After :是否生成tearDwon方法,并且加上@After注解
  • Member:这里会列出该类提供的所有public方法,这里你可以选择对哪些方法添加测试用例

点击ok按钮,会让你选择创建单元测试用例 还是 集成测试用例,如下图所示:

file

这里我们选择单元测试用例。 然后我们就会在test目录下找到对应的包名和测试文件了,如下图所示:

file

注解

单元测试的时候用的最多的是上面3个注解:

@Before : 表示该方法在其他所有的Test方法执行之前都会执行一遍。一般用于初始化。

@After :表示每个Test方法执行结束后,都会执行一遍After方法。一般用于回收相关资源

@Test:标识该方法是一个测试方法

添加用例

我们在刚才生成的CalculateTest类中增加如下代码:

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);
    }
}
复制代码
  1. 我们首先声明一个Calculate类型的变量mCalculate
  2. 我们在setUp中构造一个Calculate对象实例,赋值给mCalculate
  3. 在addOne和reduceOne方法中引用mCalculate,做对应方法的验证

这里我们用到了Junit支持的断言来判断用例是否通过:

  • Assert.assertTrue:支持条件验证,条件满足则该用例能通过,否则用例执行会失败
  • Assert.assertEquals:这里assertEquals重载了多个类型的实现,只是这里是比较int值而已。

异步测试

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() ;
        }
    }
}
复制代码

如上代码所示,针对异步场景,我们可以使用到 CountDownLatch 类来针对性的暂停执行线程,直到任务执行完成后再唤醒用例线程。

注意,上面的try 才是暂停执行线程的核心。

Mock测试

有些时候我们不免会引用Android框架的对象,但是我们单元测试又不是运行在真实设备上的,在运行时是没有构建出真实的Android对象的,不过我们可以通过mock程序来模拟一个假的对象,并且强制让该对象的接口返回我们预期的结果。

1.添加mock依赖引用,前面添加依赖项的时候有提到:

testImplementation 'org.mockito:mockito-core:2.19.0'
复制代码

2.导入静态会让代码简洁很多,这步不是必要的:

import static org.mockito.Mockito.*;
复制代码

3.创建mock对象

TextView mockView = mock(TextView.class);
复制代码

4.进行测试插桩

when(mockView.getText()).thenReturn("Junit Test");
复制代码

下面我们看一个简单的例子。 首先我们在Calculate 类中新增一个简单的方法,获取TextView的文本信息:

public String getViewString(TextView view) {
        return view.getText().toString();
    }
复制代码

然后我们在CalculateTest类中新增测试方法:

	@Test
    public void mockTest() {
        TextView mockView = mock(TextView.class);

        when(mockView.getText()).thenReturn("Junit Test");

        assertEquals(mCalculate.getViewString(mockView), "Junit Test");
    }
复制代码

最后运行这个用例,正常通过。

参数化测试

当一个方法有参数时,我们可以批量验证不同参数值,对应的用例是否通过,而不用写多遍类似的代码

1.首先参数化测试,要求我们对测试类添加如下注解

@RunWith(Parameterized.class)
复制代码

2.定义参数集合 - 方法必须定义为 public static 的 - 必须添加@Parameterized.Parameters 3.定义接收参数和期望参数对象 4.增加对应的用例

我们看下面的例子: 首先我们在Calculate 中添加一个有参数的add方法:

public class Calculate {

    private int mValue;
	......
    public int add(int other) {
        mValue += other;
        return mValue;
    }
}
复制代码

接着修改测试类

@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)));
    }
}
复制代码

@1 : 给类添加注解RunWith(Parameterized.class)

@2 : 添加数据集合方法,用@Parameterized.Parameters 注解修饰

@3 : 添加输入参数和期望参数

@4 : 添加构造方法,供给输入参数和期望参数赋值

@5 : 添加测试方法,直接使用输入参数和期望参数进行验证

异常测试

异常验证通过@Test注解参数来指定:

@Test(expected = InvalidParameterException.class)
复制代码

看下面具体的例子:

public class Calculate {

    private int mValue;

    public int addException(int other) {
        if (other < 0) {
            throw  new InvalidParameterException();
        }

        return add(other);
    }
}
复制代码

测试类如下:

@RunWith(Parameterized.class)
public class CalculateTest {

    private Calculate mCalculate;

    @Test(expected = InvalidParameterException.class)
    public void exceptionTest() {
        mCalculate.addException(-1);
    }
}
复制代码

这里可以注意以下几点:

  • expected的异常如果是抛出异常的基类,用例测试也是可以通过的
  • 若没有添加expected参数,则用例会失败

运行用例

  • 运行单个用例方法

运行单个用例方法

点击左侧绿色箭头,会弹出如上图菜单,单机Run 即可执行该用例。

  • 批量执行某个类的所有用例

file

如上图所示,选中测试类文件,右键执行 "Run 类名",就会批量执行该类所有的用例了

  • 批量执行项目所有用例

file

如上图所示,右键包名,执行"Run Test in 包名" 即可执行该包下所有类对应的用例

导出测试报告

在执行完测试用例之后,我们可以导出测试报告,如下图所示:

导出测试报告

查看测试覆盖度

查看测试覆盖度

如上图所示:点击converage按钮,在右边窗口会弹出如下覆盖情况,这里从3个方面统计测试覆盖度:

  • class
  • method
  • Line

最后,我们可以导出覆盖报告.

本文由博客一文多发平台 OpenWrite 发布!

Guess you like

Origin juejin.im/post/6999168679166345230