Introduction
The Vanilla library is a multi-purpose utility library containing a lot of "what if" and experimentation code that actually seemed to have a potential use, so it was made publicly available.
The project web site is: http://stehno.com/vanilla
The source code for the project is hosted on GitHub at: https://github.com/cjstehno/vanilla
Vanilla is licensed under the Apache 2 open source license.
Lazy Immutable
The Groovy @Immutable
annotation is quite useful; however, it is an all-or-nothing object, meaning that you create it once and then you can’t touch
it again (other than copying it with different properties). The @LazyImmutable
annotation in Vanilla allows for a slighly more flexible configuration
of immutable objects.
Consider the case of of an immutable Person object:
@Immutable
class Person {
String firstName
String middleName
String lastName
int age
}
With @Immutable
you have to create the object all at once:
def person = new Person('Chris','J','Stehno',42)
and then you’re stuck with it. You can create a copy of it with one or more different properties using the copyWith
method, but you need to specify
the copyWith=true
in the annotation itself, then you can do something like:
Person otherPerson = person.copyWith(firstName:'Bob', age:50)
With more complicated immutables, this "all at once" requirement can be annoying. This is where the @LazyImmutable
annotation becomes useful. With
a similar Person class:
@LazyImmutable @Canonical
class Person {
String firstName
String middleName
String lastName
int age
}
using the new annotation, you can create and populate the instance over time:
def person = new Person('Chris')
person.middleName = 'J'
person.lastName = 'Stehno'
person.age = 42
Notice that the @LazyImmutable
annotation does not apply any other transforms (as the standard @Immutable
does). It’s a standard Groovy object,
but with an added method: the asImmutable()
method is injected via AST Transformation. This method will take the current state of the object and
create an immutable version of the object - this does imply that the properties of lazy immutable objects should follow the same rules as those of
the standard immutable so that the conversion is determinate. For our example case:
Person immutablePerson = person.asImmutable()
The created object is the same immutable object as would have been created by using the @Immutable
annotation and it is generated as an extension
of the class you created so that it’s type is still valid. The immutable version of the object also has a useful added method, the asMutable()
method is used to create a copy of the original mutable object.
Person otherMutable = immutablePerson.asMutable()
You can swap back and forth between immutability and mutability as needed without loss of functionality or data integrity.
Property Randomizers
Generating large amounts of meaningful test data can be tedious and sometimes error prone if your test values arbitrarily fit your code and don’t properly exercise it. Randomly generating test data objects can be a simple way to test different scenarios across the same tests and also simplify your test writing.
The PropertyRandomizer
can be used as a builder by using the classes directly or as a DSL.
Operations
The PropertyRandomizer
instances have six configuration methods and three builder methods. The configuration methods are:
ignoringTypes(Class…)
-
Specifies the given property types should be ignored and not randomized.
ignoringProperties(String…)
-
Specifies that the given property names should be ignored and not randomized.
typeRandomizers(Map<Class,Object>)
-
Allows the specification of multiple type randomizers, where the map is property type to the appropriate randomizer.
typeRandomizer(Class, Object)
-
Specifies a single randomizer for a property type.
propertyRandomizers(Map<String,Object>)
-
Allows the specification of multiple property randomizers, where the map is property name to the appropriate randomizer.
propertyRandomizer(String, Object)
-
Specifies a single randomizer for a property name.
The Object
configured as a "randomizer" may be either a PropertyRandomizer
instance or a Closure
. If a Closure
is used, it will allow up to
two arguments to the Closure
:
-
If no arguments are specified, the returned value must be built using external code
-
If one argument is specified, it will be the
Random
instance used by the randomizer for generating random data. -
If two arguments are passed in, the first will be the
Random
as above, and the second will be the map of properties which will be used to populate the target object.
The allowed builder methods are:
one()
-
Instantiates a single randomized instance of the target object.
times(int)
-
Instantiates a collection of multiple randomized instances of size equal to the provided count.
*(int)
-
An alias to the
times(int)
method for operator-overridden shorthand notation.
Randomizer Builder
The PropertyRandomizer
class allows randomizers to be built directly from the mutator methods of the class. Pulling an example from the unit tests
we can see how the builder uses the configuration and builder operations:
PropertyRandomizer rando = PropertyRandomizer.randomize(Person)
.ignoringProperties('bankPin')
.typeRandomizers(
(Date) : { new Date() },
(Pet) : { randomize(Pet).one() },
(String[]): forStringArray()
)
.propertyRandomizers(name: { 'FixedValue' })
The code above creates a PropertyRandomizer
for the Person
class where the bankPin
property is ignored, the name
property is "randomized" to a
fixed value "FixedValue", and the properties of type Date
, Pet
, and String[]
are randomized using the provided custom randomizers. To generate
random instances using this randomizer you could do any of the following:
Person personA = rando.one()
Collection<Person> fourPeople = rando.times(4)
Collection<Person> tenPeople = rando * 10
Randomizer DSL
Using the DSL version of the randomizer is not really any different than using the builder style. For the example specified above, we would have the following code to achieve the same randomizer:
PropertyRandomizer rando = PropertyRandomizer.randomize(Person){
ignoringProperties 'bankPin'
typeRandomizers(
(Date) : { new Date() },
(Pet) : { randomize(Pet).one() },
(String[]): forStringArray()
)
propertyRandomizers name: { 'FixedValue' }
}
The main difference being that the operations are specified within a Closure
, which could be shared or configured externally to the rest of the code.
Provided Randomizers
The library provides a collection of useful randomizers, some of which are configured by default in the PropertyRandomizer
, in the Randomizers
utility class. These may be used directly or as randomizer objects for the PropertyRandomizer
configurations.
Randomizing Simple Types
The PropertyRandomizer
is not just useful for complex objects, it will also provide random instances of more simple types, such as String
and
Date
objects.
If the type to be randomized is one of the built-in class randomizers (String
and the primitive types and their wrapper classes) you can randomize
them directly:
PropertyRandomizer stringRando = PropertyRandomizer.randomize(String)
PropertyRandomizer dateRando = PropertyRandomizer.randomize(Date)
Other, non-default types may also be configured in a similar manner, by adding a typeRandomizer(Class,Object)
:
Class byteArray = ([] as byte[]).class
PropertyRandomizer stringRando = PropertyRandomizer.randomize(byteArray){
typeRandomizer byteArray, Randomizers.forByteArray()
}
This code will create random instances of byte
arrays. The extra code for determining the Class
for the byte
array is a work-around for an odd
issue with Groovy typing and byte arrays - not really related to the example at hand.
Generally, this approach should be left to very simple value-type objects. In many cases, you could also just use the randomizers provided in the
Randomizers
class directly.
Object Mappers
When working with legacy or external code-bases you may run into an API where you end up doing a lot of domain object mapping, from one object format to another. The Vanilla Object Mappers framework can help simplify the mappings. It comes in two forms, a dynamic runtime implementation and a compile-time generated implementation.
Generated ObjectMapper
instances are stateless and thread-safe.
Object Mapper DSL
The DSL used by the ObjectMapper
framework is shared between both the runtime and compiled implementations. There are four supported mapping
statements:
map <source-name>
-
Maps the specified source property into the destination property of the same name, with no conversion.
map <source-name> into <destination-name>
-
Maps the specified source property into the specified destination property, with no conversion.
map <source-name> into <destination-name> using <closure>
-
Maps the specified source property into the specified destination property, with the given conversion closure.
map <source-name> using <closure>
-
Maps the specified source property int the destination property of the same name, with the given conversion closure.
The conversion closures configured by the using
clause are standard Groovy closures that are allowed to accept up to three arguments:
-
Argument one - source property value
-
Argument two - source object reference
-
Argument three - destination object reference
A sample of the DSL pulled from the unit tests is shown below:
map 'name'
map 'age' into 'years'
map 'startDate' using { Date.parse('MM/dd/yyyy', it) }
map 'birthDate' into 'birthday' using { LocalDate d -> d.format(BASIC_ISO_DATE) }
Runtime Mappers
The RuntimeObjectMapper
is created using its static mapper(Closure)
method, where the Closure
contains the configuration DSL. An example would
be something like:
ObjectMapper inividualToPerson = RuntimeObjectMapper.mapper {
map 'id'
map 'givenName' into 'firstName'
map 'familyName' into 'lastName'
map 'birthDate' using { d-> LocaleDate.parse(d) }
}
Compiled Mappers
Sometimes you need to squeeze a bit more performance out of a mapping operation, or you just want to generate cleaner code. The
InjectCompiledObjectMapper
annotation is used to annotate a field or property to inject a compile-time generated ObjectMapper
implementation
(using AST Transformations) based on the supplied DSL configuration.
Using the compiled approach is as simple as the runtime approach, you just write the DSL code in the @InjectObjectMapper
annotation on a method,
or field:
class ObjectMappers {
@InjectObjectMapper({
map 'id'
map 'givenName' into 'firstName'
map 'familyName' into 'lastName'
map 'birthDate' using { d-> LocaleDate.parse(d) }
})
static final ObjectMapper personMapper(){}
}
When the code compiles, a new implementation of ObjectMapper will be created and installed as the return value for the personMapper() method.
The compiled version of the DSL has all of the same functionality of the dynamic version except that it does not support using ObjectMappers
directly in the using command; however, a workaround for this is to use a closure to wrap another ObjectMapper
.
Usage
Once you have created an ObjectMapper
using either the runtime or compile-time implementations, objects can be copied using the
copy(Object,Object)
method, which will copy the properties of the source object into the destination object according to the configured mapping
information in the DSL.
def people = individuals.collect { indiv->
Person person = new Person()
individualToPerson.copy(indiv, person)
person
}
The Person
objects will now contain the correctly mapped property values from the Individual
objects. The ObjectMapper
also has a
create(Object, Class)
method to help simplify the use case where you are simply creating a new populated instance of the destination object:
def people = individuals.collect { indiv->
individualToPerson.create(indiv, Person)
}
When using the create()
method, the destination object must have a default constructor.
The third, slightly more useful option in this specific collector case is to use the collector(Class)
method, which again takes the type of the
destination object (with a default constructor):
def people = individuals.collect( individualToPerson.collector(Person) )
The collector(Class) method returns a Closure that is also a shortcut to the conversion code shown previously. It’s mostly syntactic sugar, but it is nice and clean to work with.
ResultSet Mappers
A common operation while working with a databases is the mapping of row data to objects in code. This has always seemed very repetitive and error
prone. The Vanilla library provides a simple DSL-based solution for quickly implementing code that extracts, and optionally transforms, the data
from the database table (via a ResultSet
) and uses it to populate the object representation of your choice.
There are two implementations available, one that is dynamic and does all the extraction and conversion work at runtime, and a second which pre-compiles the extraction code at compile time using AST transformations.
Mapping Style
There are two styles of mapping available to both implementations of the mapping framework:
-
MappingStyle.IMPLICIT
- All properties of the mapped object are mapped by default, though the DSL may be used to alter property extraction or ignore properties. -
MappingStyle.EXPLICIT
- No properties of the mapped object are mapped by default. All mapping must be explicitly configured by the DSL.
DSL
The DSL used by the ResultSetMapper
framework is shared between both the runtime and Compiled Implementations. There are five supported mapping
statements:
ignore <property-name>[,<property-name>…]
-
Used to ignore one or more of the mapped objects properties. This operation is meaningless for explicit mappers.
map <property-name>
-
Maps the specified object property with the default extraction type (uses
from <field-name>
) and no conversion. map <property-name> from[TYPE] <field-name>
-
Maps the specified object property with the specified extraction type and field name with no conversion.
map <property-name> from[TYPE] <field-name> using <closure>
-
Maps the specified object property with the specified extraction type with the provided conversion closure.
map <property-name> using <closure>
-
Maps the specified object property with the default extraction type (uses
from <field-name>
) with the provided conversion closure.
Auto-conversions of the property name to a database field name are done as camel-case to underscore-based notation similar to the following:
something --becomes--> something somethingInteresting --becomes--> something_interesting anotherPropertyName --becomes--> another_property_name
The extraction types are defined analogous to the getter methods provided in the ResultSet
but with the "from" prefix in place of "get". The
supported extractors are: from
, fromObject
, fromString
, fromBoolean
, fromByte
, fromShort
, fromInt
, fromLong
, fromFloat
, fromDouble
,
fromBytes
, fromDate
, fromTime
, fromTimestamp
, fromAsciiStream
, fromUnicodeStream
, fromBinaryStream
, fromCharacterStream
,
fromBigDecimal
, fromRef
, fromBlob
, fromClob
, fromArray
, fromURL
, fromMapper
.
Each extractor takes either the database field name or position value as a required parameter and extracts the database value as you would expect from its name, though you may want to consult the ResultSet
documentation for more details on the behavior of a specific getter method.
The exceptional extractor case is the fromMapper
extractor. This extractor accepts a ResultSetMapper
instance which will be used to extract the object for the specified target property. An example of such a mapping would be the following:
map 'foo' fromMapper Foo.mapper('foo_')
where Foo.mapper(String)
is a mapper factory method.
The conversion closures configured by the using
clause are standard Groovy closures that are passed in one or two arguments:
-
1-argument: the extracted database value at runtime which allows the closure to do additional conversion of the database value.
-
2-argument: the reference to the
ResultSet
being extracted.
The closure should return the desired mapped value for the property.
A sample of the DSL pulled from the unit tests is shown below:
ignore 'bankPin', 'pet'
ignore 'children'
map 'birthDate' fromDate 'birth_date'
map 'age' from 2 using { a -> a - 5 }
map 'name' from 'name'
Runtime Implementation
The runtime implementation uses the static mapper(Class, MappingStyle, Closure)
method from ResultSetMapperBuilder
as its entry point. The object
type being mapped is provided, as well as the mapping style and an optional configuration closure (DSL). Using our example code from the DSL
section, you would have something like the following for an IMPLICIT
mapping:
ResultSetMapper mapper = ResultSetMapperBuilder.mapper(Person){
ignore 'bankPin', 'pet'
ignore 'children'
map 'birthDate' fromDate 'birth_date'
map 'age' from 2 using { a -> a - 5 }
map 'name' from 'name'
}
An IMPLICIT
mapping has a very clean format for cases where your mapped object aligns with the database fields and types. You can simply map it with
no configuration DSL.
ResultSetMapper mapper = ResultSetMapperBuilder.mapper(Person)
An EXPLICIT
mapping for the same configuration could be something like:
ResultSetMapper mapper = ResultSetMapperBuilder.mapper(Person, MappingStyle.EXPLICIT){
map 'birthDate' fromDate 'birth_date'
map 'age' from 2 using { a -> a - 5 }
map 'name' using { n-> "Name: $n"}
}
The mapping, extraction and conversion operations are all performed at runtime. Some pre-computing or caching may be performed; however, the bulk of the mapping is done at runtime with each mapped object creation.
The created mappers are reusable and thread-safe.
Tip
|
While the dynamic runtime implementation is fully supported and maintained, it is generally advisable to use the Compiled Implementation to generation more performant code. |
Compiled Implementation
The Compiled Implementation uses the InjectResultSetMapper
annotation to inject a configured ResultSetMapper
into a field or method of a class.
Using the example from the Runtime Implementation section, we could have an IMPLICIT
mapper available as a static method as follows:
class Mappers {
@InjectResultSetMapper(
value=Person,
config={
ignore 'bankPin', 'pet'
ignore 'children'
map 'birthDate' fromDate 'birth_date'
map 'age' from 2 using { a -> a - 5 }
map 'name' from 'name'
}
)
static ResultSetMapper personMapper(){}
}
The personMapper()
method returns the same ResultSetMapper
instance for every call and the mapper itself is configured at compile-time via AST
transformations so that the extraction calls are generated at compile-time rather than for each mapping call; however, the conversion closures are
still executed at runtime.
For IMPLICIT
mappings where the object property names and type align with the database fields, you can have a very simple IMPLICIT
mapping
configuration:
class Mappers {
@InjectResultSetMapper(Person)
static ResultSetMapper personMapper(){}
}
Creation of EXPLICIT
mappers follows a similar style:
class Mappers {
@InjectResultSetMapper(
value=Person,
style=MappingStyle.EXPLICIT,
config={
map 'birthDate' fromDate 'birth_date'
map 'age' from 2 using { a -> a - 5 }
map 'name' using { n-> "Name: $n"}
}
)
static ResultSetMapper personMapper(){}
}
The method or field used to provide the compiled mapper does not need to be static; however, it is advisable, since the underlying instance created will be a static field of the enclosing class.
The generated mapper class is created in the same package as the mapped object type. The InjectResultSetMapper
annotation also provides a name
property which may be used to provide an alternate name for the generated mapper class. By default, the name of the mapped object type is used with
the added "Mapper" suffix.
The generated mapper class may be used directly; however, the method or field injection is required to create the mapper class and it is recommended to use the field or method as your access point to the generated class.
Note
|
When using a factory method to create the ResultSetMapper instances, note that the method is annotated with @Memoized so that multiple calls to the method will return the same instance. This is also true when a prefix is used (method parameter); calls to the method with the same prefix value will return the same instance of the mapper.
|
Usage
Once you have created a ResultSetMapper
, you can use it anywhere you have a ResultSet
or with the groovy.sql.Sql
class as follows:
def sql = Sql.newInstance(db.url, db.user, db.password, db.driver)
def people = []
sql.eachRow('select * from people'){ rs->
people << Mappers.personMapper().call(rs)
}
Where we are using the compiled mapper implementation exposed by the Mappers.personMapper()
method. The call(ResultSet)
method performs the mapping
of the current ResultSet
data into a Person
object.
An alias to the call(ResultSet)
method is provided as the mapRow(ResultSet, int)
method, which allows for simple interaction with the
Spring Framework as a RowMapper
, for example:
List<Person> people = jdbcTemplate.query(
'select * from people',
Mappers.personMapper() as RowMapper
)
The ResultSetMapper
may be cast as a RowMapper
and then used as one.
Field Name Prefix
Both implementations of the ResultSetMapper
support an optional field name prefix, which will be applied to all field names on field lookup. So for example a property named "somethingInteresting" with a prefix of "foo_" would look for a database field named "foo_something_interesting". If no prefix is specified an empty string will be used.
The prefix is not overridden by the "fromXXX" mapping methods. This is intentional since the main goal of the prefix support is to allow the same mapper to be useful across different mapping scenarios such as in a join where each column name in the query result has been prefixed by some common known prefix.
This does not remove the ability to use numerical from indices. If a number is detected in the "from" statement, the prefix will not be applied.
Mapping Properties from Multiple Fields
It is often necessary to map an object field that is not directly represented in the database fields or is made up of more than on field. This is can be done by using the 2-argument version of the "using" closure. Consider the example of a GeoLocation
object being mapped:
@Canonical
class GeoLocation {
double latitude
double longitude
}
class Somewhere {
GeoLocation location
}
The DSL code would look something like the following:
{ map 'location' fromDouble 'latitude' using { lat,rs-> new GeoLocation(lat, rs.getDouble('longitude')) } }
This allows the flexibility to populate the target object from various database fields.
MockResultSet DSL
Mocking interactions with a database can be frustrating; however, the MockRunner JDBC library can really simplify the
mocking of ResultSet
interaction, which is often enough to get basic unit testing done. Vanilla provides an added layer on top of the MockRunner
MockResultSet
class so that the ResultSet
may be configured using a simple DSL, such as:
ResultSet rs = ResultSetBuilder.resultSet {
columns 'first_name', 'last_name', 'phone_number', 'age'
data 'Fred', 'Flintstone', '555-123-9876', 56
object phoneRecord
map firstName:'Barney', lastName:'Rubble', age:55, phoneNumber:'555-222-3456'
}
The resulting ResultSet
is implemented by the com.mockrunner.mock.jdbc.MockResultSet
class from MockRunner and should behave like a real ResultSet
within the scope of the test mocking.
ResultSet DSL
The ResultSet
DSL consists of four statements:
columns
-
Accepts a
String…
orList<String>
argument to provide the names of the columns configured in theResultSet
. data
-
Accepts a
String…
orList<String>
argument to provide the data for a single row, in the same order as the columns. object
-
Accepts an Object which will be used to populate the row. The column names will be converted to camel-case and used to find properties on the object.
map
-
Accepts a Map which will be used to populate the row. The column names will be converted to camel-case and used to find properties in the map.
Overlappable
Determining whether or not two objects overlap is pretty straight-forward when there is only a single variable of comparison; however, what about the case where you have multiple variables to consider, for example if we have a group of people "People A" who are males between 15 and 25, weighing between 200 and 300 pounds:
People A Ages: 15-25 Gender: M Weight: 200-300
Now consider the case when you have other search rules in the same search and that for the sake of efficiency we want to find overlapping rules and merge them into one (we will only consider the overlap determination here). With two other rules:
People B Ages: 13-20 Gender: M Weight: 250-400 People C Ages: 8-10 Gender: M Weight: 200-300
It’s not hard to look at the rules and determine which ones overlap. "People A" overlaps with "People B", while "People C" does not overlap with either of the other two. In code this can be a difficult comparison to do correctly and efficiently. Also, as your number of comparison axes increases, so does the complexity of determining overlap for any given pair of objects.
What is needed is a simple means of determining the overlap of two objects and the best way I have found to do that is to break each object down into its overlap-comparison components, each of which I will call a "Lobe" from here on out. I chose the term lobe because it is defined as: "any rounded projection forming part of a larger structure". Also, terms like element and node are used far too much already in programming.
When you break each object down into it’s lobes, you will have one lobe for Gender, one for Ages, and one for weights. Now you can build your overlap determination based on whether or not each lobe overlaps with its corresponding lobe on the other object. If all of the lobes overlap those of the other object, then the two objects are considered overlapping, otherwise they are not. This allows for a fail-fast comparison since if the comparison of any given lobe fails, you cannot have an overlapping object and no further comparison is necessary.
In code a lobe can be defined by interface as:
interface Lobe {
boolean overlaps( Lobe other )
}
Each Lobe
implementation defines what it means to overlap another Lobe
of the same type. Using Groovy and some generic logic we can easily come
up with a ComparableLobe
which is based on single values and ranges of Comparable
objects such as numbers, strings and ranges. This allows us to
do things like:
new ComparableLobe( 10..20, 50, 75 )
new ComparableLobe( 'a', 'h'..'j', 'm' )
lobeA.overlaps( lobeB )
which can make the overlap determination very flexible.
def genderLobe = new ComparableLobe( 'M' )
def agesLobe = new ComparableLobe( 15..25 )
def weightsLobe = new ComparableLobe( 200..300 )
The next thing we need is a way of comparing these lobes in a simple and repeatable manner and that’s where the Overlappable
trait comes in. The
Overlappable
trait defines an object that can be compared for overlap. The required method is basically the same as that of the Lobe; however, this
trait is for the parent object itself. By providing an abstract implementation of this interface we have a nice clean way of providing overlap
detection functionality for an object type. You could create a simple Overlappable
Person object:
class People implements Overlappable {
String gender
IntRange ages
IntRange weights
@Override Lobe[] getLobes() {
[
new ComparableLobe(gender),
new ComparableLobe(ages),
new ComparableLobe(weights)
]
}
}
The overlaps() method uses the provided Lobes to populate an OverlapBuilder
, which is basically a helper class for performing the actual
Lobe-to-Lobe comparison of a given set of Lobes. The OverlapBuilder
is inspired by the builder in the Apache Commons - Lang API, such as
EqualsBuilder
and HashCodeBuilder
. You create an instance and append your Lobes to it, then execute the overlap()
method to perform the
comparison.
new OverlapBuilder()
.appendLobe(new ComparableLobe(1..10), new ComparableLobe(5..15))
.overlap()
It also provides an append method for simple comparable cases:
overlapBuilder.appendComparable( 20..25, 15..30 )
which just wraps each value in a ComparableLobe
. Now, given a list of People objects, you can determine if any of them overlap any of the others
simply by iterating over the list and comparing each element with the others:
def list = [
new Person( gender:'M', ages:15..25, weights:200..300 ),
new Person( gender:'M', ages:13..20, weights:250..400 ),
new Person( gender:'M', ages:8..10, weights:200..300 )
]
list[0..-2].eachWithIndex { self, idx->
list[(idx+1)..(-1)].each { other->
if( self.overlaps( other ) ){
println "$self overlaps $other"
}
}
}
As a final little bonus feature, there is a ComparableLobe.ANY
object which denotes a Lobe
that will always be considered to overlap, no matter
what the other value is.
Test Fixtures
Unit testing with data fixtures is good habit to get into, and having a simple means of creating and managing reusable fixture data makes it much
more likely. The FixtureBuilder
and Fixture
class can simplify the creation of reusable test fixtures using a small DSL.
The text fixtures created using the FixtureBuilder
are the properties required to build new instances of objects needed for testing.
The reasoning behind using Maps is that Groovy allows them to be used as constructor arguments for creating objects; therefore, the maps give you a reusable and detached data set for use in creating your test fixture instances. Two objects instances created from the same fixture data will be equivalent at the level of the properties defined by the fixture; however, each can be manipulated without effecting the other.
One thing to note about the fixtures is that the fixture container and the maps that are passed in as individual fixture data are all made immutable via the asImmutable() method; however, if the data inside the fixture is mutable, it still may have the potential for being changed. Be aware of this and take proper precautions when you create an interact with such data types.
Fixture DSL
The FixtureBuilder
has three operations available to its DSL:
define
-
The DSL entry point method, which accpets the DSL closure and creates the
Fixture
container. fix(Object,Map)
-
Uses the specified Map data as the content for the fixture with the provided key.
fix(Object,PropertyRandomizer)
-
Uses the specified
PropertyRandomizer
to generate random content for the fixture with the provided key.
The build()
method is then used to create the populated Fixture
container object.
Usage
A fixture for a Person
class, such as:
class Person {
Name name
LocalDate birthDate
int score
}
could have fixtures created using the following code:
class PersonFixtures {
static final String BOB = 'Bob'
static final String LARRY = 'Larry'
static final Fixture FIXTURES = define {
fix BOB, [ name:new Name('Bob','Q','Public'), birthDate:LocalDate.of(1952,5,14), score:120 ]
fix LARRY, [ name:new Name('Larry','G','Larson'), birthDate:LocalDate.of(1970,2,8), score:100 ]
}
}
I tend to create a main class to contain my fixtures and to also provide the set of supported fixture keys. Notice that the define
method is where
you create the data contained by the fixtures, each mapped with an object key. The key can be any object which may be used as a Map
key (proper
equals and hashCode implementation).
Once your fixtures are defined, you can use them in various ways. You can request the immutable data map for a fixture:
Map data = PersonFixtures.FIXTURES.map(PersonFixtures.BOB)
You can create an instance of the target object using the data mapped to a specified fixture:
Person person = PersonFixtures.FIXTURES.object(Person, PersonFixtures.LARRY)
Or, you can request the data or an instance for a fixture while applying additional (or overridden) properties to the fixture data:
Map data = PersonFixtures.FIXTURES.map(PersonFixtures.BOB, score:53)
Person person = PersonFixtures.FIXTURES.object(Person, PersonFixtures.LARRY, score:200)
You can easily retrieve field property values for each fixture for use in your tests:
assert 100 == PersonFixtures.FIXTURES.field('score', PersonFixtures.LARRY)
This allows field-by-field comparisons for testing and the ability to use the field values as parameters as needed.
Lastly, you can verify that an object instance contains the expected data that is associated with a fixture:
assert PersonFixtures.FIXTURES.verify(person, PersonFixtures.LARRY)
which will compare the given object to the specified fixture and return true of all of the properties defined in the fixture match the same properties of the given object. There is also a second version of the method which allows property customizations before comparison.
One step farther… you can combine fixtures with property randomization to make fixture creation even simpler for those cases where you don’t care about what the properties are, just that you can get at them reliably.
static final Fixture FIXTURES = define {
fix FIX_A, [ name:randomize(Name).one(), birthDate:LocalDate.of(1952,5,14), score:120 ]
fix FIX_B, randomize(Person){
typeRandomizers(
(Name): randomize(Name),
(LocalDate): { LocalDate.now() }
)
}
}
The fixture mapper accepts PropertyRandomizer
instances and will use them to generate the random content once, when the fixture is created and
then it will be available unchanged during the testing.
Rolling File
We often run into rolling log files that cutoff and rollover to a new file according to some configured rules; however, this is
also a useful feature outside of logging. A RollingFile
is based on a standard File
but will "roll" the file based on a
RolloverTriggerStrategy
implementation - the default is based on file size. Once the conditions of the strategy are met, the file
will rollover using a RolloverFileProvider
implementation to determine the name and path of the rolled file (the default is to
create a file in the same directory with a name suffixed with the timestamp at the time the file was cut. Being that both of these
strategies are interfaces, the functionality is pluggable and extensible.
Optionally, the rolled files may be compressed.
An example of using a RolledFile
is as follows:
RollingFile rollingFile = new RollingFile(
file: new File('/some/file.json'),
rolloverStrategy: new FileSizeRolloverTriggerStrategy(1, StorageUnit.MEGABYTES),
compression: true
)
rollingFile.append '{ "id:"12345", "label":"item-01", "value:":8675309 }'
which will use /some/file.json
as the initial target file, which will be rolled when a file write would cause the file size to
exceed 1 MB.
If for some reason you want the ability to optionally make a file rolling or not, you can use the NoopRolloverTriggerStrategy
as
the rolloverStrategy
so that the file will never roll. The strategy may be changed at runtime to cause the file to begin rolling
behavior.
Layered Configuration
The com.stehno.vanilla.config
package contains a set of classes useful for providing a layered configuration framework similar to that provided by Ratpack or Spring, but without the other
framework overhead.
You could create a cached layered configuration mechanism with something like:
ConfigurationSource config = new CachedConfigurationSource(new CompositeConfigurationSource([
new ZookeeperConfigurationSource(zkconfig),
new PropertiesConfigurationSource(props)
]))
String serverHost = config.getString('server.host', 'localhost:1234')
where the ZookeeperConfigurationSource
would be your own implementation of the ConfigurationSource
implementation used to extract configuration from Zookeeper.
When a configuration property is requested, such as in the example above, the ZookeeperConfigurationSource
will be checked for the desired property key (server.host
), if it does not contain
the key, the PropertiesConfigurationSource
will be checked - if it is found, the value will be returned, if not, the default value of localhost:1234
will be returned. The CachedConfigurationSource
decorator is used to provide a top-level caching layer so that once a property is resolved, it will not need to be resolved again.
The ConfigurationSource
interface provides a rich set of property getter methods to retrieve properties as types other than just `String`s.
Multiple levels of nested configurations could be configured in this manner.