Android|Android 单元测试 (搭配JsonUnit检查返回数据模型的正确性)

写在前面 近段时间在做项目的api测试,主要是验证返回的数据模型是否正确,最开始的时候直接使用的AssertJ的isInstanceOf检验一下是否是这个模型,感觉也不是自己想要的效果。后来看了Android单元测试 - 验证函数参数、返回值的正确姿势知道了JsonUnit,一个在单元测试中比较json的库。

JsonUnit里面有许多有用的比较json的方法和可选的配置
【Android|Android 单元测试 (搭配JsonUnit检查返回数据模型的正确性)】本文主要测试的是必需的字段不为null
使用到了注解反射和JsonUnitassertJsonNodePresent方法
以登录接口为例 返回模型
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
LoginResulttoken不能为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); }}

测试结果
Android|Android 单元测试 (搭配JsonUnit检查返回数据模型的正确性)
文章图片
result.png 假如服务器返回的数据格式不正确,比如:
{"success": true, "code":200, "mToken": "eyJ0e06dTo"}

运行我们的测试用例就能方便的知道哪个字段出错了

Android|Android 单元测试 (搭配JsonUnit检查返回数据模型的正确性)
文章图片
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的一大优势就是写单元测试比较方便
希望接触单元测试的可以看看小创的单元测试系列关于安卓单元测试,你需要知道的一切,从他那里可以学到很多东西,不只是技术上的。

    推荐阅读