Android|Android 单元测试 (搭配JsonUnit检查返回数据模型的正确性)
写在前面
近段时间在做项目的api测试,主要是验证返回的数据模型是否正确,最开始的时候直接使用的AssertJ的isInstanceOf
检验一下是否是这个模型,感觉也不是自己想要的效果。后来看了Android单元测试 - 验证函数参数、返回值的正确姿势知道了JsonUnit,一个在单元测试中比较json的库。
JsonUnit里面有许多有用的比较json的方法和可选的配置【Android|Android 单元测试 (搭配JsonUnit检查返回数据模型的正确性)】本文主要测试的是必需的字段不为
null
使用到了注解反射和
JsonUnit
的assertJsonNodePresent
方法以登录接口为例 返回模型
public class ResponseModel {
private String code;
private boolean success;
private String message;
public String getCode() {
return code;
}public void setCode(String code) {
this.code = code;
}public boolean isSuccess() {
return success;
}public void setSuccess(boolean success) {
this.success = success;
}public String getMessage() {
return message;
}public void setMessage(String message) {
this.message = message;
}
}
public class LoginResult extends ResponseModel {private String token;
public String getToken() {
return token;
}public void setToken(String token) {
this.token = token;
}
}
使用Charles的mapLocal模拟返回的
json
数据:{"success": true, "code":200, "token": "eyJ0e06dTo"}
由于
assertJsonNodePresent
方法只能断言模型的一个节点是否存在,所以这里用到了反射来遍历模型所有的字段public class ObjectHelper {private ObjectHelper() {
}/**
* 断言节点是否存在
* @param expect
* @param c
* @throws IllegalAccessException
*/
public static void assertFieldsPresent(Object expect, Class c) throws IllegalAccessException {if (expect == null || c == null) {
throw new NullPointerException("参数不能为空");
}if (expect != null) {
Class clazz = c;
while (!(clazz.equals(Object.class))) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {field.setAccessible(true);
System.out.print(field.getName()+"\t");
assertJsonNodePresent(expect, field.getName());
}System.out.println();
clazz = clazz.getSuperclass();
}
}}}
由于有些非必需的字段可能为
null
,所以断言的主要目标是必需字段,也就是不能为null
的字段。首先我们定义一个
NotNull
的注解import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotNull {
}
对于不能为
null
的字段使用@NotNull
如
LoginResult
的token
不能为null
public class LoginResult extends ResponseModel {@NotNull
private String token;
public String getToken() {
return token;
}public void setToken(String token) {
this.token = token;
}
}
更改我们的方法,只对进行了
NotNull
注解的字段进行断言//...
if (expect != null) {
Class clazz = c;
while (!(clazz.equals(Object.class))) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {field.setAccessible(true);
if (field.isAnnotationPresent(NotNull.class)) {
System.out.print(field.getName() + "\t");
assertJsonNodePresent(expect, field.getName());
}
}System.out.println();
clazz = clazz.getSuperclass();
}
}
//...
当然,有些字段可能用了
@SerializedName
注解,这时候就再判断一下举个例子: 现在我们的LoginResult是这样的
public class LoginResult extends ResponseModel {@NotNull
@SerializedName(value = "https://www.it610.com/article/token")
private String mToken;
public String getToken() {
return mToken;
}public void setToken(String token) {
this.mToken = token;
}
}
修改ObjectHelper
public class ObjectHelper {private ObjectHelper() {
}/**
* 断言节点是否存在
* @param expect
* @param c
* @throws IllegalAccessException
*/
public static void assertFieldsPresent(Object expect, Class c) throws IllegalAccessException {if (expect == null || c == null) {
throw new NullPointerException("参数不能为空");
}if (expect != null) {
Class clazz = c;
while (!(clazz.equals(Object.class))) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {field.setAccessible(true);
if (field.isAnnotationPresent(NotNull.class)) {
if (field.isAnnotationPresent(SerializedName.class)){
System.out.print(field.getAnnotation(SerializedName.class).value() + "\t");
assertJsonNodePresent(expect, field.getAnnotation(SerializedName.class).value());
}else {
System.out.print(field.getName() + "\t");
assertJsonNodePresent(expect, field.getName());
}
}
}System.out.println();
clazz = clazz.getSuperclass();
}
}}}
完整的测试代码:
public class ApiTest {@Rule
public ErrorRule mRule=new ErrorRule();
static LoginResult actual;
@BeforeClass
public static void setUp() throws Exception {
String username="xxx";
String password="xxx";
actual=new LoginCase(username,password).buildUseCaseObservable().toBlocking().last();
}@Test
@JSpec(desc = "通过反射验证")
public void reflection() throws Exception {ObjectHelper.assertFieldsPresent(actual,LoginResult.class);
}}
测试结果
文章图片
result.png 假如服务器返回的数据格式不正确,比如:
{"success": true, "code":200, "mToken": "eyJ0e06dTo"}
运行我们的测试用例就能方便的知道哪个字段出错了
文章图片
result.png Ps: ErrorRule是自定义的JuintRule,作用是显示测试方法成功了还是失败
public class ErrorRule implements TestRule {
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
base.evaluate() ;
System.out.println("@" + description.getMethodName() + "\t--->\t"+description .getAnnotation(JSpec . class).desc()+"\t通过" ) ;
} catch (Throwable t) {
try {
System.err.println("@" + description.getMethodName() + "\t--->\t" + description .getAnnotation(JSpec . class).desc()+"\t未通过\t"+t.getMessage()) ;
} catch (NullPointerException e) {} finally {
throw t;
}}
}
};
}
}
最后 知道单元测试和会写单元测试是一个漫长的过程,笔者知道单元测试是在16年4月份,而开始写单元测试却是在17年了,这中间大多数时间是在完善功能和学习其它的东西,当然对单元测试也积累了很多,所以现在来看单元测试来也不算费力。
而单元测试的重要性相信了解过单元测试的人都很清楚,现在比较常用的MVP的一大优势就是写单元测试比较方便
希望接触单元测试的可以看看小创的单元测试系列关于安卓单元测试,你需要知道的一切,从他那里可以学到很多东西,不只是技术上的。
推荐阅读
- android第三方框架(五)ButterKnife
- Android中的AES加密-下
- 带有Hilt的Android上的依赖注入
- android|android studio中ndk的使用
- Android事件传递源码分析
- RxJava|RxJava 在Android项目中的使用(一)
- Android7.0|Android7.0 第三方应用无法访问私有库
- 深入理解|深入理解 Android 9.0 Crash 机制(二)
- 女生该不该用小号测试男朋友()
- BNC公链|BNC公链 | Eth2.0测试网Topaz已质押超100万枚ETH