造完了这个轮子--Mocker,却让我感到很羞愧|项目复盘

有必要声明,这篇文章不是标题党,内容也不是凡尔赛。

作为一个Android开发者,我这5年造了 很多 轮子, 粗略数一下:

  • 基于FastJson修改了一些漏洞,并兼容了ThinkPHP框架 简化JSON结构的机制 -- 单元素数组直接变对象
  • MagicBox -- 注解 + 运行时反射 简化SaveInstanceState样板代码
  • JSBridge -- 基于消息队列机制建立Android和WebView桥接
  • RetrofitExt -- 注解 + APT + 动态代理 实现Retrofit请求生命周期管理
  • DDComponentForAndroid(后改为JIMU)组件化套件
    • Gradle Plugin 优化
    • 路由中间件
    • 消息中间件
    • Maat -- 代码隔离场景下的模块加载框架
  • Pandora套件
    • Pandora -- 适用于复杂列表页面的数据结构
    • Pandorarv -- RecyclerView 多样式表
    • Pandora Plugin -- Intellij 插件,快速生成 数据代理 + ViewHolder + 基础布局
  • Reporter -- 注解生成文档
  • DaVinCi -- 干掉shape xml
  • Mocker -- 注解约束的Mock框架
  • UiBinding -- 取代ButterKnife&更方便的使用ViewBinding、DataBinding

至于自定义视图,RecyclerView相关套件等等,也是一大堆。

其中:

  • 有一些是项目不得不拥有
  • 有一些,是项目中遇到了效率问题,为了解决开发效率问题而开发的,例如Pandora套件
  • 还有一些,纯粹是出于 兴趣好玩 例如 Reporter,利用注解生成文档。

但是,有这么一个轮子,写完之后,让我自己感觉很羞愧,并且 为团队 感到羞愧。标题中提到的 Mocker -- 注解约束的Mock框架


为什么我造了那么多轮子却要复盘这个?

Mocker

  • 这个轮子,和其他的有 本质区别,可以说,别的轮子,都是服务于 业务实现效率,而Mocker,服务于 测试
  • 这是复盘系列的第一篇,我希望利用形式来践行:先思败避开失败点,自然走向成功

我属于 极限编程 的极力反对者,毫无疑问,我认为 单元测试 用例长期有效即具有长效性 ,并值得 持续维护。而我们的项目团队,单元测试覆盖量近乎于1%。

创业团队价值导向快才是好,越快越好快也不是出错的理由 这是目前的团队背景标签。 处于这样的背景下,还身在前端团队,大力推行 TDD 阻力很大,但这不是不作为的理由。

把握测试的粒度

前面提到一句:"大力推行 TDD 阻力很大",Why?

对TDD有一定了解的读者不难理解:设计、编写单测用例的时间 > 开发对应模块功能的时间

打工人资本家难以就此达成理念一致,能够被接受的,只是一个权衡下的结果,这要求我们 把握测试的粒度

我们知道:单元测试 的本质是 对软件中的最小可测试单元进行检查和验证。

而在Android客户端代码中,必然有一道分水岭,隔开了 业务框架

对于框架,引入的三方框架,进行一定的冒烟即可,而对于自研的框架,最好严格的遵循TDD。但是注意,这部分工作和 业务层的测试 是无关的。

不见得会天天折腾框架,但一定天天要处理业务。

对于业务,基本可以分为3块:

  • 视图
  • 业务逻辑
  • 数据

一般而言:

  • 视图层的,集成测试即可,哪怕自己写完了运行下过过眼。
  • 数据层的,对Web-Service进行的测试不需要前端,基于本地DB建立的业务,作为业务逻辑测试。
  • 业务逻辑,把握重点。

另外,项目中会存在各种 数据类代理数据中介者Wrapper,这些类基于基础数据类进行了读写封装,或者功能装饰,也值得进行逻辑测试

测试时的障碍--为什么造这个轮子

在前面的复盘中,我们已经清楚了对哪些内容进行单测,即粒度问题。而我们项目的分层架构,基本是:

  • MVC
  • MVP
  • MVVM
  • 一些变种 如MVI

那么,测试的主要对象就是:C,P,VM,I这些层。而对于这些内容测试时,需要:注入ModelView,这一点无法避免。

如果说,设计时考虑了 可测性,那么至少在这些业务层,考虑了依赖注入依赖抽象。那么在此基础上,我们不用担心 View的复杂性, 因为View层东西都不参与测试,而且不应当干预到测试。

但是Model层不一样。这些业务层的业务就是:

响应视图层交互 -> 执行附加业务 ->操作Model-> .... -> 响应Model的变化 -> 执行附加业务 -> 反馈到视图层显示

而利用真实的Model来进行测试,太重,并且可能对测试产生干扰项。所以 不可避免 的,我们需要 设计Model的状态 并进行 Mock,参与业务层测试。

如果能够以 最小的时间 代价,准确地获得期望的 Model实例,那是极好的。

轮子有了,却没有车

非常遗憾,我花费了半个月的个人时间实现了轮子,转过头来,车并没有来。这一点,我 为团队 感到一丝羞愧。

分明大家都知道,一旦开始考虑TDD:

  • 设计会变得更加合理、健壮
  • 编码会变得目的明确
  • bug会变少、交付质量提升
  • 用例作为资产,可以检验迭代需求的合理性

除此之外,我更为自己感到羞愧,因为 自己没有极力推行证明这样做给团队带来的改变

知耻而后勇

自我鞭策,在项目中真正落地。自我检讨到此为止。

关于项目本身

存在一个和Mock 相对 的一个概念: Bean Validation,Bean Validation 是契约式编程的一种体现,基于契约 对数据类进行数据、状态校验。

而Mock 是基于契约生成假数据,构建数据类,但往往这个契约的 约束内容较少

因为使用场景是单测,所以性能并不是第一要素,我很大胆的尝试了 Kotlin的运行时反射,并第一次在项目中直接使用Unsafe,收益良多。

设计概要

mock的思路还是比较清晰的,大体上需要处理三类事情:

  • 数组和集合
    • 结构性
    • 泛型
    • 元素赋值
  • 数据类
    • 泛型
    • 依赖注入
    • 基本数据类型赋值
  • 深度问题,举个例子:单链表导致mock死循环

在设计契约约束时,采用了 注解表达,例如:IntRange 和 IntDef,可以分别表示 取值范围取值枚举

只需要为 基础数据类型String 设计一个 ,可以表达 范围枚举,并且可以获知 size,即可利用Random,得到随机值。

水到渠成地,在反射数据类时,获取 field 信息以及其注解信息, 在为 基本数据类型及其箱体类型 & String field 赋值时, 先维护类型取值池,再Random取值即可达成:取值受限随机性

而结构型和数据类的构建,对Gson项目比较熟悉的读者就 熟稔于心 了,项目采用了类似实现。

但是在 深度问题 上,我 并没有 想到比较好的方案,目前只是尽力规避了死循环,并在绝大多数场景下,复用了先前创建的对象。

总结

在活动的驱使下,这篇复盘思考了 Mocker项目公司商业应用在单测方面的不足。既然复盘,那就需要 规划改善不足

对于Mocker:

  • 参考 Jsr303Jsr380,提供更广兼容、并且打通顺势造一个Android中轮子,一套注解服务于Mock 和 Bean Validation。
  • 尝试解决深度问题。

对于公司项目,推进好的idea落地。

最后引用一句名人名言:

在需要面前,一切理想主义都是虚伪的 -- 尼采

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情