8.5 KiB
Parameter Constructor
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.
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 typenewArray(<AnyClass>, <ArraySize>)
➜ Specify any type, and return an array of that type
Usage example:
// 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.
// 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 1: In the current version, the member fields of type interface or abstract class will still be initialized to
null
, this problem will be fixed in the future.Note 2: Based on the light-weight principle, in the default mode,
OmniConstructor
will only uses the original constructor of the class to create objects. For POJO and most model layer objects, this mode has been able to meet the needs. But for more complex situations, such as when certain class have constructors throwing exceptions or contain other statements that hinder the normal execution of the construction, the object construction may fail. In those situations, you can use the Testable global configurationomni.constructor.enhance.enable = true
to enable bytecode enhancement mode ofOmniConstructor
, in this mode, any Java class can be constructed.
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 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-matchingset(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:
// 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 ischild
child/grandChild
: matches the child member namedgrandChild
among the descendants of the namechild
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 ofChild
{Children[]}
: match all descendants of theChildren
array{Child}/{GrandChild}
: match all descendant members ofChild
, all types are children ofGrandChild
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 ischild
, all types are child members ofGrandChild
{Child}/grandChild/content
: match all descendant members whose type isChild
, the child members namedgrandChild
, and the child members namedcontent
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 namedchildren
, and take the child member namedcontent
in the2
th objectparent/children[1]
: match the child member of the array type namedchildren
among the descendant members namedparent
, and take the2
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 withchild
{*Child}
: match all descendant members whose type ends withChild
c*ld/{Grand*ld}
: match the descendant members whose name starts withc
and ends withld
, and the members whose type starts withGrand
and ends withld
child/*/content
: At this time,*
will match any member, that is, the child member ofcontent
contained in any child member of thechild
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, theOmniAccessor
will immediately throw aNoSuchMemberError
error, so that the unit test is terminated early. However, compared to the conventional member access method, the support ofOmniAccessor
in IDE refactoring is still weak.For content assignment of complex objects, in most cases, we recommend using Builder Pattern, 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.