研究良久,终于发现了他代码写的快且bug少的原因
前言
读者诸君,今日我们适当放松一下,不钻研枯燥的知识和源码,分享一套高效的摸鱼绝活。
我有一位程序员朋友,当时在一个团队中开发Android应用,历经多次考核后发现:
在组内以及与iOS团队的对比中:
- 他的任务量略多
- 但他的bug数量和严重度均低
- 但他加班的时间又少于其他人
不禁令人产生好奇,他是如何做到代码别的又快,质量又高的
经过多次研究我终于发现了奥秘。
为了行文方便我用"老L"来代指这位朋友。
最常见的客户端bug
"老L,听说昨晚上线,你又坐那摸鱼看测试薅别人,有什么秘诀吗?"
老L:"秘诀?倒也谈不上,你这么说,我倒是有个问题,你觉得平日里最常见的bug有哪些?"
"emm,编码上不健壮的地方,例如NPE,IndexOutOfBoundsException,UI上的可就海了去了,文本长度不一导致显示不下,间距问题,乱七八糟的一大堆"
老L:"哈哈,都是些看起来很幼稚、愚蠢的问题吧?是不是测试挂嘴边的那句:' 你就不能跑一跑吗,你又不瞎,跑两下不就看到了,这么明显!!!' "
我突然来了兴致,"你是说我们有必要上 TDD(test-driven-develop),按照DevOps思想,在CI(Continuous Integration)的时候,顺带跑自动化测试用例发现问题?"
老L突然打断了我:"不要拽你那些词了,记住了,事情是要人干的,机器只能替代可重复劳动,现在还不能替代人的主观能动性,拽词并不能解决问题。我们已经找到了第一个问题的答案,现在换个角度"
平日里最常见的bug有哪些?
- 编码不健壮, 例如NPE,IndexOutOfBoundsException
- UI细节问题, 例如文本长度不一导致显示不下,间距,等
为什么很浅显的问题没有被发现
老L:"那么问题来了,为什么这些浅显的问题,在交测前没有被发现呢?"
我陷入了思考...
是开发们都很懒吗?也不至于啊!
是时间很紧来不及吗?确实节奏紧张,但也不至于不给调试就拿去测了!
"emm, 可能是迭代的节奏的太频繁,压力较大,并没有整块的时间用来自测联调"
老L接过话茬,"假定你说的是正确的,那么就有两种可能。"
"第一种,自测与联调要比开发还要耗费心思的一件事情。但实际上,你我都知道,这一点并站不住脚!"
"而第二种,就是在开发阶段无法及时测试,拖到开发完,简单测测甚至被催促着就交差了"
仔细的思考后
- 业务逐步展开,无法在任意时间自由地进行有效的集成测试
- 后端节奏并不比前端快多少,在前端的开发阶段,难以借助后端接口测试,也许接口也有问题
"确实,这是一个挺麻烦的问题,听你一说,我感觉除了多给几天,开发完集中自测一波才行" 我如是说到。
"NO NO NO",老L又打断了我:"你想的过多了,你想借助一个可靠的、已经完备的后端系统来进行自测。对于你的需求来说,这个要求过高了,你这是准备干QA的活"
"我帮你列举一下情况"
- 一些数据处理的算法,这种没有办法,老老实实写单元测试,在开发阶段就可以做好,保障可靠性
- UI呢,我们现在写的代码,基本都做到了UI与逻辑分层,只要能模拟数据,就能跑起来看页面
- 业务层,后端逻辑我们无法控制,但 Web-API 调用的情况可以分析下并做一下测试,而对于返回数据的JSON结构校验、约束性校验也可以考虑做一下测试
总而言之,我们只需要先排除掉浅显的错误。而这些浅显的错误,属于情况2、3
老L接着说道:"你先歇歇吧,我来说,你再插嘴这文章就太长了!"
接下来就可以实现矛盾转移:"如何模拟数据进行测试",准确的说,问题分成两个子问题:
- 如何生成模拟数据
- 如何从接缝中塞入数据,让系统得以使用
可能存在的接缝
先看问题2:"如何从接缝中塞入数据,让系统得以使用"
脑暴一下,可以得出结论:
- 应用内部
- 替换调用web-api的业务模块,使用假数据调用业务链,一般替换Presenter、Controller实例
- 替换Model层,不调用web-api,返回假数据或用假数据调用回调链
- 侵入网络层实现,不进行实际网络层交互,直接使用假数据
- 遵循切面,向缓存等机制模块中植入假数据
- 应用外部
- 使用代理,返回假数据
- 假数据服务器
简单分析:
"假数据服务器"的代价太大,并且使用逻辑编造假数据的代价太大,过。- "使用代理,返回假数据",可以用于特定问题的调试,不适用广泛情况,过。
"替换调用web-api的业务模块",成本过大,过。- "替换Model层",对项目的依赖注入管理具有较大挑战,备选,可能带来很多冗余代码。
- "侵入网络层实现",优选。
- "向缓存等机制模块中植入假数据",操作真实的缓存较复杂,但可以考虑增加一个 Mock缓存实现模块,基于SPI等机制,可以解决冗余代码问题,备选。
得出结论:
- 方案1:"侵入网络层实现",优选
- 方案2:"替换Model层",(项目的依赖注入做得很好时)作为备选,可能带来冗余代码
- 方案3:"向缓存等机制模块中植入假数据",增加一个 Mock缓存实现模块,备选。(基于SPI等机制,可以解决冗余代码问题)
再仔细分析: 方案1和方案3可以合并,形成一个完整的方案,但未必需要限定在缓存机制中
OK 我们先搁置一下这个问题,看前一个问题。
创造假数据
简单脑暴一下,无非三种:
- 人工介入,手动编写 -- 成本过大
- 可能在前期准备好,基本是纯文本
- 可能使用一个交互工具,在需要数据时介入,通过图形化操作和输入产生数据
- 人工介入,逻辑编码
- 基于反射等自省机制,并完全随机或者基于限制生成数据
"第一种代价过大,暂且抛弃"
"第二种可以采用,但是人力成本不容忽视! 一个可以说服我使用它的理由是:"可以精心设计单测数据,针对性的发现问题"
"第三种很轻松,例如使用Mockito,但生成合适的数据需要花费一定的精力"
我们来扒一扒第三种方式,其核心思想为:
- 获取类信息,得到属性集
- 遍历属性填充 >
- 基础类型、箱体类型,枚举,确定取值范围,使用Random取值,赋值
2. 普通类、泛型类,创建实例,回归步骤1 3. 集合、数组等,创建实例,回归步骤1,收集填充
不难得出结论,这一方法虽然很强大,但 创建高度定制化的数据
是一件有挑战的事情。
举个例子,模拟字符串时,一般会使用语料集作为枚举,进行取值。要得到“地址”、“邮箱”等特定风格的数据,需要结合框架做配置,客观上存在较高地学习、使用门槛。
你也知道,前几年我图好玩,写了个 mock库 。
必须强调的一点:“我并不认为我写的库比Mockito等库强大,仅仅是在我们开发人员够用的基础上,做到尽可能简单!”
你也知道,Google 在Androidx(前身为support)中提供了一套注解包: annotations。但Google并未提供bean validation 实现
,我之前也基于此做过一套JSR303实现,有一次突发灵感,这套注解的含义同样适用于 声明假数据取值范围
!!!
所以,我能使用它便捷的生成合适的假数据,在开发阶段及时的进行 “伪集成”
此刻,我再也忍不住要发言了:“且慢,老L,你这个做法有一定的侵入性吧。而且,如果数据类在不同业务下复用的话,是否存在问题呢?”
老L顿了顿,“确实,google的annotations是源码级注解,并不是运行时,我为了保持简单,使用了运行时反射而非代码生成。所以确实存在一定的代码侵入性”。
但是,我们可以基于此建立一套简单的MOCK-API,这样就不存在代码侵入了。
另外,也可以增加一套Annotation-Processor 实现方案,这样就可以适当沿用项目中的注解约束了,但我个人认为华而不实。
看你的第二个问题,Mocker一开始确实存在这个问题,有一次从Spring的JSR380中得到灵感,我优化了注解规则,这个问题已经被解决了。得空你可以顺着这个图看看:
或者去看看代码和使用说明:https://github.com/leobert-lan/Mocker
再次审视如何处理接缝
此时我已经有点云里雾里,虽然听起来很牛,如何用起来呢?我还是很茫然,简直人麻了!不得不再次请教。
老L笑着说:“你问的是一个实践方案的问题,而这类问题没有银弹.不同的项目、不同的习惯都有最适宜的方法,我只能分享一下我的想法和做法,仅做参考”
在之前的项目中,我自己建了一个Mock-API,利用我的Mocker库,写一个假数据接口就是分分钟的事情。
测试机挂上charles代理,有需要的接口直接进行mapping,所以在客户端代码中,你看不到我做了啥。
当然,这个做法是在软件外部。
如果要在软件内部做,我个人认为这也是一个华而不实的事情。不过不得不承认是一件好玩的事情,那就提一些思路。
基于Retrofit的CallAdapter
public interface CallAdapter<R, T> {
Type responseType();
T adapt(Call<R> call);
abstract class Factory {
public abstract @Nullable
CallAdapter<?, ?> get(Type returnType, Annotation[] annotations,
Retrofit retrofit);
protected static Type getParameterUpperBound(int index,
ParameterizedType type) {
return Utils.getParameterUpperBound(index, type);
}
protected static Class<?> getRawType(Type type) {
return Utils.getRawType(type);
}
}
}
很明显,我们可以追加注解,用以区分是否需要考虑mock;
可选:对于有可能需要mock的接口,可以继续追加切面,实现在软件外部控制使用 mock数据
或 真实数据
而Retrofit已经使用反射确定了方法的 return Type
,在Mocker中也有适应的API直接生成假数据
基于Retrofit的Interceptor
相比于上一种,拦截器已经在Retrofit处理流程中靠后,此时在 Chain
中能够得到的内容已经属于Okhttp库的范畴。
所以需要一定的前置措施用于确定 "return Type"、"是否需要Mock" 等信息。可以借助Tag机制:
@Documented
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface Tag {
}
@GET("/")
Call<ResponseBody> foo(@Tag String tag);
最终从 Request#tag(type: Class<out T>): T?
方式获取,并接入mock,并生成 Response
其他针对Okhttp的封装
思路基本类似,不再展开。
写在最后
听完老L的思路,我若有所思,若有所悟。他的方案似乎很有效,而且直觉告诉我,这些方案中还有很多留白空间,例如:
- 借用SPI等技术思路,可以轻易的解决 "Mock 模块集成与移除" 的问题
- 提前外部控制是否Mock的接缝,可以在加一个工具APP、或者Socket+网页端工具 用以实现控制
但我似乎遗漏了问题的开始
是否原意做 用于约束假数据生成规则的基础建设工作呢??? 例如维护注解
事情终究是人干的,人原意做,办法总比困难多。
最后一个小问题: