mirror of
https://github.com/alibaba/testable-mock.git
synced 2025-01-09 20:00:21 +08:00
add english doc for omni methods
This commit is contained in:
parent
e370114b01
commit
929abffb66
@ -1,6 +1,113 @@
|
||||
Parameter constructor
|
||||
Parameter Constructor
|
||||
---
|
||||
|
||||
No matter how intricate the parameter structure required by the method under test is, even there is no suitable construction method, or even there are private internal class objects... Call `TestableMock`, the parameter object will be handed to you immediately~
|
||||
In unit testing, the preparation and construction of test data is a necessary and tedious task. Object-oriented layer-by-layer encapsulation becomes an obstacle to initializing the state of the object during testing. Especially when the type structure is complicated, there is no suitable construction method, or some fields need to use private inner classes, etc. Using conventional methods to construct those class often appear to be inadequate.
|
||||
|
||||
This feature is planned to be released in the `0.6` version.
|
||||
For this reason, `TestableMock` provides two minimalist tool classes, `OmniConstructor` and `OmniAccessor`, which makes the construction of any object no longer difficult.
|
||||
|
||||
### 1. Construct any object with one line of code
|
||||
|
||||
No matter how special the target type is, `OmniConstructor` will hand it to you immediately~~ The universal object constructor `OmniConstructor` has two static methods:
|
||||
|
||||
- `newInstance(<AnyClass>)` ➜ Specify any type, and return an object of that type
|
||||
- `newArray(<AnyClass>, <ArraySize>)` ➜ Specify any type, and return an array of that type
|
||||
|
||||
Usage example:
|
||||
|
||||
```java
|
||||
// Construct a object of "WhatEverClass" type
|
||||
WhatEverClass obj = OmniConstructor.newInstance(WhatEverClass.class);
|
||||
// Construct a array of "WhatEverClass[]" type with capability of 5
|
||||
WhatEverClass[] arr = OmniConstructor.newArray(WhatEverClass.class, 5);
|
||||
```
|
||||
|
||||
Beside that, object constructed by `OmniConstructor` is not just a simple empty object with all member values of `null`, but a "fullness" object in which all members and all sub-members of all members have been recursively initialized. Compared with using `new` operation, `OmniConstructor` can ensure the integrity of the object structure and avoid the `NullPointerException` problem caused by partial initialization of test data.
|
||||
|
||||
```java
|
||||
// Construct object using new operation
|
||||
Parent parent = new Parent();
|
||||
// Inner member is not initialized, will cause NullPointerException (❌)
|
||||
parent.getChild().getGrandChild();
|
||||
|
||||
// Construct object using OmniConstructor
|
||||
Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
// No need to worry, visit any child member safely (✅)
|
||||
parent.getChild().getGrandChild().getContent();
|
||||
```
|
||||
|
||||
> **Note**: In the `0.6.0` version, the member fields of type interface or abstract class will still be initialized to `null`, this problem will be fixed in subsequent versions
|
||||
|
||||
In addition to use as input parameters of the method under test, `OmniConstructor` can also be used to quickly construct the return value of the mock method. Compared to using `null` as the return value of the mock method, using a fully initialized object can better guarantee the reliability of the test .
|
||||
|
||||
In the `DemoOmniMethodsTest` test class of the `java-demo` and `kotlin-demo` sample projects, it is shown in detail how `OmniConstructor` could be used when the target type has a multi-layered nested structure, the construction method is throwing exception, and even without public construction method available.
|
||||
|
||||
### 2. Access any inner member with one line of code
|
||||
|
||||
For test data, even with complex structure, it is usually only part of its attributes and states that are related to a specific test case. However, it is sometimes not easy to assign values to these fields deep wrapped in the object structure.
|
||||
|
||||
As an enhanced version of the `PrivateAccessor` tool, `OmniAccessor` is inspired by the [XPath node selector](https://www.w3schools.com/xml/xpath_syntax.asp) in the `XML` language, It provides two main static methods of `get` and `set`:
|
||||
|
||||
- `get(arbitrary object, "access path")` ➜ returns all member objects searched for based on path-matching
|
||||
- `set(arbitrary object, "access path", new value)` ➜ Assign a value to any objects based on path-matching
|
||||
|
||||
There is also a `getFirst()` method used to directly obtain the unique target object during exact path matching. Its function is equivalent to `OmniAccessor.get(...).get(0)`:
|
||||
|
||||
- `getFirst(arbitrary object, "access path")` ➜ returns the first member object searched based on path-matching
|
||||
|
||||
You only need to write the access path that meets the rules, no matter what type and depth of members, you can directly reach them with one line of code:
|
||||
|
||||
```java
|
||||
// Get all field of the parent object, which named as content and inside type GrandChild
|
||||
OmniAccessor.get(parent, "{GrandChild}/content");
|
||||
// Assign 100 to any fields which named as value and inside any child member that matches the 3rd item of the array named children
|
||||
OmniAccessor.set(parent, "children[2]/*/value", 100);
|
||||
```
|
||||
|
||||
The path rules are as follows:
|
||||
|
||||
**1. Matching member name**
|
||||
|
||||
The path name without additional decoration will match any member object with the same name
|
||||
|
||||
- `child`: match any descendant member whose name is `child`
|
||||
- `child/grandChild`: matches the child member named `grandChild` among the descendants of the name `child`
|
||||
|
||||
**2. Matching member type**
|
||||
|
||||
Use curly braces to match the type name, usually used to obtain or assign multiple member objects of the same type in batches
|
||||
|
||||
- `{Child}`: match all descendants of `Child`
|
||||
- `{Children[]}`: match all descendants of the `Children` array
|
||||
- `{Child}/{GrandChild}`: match all descendant members of `Child`, all types are children of `GrandChild`
|
||||
|
||||
The member name and type can be mixed on the path (currently it is not supported to specify the member name and type at the same time in the same level path)
|
||||
|
||||
- `child/{GrandChild}`: match all descendant members whose name is `child`, all types are child members of `GrandChild`
|
||||
- `{Child}/grandChild/content`: match all descendant members whose type is `Child`, the child members named `grandChild`, and the child members named `content`
|
||||
|
||||
**3. Use subscripts to access array members**
|
||||
|
||||
Use square brackets with numerical values to indicate that the matching position is an array type, and the object with the specified subscript is taken (without subscript, when the matching object is an array type, all objects in the array are matched)
|
||||
|
||||
- `children[1]/content`: match the descendant members of the array type named `children`, and take the child member named `content` in the `2`th object
|
||||
- `parent/children[1]`: match the child member of the array type named `children` among the descendant members named `parent`, and take the `2`th object among them
|
||||
|
||||
**4. Use wildcards**
|
||||
|
||||
Wildcards can be used to match member names or type names
|
||||
|
||||
- `child*`: match all descendant members whose name starts with `child`
|
||||
- `{*Child}`: match all descendant members whose type ends with `Child`
|
||||
- `c*ld/{Grand*ld}`: match the descendant members whose name starts with `c` and ends with `ld`, and the members whose type starts with `Grand` and ends with `ld`
|
||||
- `child/*/content`: At this time, `*` will match any member, that is, the child member of `content` contained in any child member of the `child` object
|
||||
|
||||
For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoOmniMethodsTest`.
|
||||
|
||||
### 3. Special instructions
|
||||
|
||||
> **Do you really need to use `OmniAccessor`? **
|
||||
>
|
||||
> `OmniAccessor` implement the basic anti-refactoring mechanism based on the Fail-Fast principle. When the access path provided by the user cannot match any member, the `OmniAccessor` will immediately throw a `NoSuchMemberError` error, so that the unit test is terminated early. However, compared to the conventional member access method, the support of `OmniAccessor` in IDE refactoring is still weak.
|
||||
>
|
||||
> For content assignment of complex objects, in most cases, we recommend using [Builder Pattern](https://www.geeksforgeeks.org/builder-pattern-in-java/), or exposing Getter/Setter method implementations. Although these conventional methods are slightly clumsy (especially when you need to assign values to many similar members in batches), they are more friendly to the encapsulation and reconstruction of business logic.
|
||||
> Only when the original type is not suitable for transformation, and there is no other way to access the target member, `OmniAccessor` is the last resort.
|
||||
|
@ -1,47 +1,51 @@
|
||||
快速构造复杂的参数对象
|
||||
---
|
||||
|
||||
在单元测试中,测试数据的准备和构造是一件既必须又繁琐的任务,尤其遇到被测函数的入参类型结构复杂、没有合适的构造方法、成员对象使用私有内部类的时候,常规方法往往无处下手。为此`TestableMock`提供了`OmniConstructor`和`OmniAccessor`两个极简的工具类,从此让一切对象构造不再困难。
|
||||
在单元测试中,测试数据的准备和构造是一件既必须又繁琐的任务,面向对象的层层封装,在测试时就成为了初始化对象状态的重重阻碍。尤其遇到类型结构嵌套复杂、没有合适的构造方法、需要使用私有内部类等等状况时,常规手段往往显得力不从心。
|
||||
|
||||
为此`TestableMock`提供了`OmniConstructor`和`OmniAccessor`两个极简的工具类,从此让一切对象构造不再困难。
|
||||
|
||||
### 1. 一行代码构造任何对象
|
||||
|
||||
万能的对象构造器`OmniConstructor`有两个静态方法:
|
||||
不论目标类型多么奇葩,呼唤`OmniConstructor`,马上递给您~ 万能的对象构造器`OmniConstructor`有两个静态方法:
|
||||
|
||||
- `newInstance(任意类型)` ➜ 指定任意类型,返回一个该类型的对象
|
||||
- `newArray(任意类型, 数组大小)` ➜ 指定任意类型,返回一个该类型的数组
|
||||
|
||||
例如:
|
||||
用法举例:
|
||||
|
||||
```java
|
||||
// 构造一个ComplicatedClass类型的对象
|
||||
ComplicatedClass obj = OmniConstructor.newInstance(ComplicatedClass.class);
|
||||
// 构造一个ComplicatedClass[]类型,容量为5的数组
|
||||
ComplicatedClass[] arr = OmniConstructor.newArray(ComplicatedClass.class, 5);
|
||||
// 构造一个WhatEverClass类型的对象
|
||||
WhatEverClass obj = OmniConstructor.newInstance(WhatEverClass.class);
|
||||
// 构造一个WhatEverClass[]类型,容量为5的数组
|
||||
WhatEverClass[] arr = OmniConstructor.newArray(WhatEverClass.class, 5);
|
||||
```
|
||||
|
||||
值得一提的是,使用`OmniConstructor`构造出来的并非是一个所有成员值为`null`的简单空对象。该对象的所有成员,以及所有成员的所有子成员,都会在构造时被依次递归赋值。相比直接用`new`构造的对象,使用`OmniConstructor`能够确保对象完全初始化,无需担心测试过程中发生`NullPointerException`问题。
|
||||
|
||||
> **注意**:在`0.6.0`版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在近期版本修复
|
||||
不仅如此,`OmniConstructor`构造出的绝非是所有成员值为`null`的简单空对象,而是所有成员、以及所有成员的所有子成员,都已经依次递归初始化的"丰满"对象。相比使用`new`进行构造,`OmniConstructor`能够确保对象结构完整,避免测试数据部分初始化导致的`NullPointerException`问题。
|
||||
|
||||
```java
|
||||
// 使用构造函数创建对象
|
||||
Parent parent = new Parent();
|
||||
// 内部成员未初始化,直接访问报NullPointerException异常
|
||||
// 内部成员未初始化,直接访问报NullPointerException异常(❌)
|
||||
parent.getChild().getGrandChild();
|
||||
|
||||
// 使用OmniConstructor创建对象
|
||||
Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
// 无需顾虑,安心访问任意子成员
|
||||
// 无需顾虑,安心访问任意子成员(✅)
|
||||
parent.getChild().getGrandChild().getContent();
|
||||
```
|
||||
|
||||
> **注意**:在`0.6.0`版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在后续版本中修复
|
||||
|
||||
除了用于构造方法的入参,`OmniConstructor`也可以用于快速构造Mock方法的返回值,相比将`null`作为Mock方法的返回值,使用完全初始化的对象能够更好保障测试的可靠性。
|
||||
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoOmniMethodsTest`测试类中的用例。
|
||||
在`java-demo`和`kotlin-demo`示例项目的`DemoOmniMethodsTest`测试类中,详细展示了当目标类型有多层嵌套结构、构造方法无法正常使用,甚至没有公开的构造方法时,如何用`OmniConstructor`轻松创建所需对象。
|
||||
|
||||
### 2. 一行代码访问任意深度成员
|
||||
|
||||
在单元测试中,有时会遇到一些结构复杂的参数对象,但与特定测试用例有关的仅仅是该对象结构深处的个别几个属性和状态。`OmniAccessor`的灵感来自于`XML`语言中的`xpath`节点选择器,它有`get`、`set`两个主要的静态方法:
|
||||
对于测试数据而言,即使是结构复杂的参数对象,与特定测试用例有关的通常也只是其中的部分属性和状态,然而要为这些深藏在对象结构内部的字段赋值有时却并非易事。
|
||||
|
||||
做为`PrivateAccessor`功能的加加加强版,`OmniAccessor`的灵感来自于`XML`语言中的[XPath节点选择器](https://www.w3school.com.cn/xpath/xpath_syntax.asp),它提供了`get`、`set`两个主要的静态方法:
|
||||
|
||||
- `get(任意对象, "访问路径")` ➜ 返回根据路径匹配搜索到的所有成员对象
|
||||
- `set(任意对象, "访问路径", 新的值)` ➜ 根据路径匹配为指定位置的对象赋值
|
||||
@ -76,7 +80,7 @@ OmniAccessor.set(parent, "children[2]/*/value", 100);
|
||||
- `{Children[]}`: 匹配所有类型是`Children`数组的子孙成员
|
||||
- `{Child}/{GrandChild}`: 匹配所有类型是`Child`的子孙成员里,所有类型是`GrandChild`子成员
|
||||
|
||||
成员名和类型可以在路径上混用,但暂不支持在同一级路径既指定成员名称又指定类型的写法
|
||||
成员名和类型可以在路径上混用(暂不支持在同一级路径同时指定成员名称和类型)
|
||||
|
||||
- `child/{GrandChild}`: 匹配名字为`child`的子孙成员里,所有类型是`GrandChild`的子成员
|
||||
- `{Child}/grandChild/content`: 匹配所有类型是`Child`的子孙成员里,名为`grandChild`子成员里的,名为`content`的子成员
|
||||
@ -102,10 +106,8 @@ OmniAccessor.set(parent, "children[2]/*/value", 100);
|
||||
### 3. 特别说明
|
||||
|
||||
> **你真的需要用到`OmniAccessor`吗?**
|
||||
>
|
||||
>
|
||||
> `OmniAccessor`具有基于Fail-Fast机制的防代码重构能力,当用户提供的访问路径无法匹配到任何成员时,`OmniAccessor`将立即抛出`NoSuchMemberError`错误,使单元测试提前终止。然而相比常规的成员访问方式,`OmniAccessor`在IDE重构方面的支持依然偏弱。
|
||||
>
|
||||
> 对于复杂对象的内容赋值,大多数情况下,我们更推荐使用[构造者模式](https://developer.aliyun.com/article/705058),或者暴露Getter/Setter方法实现。这些常规手段虽然稍显笨拙(尤其在需要为许多相似的成员批量赋值的时候),但对业务逻辑的封装和重构都更加友好。
|
||||
> 仅当原类型不适合改造,且没有其它可访问目标成员的方法时,`OmniAccessor`才是最后的终极手段。
|
||||
>
|
||||
> 出于相同的原因,我们并不推荐在除单元测试之外的场景使用`OmniAccessor`方式来读写业务类的成员字段(虽然技术上可行)。
|
||||
|
Loading…
Reference in New Issue
Block a user