Android 单元测试实践
什么是单元测试
定义:单元测试就是针对最小的功能单元编写测试代码
Java程序最小的功能单元是方法,因此,对Java程序进行单元测试就是针对单个Java方法的测试。
什么是JUnit
JUnit是一个开源的Java语言的单元测试框架,专门针对Java设计,使用最广泛。JUnit是事实上的单元测试的标准框架,任何Java开发者都应当学习并使用JUnit编写单元测试。
使用JUnit编写单元测试的好处在于:
- 非常简单地组织测试代码,并随时运行它们
- JUnit会给出成功的测试和失败的测试,还可以生成测试报告
- 不仅包含测试的成功率,还可以统计测试的代码覆盖率,即被测试的代码本身有多少经过了测试。
- 几乎所有的IDE工具都集成了JUnit,这样我们就可以直接在IDE中编写并运行JUnit测试
对于高质量的代码来说,测试覆盖率应该在80%以上。
单元测试的好处
- 单元测试可以确保单个方法按照正确预期运行。如果修改了某个方法的代码,只需确保其对应的单元测试通过,即可认为改动正确。
- 测试代码本身就可以作为示例代码,用来演示如何调用该方法
在编写单元测试的时候,我们要遵循一定的规范:
- 单元测试代码本身必须非常简单,能一下看明白,决不能再为测试代码编写测试;
- 每个单元测试应当互相独立,不依赖运行的顺序;
- 测试时不但要覆盖常用测试用例,还要特别注意测试边界条件,例如输入为0,null,空字符串””等情况。
线上更多暴露的都是异常场景,所以在单元测试中有必要重点验证相关异常逻辑。
如何编写单元测试
添加依赖
新建Android项目中app模块的build.gradle中会自动添加如下依赖:
testImplementation \'junit:junit:4.12\'
androidTestImplementation \'androidx.test.ext:junit:1.1.3\'
androidTestImplementation \'androidx.test.espresso:espresso-core:3.4.0\'
- testImplementation :表示Junit单元测试依赖,对应的是test目录
- 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”:
如果之前该类没有创建过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按钮,会让你选择创建单元测试用例 还是 集成测试用例,如下图所示:
这里我们选择单元测试用例。 然后我们就会在test目录下找到对应的包名和测试文件了,如下图所示:
注解
单元测试的时候用的最多的是上面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);
}
}
- 我们首先声明一个Calculate类型的变量mCalculate
- 我们在setUp中构造一个Calculate对象实例,赋值给mCalculate
- 在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 即可执行该用例。
- 批量执行某个类的所有用例
如上图所示,选中测试类文件,右键执行 “Run 类名”,就会批量执行该类所有的用例了
- 批量执行项目所有用例
如上图所示,右键包名,执行”Run Test in 包名” 即可执行该包下所有类对应的用例
导出测试报告
在执行完测试用例之后,我们可以导出测试报告,如下图所示:
查看测试覆盖度
如上图所示:点击converage按钮,在右边窗口会弹出如下覆盖情况,这里从3个方面统计测试覆盖度:
- class
- method
- Line
最后,我们可以导出覆盖报告.
本文由博客一文多发平台 OpenWrite 发布!