testable-mock/docs/zh-cn/doc/omni-constructor.md
2021-03-28 14:47:00 +08:00

6.7 KiB
Raw Blame History

快速构造复杂的参数对象

在单元测试中,测试数据的准备和构造是一件既必须又繁琐的任务,面向对象的层层封装,在测试时就成为了初始化对象状态的重重阻碍。尤其遇到类型结构嵌套复杂、没有合适的构造方法、需要使用私有内部类等等状况时,常规手段往往显得力不从心。

为此TestableMock提供了OmniConstructorOmniAccessor两个极简的工具类,从此让一切对象构造不再困难。

1. 一行代码构造任何对象

不论目标类型多么奇葩,呼唤OmniConstructor,马上递给您~ 万能的对象构造器OmniConstructor有两个静态方法:

  • newInstance(任意类型) ➜ 指定任意类型,返回一个该类型的对象
  • newArray(任意类型, 数组大小) ➜ 指定任意类型,返回一个该类型的数组

用法举例:

// 构造一个WhatEverClass类型的对象
WhatEverClass obj = OmniConstructor.newInstance(WhatEverClass.class);
// 构造一个WhatEverClass[]类型容量为5的数组
WhatEverClass[] arr = OmniConstructor.newArray(WhatEverClass.class, 5);

不仅如此,OmniConstructor构造出的绝非是所有成员值为null的简单空对象,而是所有成员、以及所有成员的所有子成员,都已经依次递归初始化的"丰满"对象。相比使用new进行构造,OmniConstructor能够确保对象结构完整,避免测试数据部分初始化导致的NullPointerException问题。

// 使用构造函数创建对象
Parent parent = new Parent();
// 内部成员未初始化直接访问报NullPointerException异常
parent.getChild().getGrandChild();

// 使用OmniConstructor创建对象
Parent parent = OmniConstructor.newInstance(Parent.class);
// 无需顾虑,安心访问任意子成员(✅)
parent.getChild().getGrandChild().getContent();

注意:在0.6.0版本中,类型为接口或抽象类的成员字段依然会被初始化为null,此问题将在后续版本中修复

除了用于构造方法的入参,OmniConstructor也可以用于快速构造Mock方法的返回值相比将null作为Mock方法的返回值使用完全初始化的对象能够更好保障测试的可靠性。

java-demokotlin-demo示例项目的DemoOmniMethodsTest测试类中,详细展示了当目标类型有多层嵌套结构、构造方法无法正常使用,甚至没有公开的构造方法时,如何用OmniConstructor轻松创建所需对象。

2. 一行代码访问任意深度成员

对于测试数据而言,即使是结构复杂的参数对象,与特定测试用例有关的通常也只是其中的部分属性和状态,然而要为这些深藏在对象结构内部的字段赋值有时却并非易事。

做为PrivateAccessor功能的加加加强版,OmniAccessor的灵感来自于XML语言中的XPath节点选择器,它提供了getset两个主要的静态方法:

  • get(任意对象, "访问路径") ➜ 返回根据路径匹配搜索到的所有成员对象
  • set(任意对象, "访问路径", 新的值) ➜ 根据路径匹配为指定位置的对象赋值

还有一个用于精确路径匹配时直接获取唯一目标对象的getFirst()辅助方法,其作用等效于OmniAccessor.get(...).get(0)

  • getFirst(任意对象, "访问路径") ➜ 返回根据路径匹配搜索到的第一个成员对象

只需书写符合规则的访问路径,不论什么类型和深度的成员,都可以一键直达:

// 返回parent对象中所有符合类型是GrandChild的子对象中叫做content的成员对象
OmniAccessor.get(parent, "{GrandChild}/content");
// 将parent对象中符合名称为children的数组第3位的任意子成员的value字段赋值为100
OmniAccessor.set(parent, "children[2]/*/value", 100);

具体路径规则如下:

1. 匹配成员名

不带额外修饰的路径名将匹配与之同名的任意成员对象

  • child: 匹配任意名字为child的子孙成员
  • child/grandChild: 匹配名字为child的子孙成员里,名为grandChild的子成员

2. 匹配成员类型

使用花括号匹配类型名称,通常用于批量获取或赋值同类的多个成员对象

  • {Child}: 匹配所有类型是Child的子孙成员
  • {Children[]}: 匹配所有类型是Children数组的子孙成员
  • {Child}/{GrandChild}: 匹配所有类型是Child的子孙成员里,所有类型是GrandChild子成员

成员名和类型可以在路径上混用(暂不支持在同一级路径同时指定成员名称和类型)

  • child/{GrandChild}: 匹配名字为child的子孙成员里,所有类型是GrandChild的子成员
  • {Child}/grandChild/content: 匹配所有类型是Child的子孙成员里,名为grandChild子成员里的,名为content的子成员

3. 使用下标访问数组成员

使用带数值的方括号表示匹配该位置为数组类型,且取指定下标的对象(不带下标时,当匹配对象为数组类型,默认匹配数组中的所有对象)

  • children[1]/content: 匹配名称为children的数组类型子孙成员,取其中第2个对象中名为content的子成员
  • parent/children[1]: 匹配名称为parent的子孙成员里,名为children的数组类型子成员,取其中第2个对象

4. 使用通配符

通配符可以用于成员名或类型名的匹配

  • child*: 匹配名称以child开头的所有子孙成员
  • {*Child}: 匹配类型以Child结尾的所有子孙成员
  • c*ld/{Grand*ld}: 匹配名称以c开头ld结尾的子孙成员里,类型以Grand开头ld结尾的成员
  • child/*/content: 此时*将匹配任意成员,即child对象任意子成员中,包含的content子成员

详见java-demokotlin-demo示例项目DemoOmniMethodsTest测试类中的用例。

3. 特别说明

你真的需要用到OmniAccessor吗?

OmniAccessor具有基于Fail-Fast机制的防代码重构能力当用户提供的访问路径无法匹配到任何成员时OmniAccessor将立即抛出NoSuchMemberError错误,使单元测试提前终止。然而相比常规的成员访问方式,OmniAccessor在IDE重构方面的支持依然偏弱。

对于复杂对象的内容赋值,大多数情况下,我们更推荐使用构造者模式或者暴露Getter/Setter方法实现。这些常规手段虽然稍显笨拙尤其在需要为许多相似的成员批量赋值的时候但对业务逻辑的封装和重构都更加友好。 仅当原类型不适合改造,且没有其它可访问目标成员的方法时,OmniAccessor才是最后的终极手段。