Introduction
Creating maintainable test data is hard. Often an entire object graph needs to be created to support the instantiation of a single domain object. This leads to either the cutting and pasting of that creation code, or relying on a canned set of objects that we’ve grown over time and maintained as the domain objects change. After a while, adding just one more widget to that set of canned data ends up breaking tests just about every time.
There has to be a better solution, right?
Yep! Due to the power and the glory of Grails, we have a lot of metadata at our fingertips about those domain objects. We know what constraints we’ve placed on our objects, and which objects depend on other objects to live.
Using this additional information, we’ve created a grails plugin that makes it easy to just provide those values that you want to exercise under test and not worry about the rest of the object graph that you need to create just to instantiate your domain objects.
Once installed, all you have to do is call the new "build" method on your domain class and you’ll be given a valid instance with all of the required constraints given values.
def author = Author.build(name: 'Ted Naleid')
//or
def author = TestData.build(Author, name: 'Ted Naleid')
Release History
-
January 6th, 2018 - 3.3.0.RC1
-
Support for Grails 3.3.x
-
Major cleanup and performance improvements
-
Trait based API
-
-
May 7th, 2018 - 3.3.0
-
Official release
-
Minor bug fixes from release candidate
-
-
June 1st, 2018 - 3.3.1
-
Support building of any nested
Validateable
-
Fix misc data binding issues
-
Fix build issues with Basic to-many collections
-
Add 2nd
obj
parameter to TestDataConfig closures -
Other minor bug fixes and cleanup
-
Methodology
Over the years, a number of different methods for creating and maintaining testing data have been tried. Here’s a subset of the solutions along with some pros/cons to each of them.
Manual Data Creation
Creating testing data for integration tests has gone through an evolution over the years.
It first started with all of the test data being constructed right in the body of the test method, leading to a lot of cut and paste when you hit another method.
Strengths
-
the code that defines the objects under test is right next to the code that uses those objects in test.
Weaknesses
-
If the objects under test rely on other objects, it can be necessary to create a large graph of objects in your test method
-
Not at all DRY, cutting and pasting leads to errors and extra code that you don’t need
-
When you add a new method to an existing object (or change/delete an existing one), you need to change all of the cut/pasted lines in all of the test methods
JUnit (and other xUnit) testing frameworks DRYed this up by having a "setUp" method that gets called for all test methods, but this requires you to share data across all of the tests in the test class and you’re still manually creating it in each test
Test Fixtures
Text fixtures allow you to use a DSL to define a named object graph. You can then refer to this object graph in a test to have the fixtures instantiated and saved to the database before running your test.
Strengths
-
Full control over values in every object in the object graph
Weaknesses
-
Removes definition of testing data from test which can hurt test clarity. Who knows how many family members the
johnsonFamily
fixture has and how many thesmithFamily
has? You have to look at another file to find out. -
Starts out well, but more and more tests start to rely on the same fixtures making them fragile. "I’ll just add another Widget to this fixture" ends up breaking 10 other tests.
-
Any changes to domain objects necessitate modifying every fixture that has that domain object within the object graph
Features
Auto generation of grails property types, including:
-
Associated domain objects
-
Embedded domain objects
-
String
-
Date
-
Boolean
-
Numbers (Integer, Long, Float, Short, etc)
-
Byte
-
JodaTime classes
-
Any persistable class with a zero-argument constructor
Supported Constraints
Unsupported Constraints
-
validator - custom validators can have all kinds of business logic in them and there isn’t a programmatic way to interrogate this. If the test value that’s tried doesn’t pass your logic, you’ll need to provide a value, or mock out the calls that the validator makes so that it passes.
-
unique - not directly supported but it’s possible to specify your own unique value in a config file as demonstrated on the Examples page.
-
notEqual - unlikely that our test data would match this, could be supported in the future
Installation
The test framework and testing methodology for Grails has undergone several major revisions over the past few releases.
Unfortunately, this makes it impossible to have one plugin which is compatible with all prior versions.
Grails 5.0 or later
testImplementation "io.github.longwa:build-test-data:5.0.0"
Grails 4.0
testCompile "org.grails.plugins:build-test-data:4.0.0"
Grails 3.3
testCompile "org.grails.plugins:build-test-data:3.3.1"
Grails 3.1 and 3.2
testCompile "org.grails.plugins:build-test-data:3.0.1"
Version 3.0.x is required for Grails versions 3.0 through 3.2 |
See the legacy Wiki documentation for details: https://github.com/longwa/build-test-data/wiki
Grails 2.x
See the legacy Wiki documentation for details: https://github.com/longwa/build-test-data/wiki
Upgrading
When upgrading from older versions of Grails, first follow the standard Grails upgrade process.
Performance Improvements
The core plugin has been refactored to reduce metaClass dependency and remove a few poor performing contructs (I’m looking at you ClassPathScanningCandidateComponentProvider).
Most of the plugin classes have also been converted to @CompileStatic
. In addition, the DomainInstanceBuilder can be cached and re-used to prevent additional overhead.
Abstract Classes
Concrete subclasses are no longer discovered automatically. If you graph needs to build an abstract class, use the abstractDefaults
parameter in TestDataConfig
to specify a concrete subclass.
abstractDefault = [
'test.AbstractBook' : MyBook,
'test.AbstractAuthor' : Tolkien
]
Config Parameters
Some test data config parameters such as abstractDefault
and sampleData
used to allow either a Class or String as the map key.
In 3.3 and later, only a full package name String is allowed.
Disabling Via TestDataConfig
You can no longer disable via the testDataConfig.enabled = false
option.
Build methods refactored
They all go through static helpers in the TestData eventually
buildLazy
→ findOrBuild
or build(find:true)
buildWithoutSave
→ build(save: false)
Unit Tests
Most of the work when upgrading to Grails 3.3.x will involve refactoring related to the framework itself with only minor changes for build-test-data.
@Build([Author])
class AuthorSpec extends Specification {
...
}
Just add the trait:
@Build([Author])
class AuthorSpec extends Specification implements BuildDataTest {
...
}
Optionally, use the standard Grails mock method:
class AuthorSpec extends Specification implements BuildDataTest {
void setupSpec() {
mockDomains(Author)
}
...
}
or by overriding the getDomainClassesToMock method:
class AuthorSpec extends Specification implements BuildDataTest {
Class[] getDomainClassesToMock() {
[Author]
}
...
}
or use with the BuildDomainTest and generics
class AuthorSpec extends Specification implements BuildDomainTest<Author>{
...
}
Make sure you use DomainUnitTest for unit tests and TestDataBuilder for integration tests. |
Integration Tests
For integration tests, follow the standard Grails upgrade instructions and then add the TestDataBuilder
trait:
@Integration
@Rollback
class AuthorIntegrationSpec extends Specification implements TestDataBuilder {
...
}
Usage
The plugin should be able to generate simple to moderately complex objects and satisfy all of their constraints right out of the box without any additional configuration.
Simple Application Example
See the example/bookStore app for examples
Create an Author domain class:
grails create-domain-class com.example.Author
And give it the following contents:
package bookstore
class Author {
String firstName
String lastName
static hasMany = [books: Book]
static constraints = {
firstName(blank: false)
lastName(blank: false)
}
public String toString() {
"$firstName $lastName"
}
}
Then create a Book domain class:
grails create-domain-class com.example.Book
With these contents:
package bookstore
class Book {
String title
Date published
BigDecimal price
static belongsTo = [author: Author]
static constraints = {
title(blank: false)
published(nullable: false)
price(nullable: false, scale: 2, min: 5.95 as BigDecimal)
author(nullable: false)
}
public String toString() {
"$title published on $published by $author costs \$$price"
}
}
Now, load up the grails console and lets try build on our new domain objects:
grails console
build()
import bookstore.Author
import grails.buildtestdata.TestData
def a = TestData.build(Author)
println "First Name: $a.firstName"
println "Last Name: $a.lastName"
println "Author ID: $a.id"
The plugin will automatically create a new Author object, populate the required firstName and lastName fields, and save the object (giving it an id). If you run the script, you should see output like this:
First Name: firstName Last Name: lastName Author ID: 1
By default, for String values, the plugin will just try to use the field name as the value (truncating or expanding as necessary if there are string length limitations).
If you want to specify the first name, just pass that in a map that you pass to build on the first line:
def a = TestData.build(Author, firstName: "Daniel")
That modified script prints:
First Name: Daniel Last Name: lastName Author ID: 2
With Book, we have a more complex domain class. It has a few different property types and it also has required related domain class of Author.
If you don’t care about any of the specific values on the book, you can simply call build() to have a valid, saved book instance:
import bookstore.*
def b = Book.build()
println "Book title: $b.title"
println "Book price: $b.price"
println "Book published: $b.published"
println "Book author: $b.author"
println "Book id: $b.id"
Prints:
Book title: title Book price: 5.95 Book published: Mon Apr 13 23:11:34 CDT 2009 Book author: firstName lastName Book id: 1 Book author id: 3
Again, if you want to override some of the values, you can pass them in the map:
import com.example.*
def b = Book.build(title: "Tomorrow Midnight",
author: Author.build(firstName: "Ray", lastName: "Bradbury")
)
println "Book title: $b.title"
println "Book price: $b.price"
println "Book published: $b.published"
println "Book author: $b.author"
println "Book id: $b.id"
println "Book author id: $b.author.id"
Resulting in:
Book title: Tomorrow Midnight
Book price: 5.95
Book published: Mon Apr 13 23:13:22 CDT 2009
Book author: Ray Bradbury
Book id: 2
Book author id: 4
The build-test-data plugin lets you concentrate on the data that’s pertinent to your test, rather than building and maintaing large datasets that aren’t tailor made for your test.
findOrBuild()
Before creating an object (and it’s entire graph of required related objects) it will look to see if there’s already an object that meets the build criteria. If there is, it will return that, otherwise, it will create a new one:
// Finds book created above with the existing author
def b = Book.findOrBuild(title: "Tomorrow Midnight")
assert b.author.lastName == "Bradbury"
// Creates new book with author
def newBook = Book.findOrBuild(title: "My Fancy New Book")
assert newBook != null
assert newBook.author != null
I find findOrBuild()
to be really useful in grails console sessions where I want to create some dummy test data once and then do a number of operations on it. Previously, I had to comment out all the creation logic so that new objects weren’t getting created on every script execution. Now it’ll just return them and I can focus on writing the console code I wanted to try out rather than messing with the creation logic.
build(save: false)
In some cases you need to build a domain object, but you don’t want to actually save it to the underlying data store. This is particularly useful if you are trying to build a complex graph and need to build one of the children as part of the parent build.
def author = Author.build(
books: [Book.build(name: 'Foo', save: false)]
)
If you don’t have a complicated object graph, build with saving may not be necessary. However, in cases where you might have a cycle in your graph, it can be necessary to successfully build the graph.
Unit Testing
Starting with Grails 3.3, unit testing is implemented via a set of traits that add support for testing the various artifacts types (Controllers, Services, Domains, etc.).
The build-test-data plugin uses the same approach by adding the:
trait BuildDataTest
This trait extends the built-in DataTest
trait to setup the domain objects and domain graph for construction by build-test-data.
For unit tests, you can use the standard getDomainClassesToMock
method to provide the top-level list of domain objects that are required by the test.
class BookUnitSpec extends Specification implements BuildDataTest {
@Override
Class[] getDomainClassesToMock() {
[Book] // Author will be mocked automatically
}
void "building a book will also build an author"() {
when:
def book = Book.build()
then:
book.author != null
}
}
or it can be done in setupSpec
class LibraryUnitSpec extends Specification implements BuildDataTest {
void setupSpec(){
mockDomains(Book, Magazine)
}
...
You don’t have to explicitly declare that you are building an Author if Book has a dependency there, build-test-data will figure that out for you and tells Grails to mock it automatically. |
This plugin also provide a BuildDomainTest
trait as a replacement to the grails.testing.gorm.DomainUnitTest
that allows the use of generics like so
class BookUnitSpec extends Specification implements BuildDomainTest<Book> {
void "building a book will also build an author"() {
expect:
domain.name
domain.author
}
void "building and author will build a book"() {
when:
def author = build(Author)
expect:
author.name
author.book
}
}
Using @Build
Alternately, you can continue to use the legacy build-test-data @Build
annotation to specify classes to mock. The @Build
annotation currently has the added bonus of supporting code completion in some editors.
@Build([Book])
class BookUnitSpec extends Specification implements BuildDataTest {
void "building a book will also build an author"() {
when:
def book = Book.build()
then:
book.author != null
}
}
Even if you are using the @Build annotation, you must still implement the BuildDataTest trait.
|
Trait Methods
The TestDataBuilder
and therefore the BuildDataTest
trait adds the builder methods to your test class if you prefer to call them directly.
These call out to statics in the TestData
class.
public <T> T build(Map args = [:], Class<T> clazz) {
TestData.build(args, clazz)
}
public <T> T build(Class<T> clazz, Map<String, Object> propValues) {
TestData.build([:], clazz, propValues)
}
public <T> T build(Map args, Class<T> clazz, Map<String, Object> propValues) {
TestData.build(args, clazz, propValues)
}
public <T> T findOrBuild(Class<T> clazz, Map<String, Object> propValues = [:]) {
TestData.build([find: true], clazz, propValues)
}
In that case you can just call build()
and pass in the Class that you which to build or call the TestData methods directly.
See the groovyDocs on the TestData for more details
class BookUnitSpec extends Specification implements BuildDataTest {
@Override
Class[] getDomainClassesToMock() {
[Book]
}
void "building a book using the trait method"() {
when:
def book = build(Book, [title: 'Book Title'])
then:
book.author != null
}
}
HibernateSpec Multi-Tenant
Multi-Tenant example test using HibernateSpec
import static grails.buildtestdata.TestData
//TestDataBuilder will take care of before to load configuratio and after methods to clear cache
class MultiTenantHibernateSpec extends HibernateSpec implements TestDataBuilder {
List<Class> getDomainClasses() {
[Foo, Bar]
}
@Override
Map getConfiguration() {
[(Settings.SETTING_MULTI_TENANT_RESOLVER_CLASS) : SystemPropertyLongTenantResolver,
(Settings.SETTING_MULTI_TENANCY_MODE) : 'DISCRIMINATOR',
(org.grails.orm.hibernate.cfg.Settings.SETTING_DB_CREATE): "create-drop"]
}
def cleanupSpec() {
System.setProperty(SystemPropertyLongTenantResolver.PROPERTY_NAME, '')
}
def "Do some test"(){
when:
def foo = build(Foo)
then:
foo.id
}
def "Do some test"(){
when:
//Call on TestData directly
def bar = TestData.build(Bar, save: false)
then:
!bar.id
}
}
Integration Testing
Integration tests don’t require the same type of setup as Unit Tests since the Grails environment is fully bootstrapped.
The plugin includes a trait that should be mixed into Integration tests to provide additional functionality:
trait TestDataBuilder
The trait is not strictly required, but it does include some setup and cleanup functionality that could be useful in some cases. Additional functionality may be added in the future via this trait, so implementing it is preferred. |
Using Build Annotation
In prior versions, the @Build
annotation was strictly for Unit Tests (it would cause numerous issues when used on integration tests, which was a common source of confusion on our projects).
Starting with build-test-data 3.3, the @Build
annotation can be used safely on integration tests. The primary reason to use it would be to take advantage of IDE autocomplete for properties when calling the DomainObject.build()
method.
Test Data Config
If you want to specify global defaults, you will need to create a TestDataConfig.groovy
(using the examples below as a starting point):
src/test/resources/TestDataConfig.groovy
The config file is most useful when you are building a class that has dependencies on an entire object graph and you want to be able to control some of the values in the object graph that gets created automatically.
For example, if you’re building a Book, and a Book has an Author which has a Publisher. If you want to control the name of the publisher (without manually creating the entire object graph by hand), you can instead specify in the config file that when publishers are created, they should be named X:
testDataConfig {
sampleData {
'com.example.Publisher' {
name = "Pragmatic Bookshelf"
}
}
}
Then, when you build a Book, the Publisher is named appropriately:
assert "Pragmatic Bookshelf" == Book.build().author.publisher.name
Inside the config’s sampleData closure, you specify a domain class name. Use quotes around the fully qualified class name.
Prior versions allowed a Class as a key for sampleData, but that is not longer supported. You must specify the entity name as a fully qualified String. |
For each class specified, you can then provide the properties that you’d like populated and give it a property value to use:
testDataConfig {
sampleData {
'com.example.Hotel' {
name = "Hilton"
}
}
}
Using that config file in a test for a com.example.Hotel domain object with a name property would produce the following results in a test:
println Hotel.build().name // prints "Hilton"
It follows the normal groovy config file pattern and you can also override things at an environmental level if desired. This example shows how in development we can return "Motel 6" but production will return "Hilton".
testDataConfig {
sampleData {
'com.example.Hotel' {
name ="Motel 6"
}
}
}
environments {
production {
testDataConfig {
sampleData {
'com.example.Hotel' {
name = "Hilton"
}
}
}
}
}
You are not required to have a TestDataConfig.groovy file.
Test Specific Config
It’s also possible to specify a config that is used during a particular test.
Your test should implement the TestDataBuilder
for integration tests or BuildDataTest
for unit tests. Just override the doWithTestDataConfig
method and return a Closure just like your TestDataConfig.groovy
:
Closure doWithTestDataConfig() {{->
testDataConfig {
sampleData {
'config.Hotel' {
name = {-> "Westin" }
}
}
}
}}
This will merge the config provided from this method with the global configuration (if any) in TestDataConfig.groovy
.
The configuration is merged at the entity level, not the attribute level. In the above example, the global sample data for the 'config.Hotel' entity will be completely replaced by this configuration.
|
After using this method, you’ll likely want to call TestDataConfigurationHolder.reset()
to put the configuration back to the normal config file.
void cleanup() {
TestDataConfigurationHolder.reset()
}
Generating Dynamic Values
If you have a need to have different values given to a domain object (possibly for a unique constraint), you can instead specify a closure in your config file. This closure will be called and is expected to return the property value. Here’s an example that generates a unique title for a book:
testDataConfig {
sampleData {
'com.foo.Book' {
def i = 1
title = {-> "title${i++}" } // creates "title1", "title2", ...."titleN"
}
}
}
One thing to watch out for with this is that if your tests are expecting particular values generated by the closure, you could run into sequencing effects if your test run out of order (or you add a new test). To get around this, you can reset the config before you run the test. This is a good idea if you’re using dynamic values:
void setup() {
TestDataConfigurationHolder.reset()
}
When a Closure
is used as an attribute, it can additionally take 1 or 2 parameters. The first parameter is a Map
of all of the properties that have been resolved so far. This allows you to configure defaults based on the values of other properties given or defaulted on the object. For example:
'config.Hotel': {
name: { ->
i++ % 2 == 0 ? "Holiday Inn" : "Hilton"
},
faxNumber: { values ->
"Fax number for $values.name"
}
}
The second parameter is the instance that is currently being built. In the above example, that would be the new (unsaved) instance of Hotel
. This is useful if you want to build associated data and you need a reference to the parent object. For example:
'bookstore.Author': {
books: { values, obj -> [Book.build(save: false, author: obj, title: 'James')] }
}
In this case, the transient obj
instance is needed so that the Book
instance can associate back to the parent.
In this case, you must use the [save: false] argument, otherwise build test data will attempt to save the Book and fail with a transient object exception on Author .
|
Specifying Additional Dependencies
Occasionally it is necessary to build another object manually in your TestDataConfig
file. Usually this will look something like this:
'test.Book' {
author = {-> Author.build() }
}
There are a number of reasons that you might want to do this. For complex object graphs, this may be necessary to prevent failures due to loops in the graph. Also, for some graphs, you may need to do a build(save: false) or a findOrBuild() for a particular association. It may also just be that you want to default an optional association, since build-test-data will only build out the graph for required properties.
Regardless, this introduces a hidden dependency when building the Book
class. If you only have @Build([Book])
in your test case, you’ll likely get a MethodMissing exception building Author
. To resolve this issue, you may specify additional objects to mock each time Book is mocked:
unitAdditionalBuild = ['test.Book': [test.Author]]
Each key is the full package name of a domain object and the value is a List of either a Class or String indicating additional domains to include whenever the key object is built.
The chain is recursive in that Author could, itself, have additional dependencies. So if Book
belongsTo
an Author
which belongsTo
a Publisher
, build`ing a `Book
would give you all three without explicitly saying that a Book
needs a Publisher
with this:
unitAdditionalBuild = [
'test.Book' : [test.Author],
'test.Author' : [test.Publisher]
]
Again, this is only needed for Unit testing as Grails includes all domain objects, by default, for integration tests.
Building Abstract Classes
If your domain classes reference abstract classes, build test data needs to know which concrete class to build to satisfy the dependencies.
By using the abstractDefault
configuration option, you can override this behavior globally to indicate which specific subclass is desired for a given base class.
abstractDefault = [
'test.AbstractBook' : MyBook,
'test.AbstractAuthor' : Tolkien
]
In this example, any time BTD needs to build an AbstractBook
, it will build an instance of MyBook
by default. If you only want to override the subclass for a particular domain object, you may want to consider just adding a default value in sampleData
.
This default will also apply if you manually build an instance of an abstract class. For example, calling AbstractBook.build()
in this example will return a MyBook
instance.
Behavior Change
Prior to version 3.3, build-test-data would automatically try to find a concrete subclass. However, this was horribly slow as Java makes it very difficult to accomplish this task. For this reason, it’s now required to use abstractDefaults when subclasses are required.
|
Using a different name for TestDataConfig.groovy
In some cases you need to have a different name for TestDataConfig.groovy
. The reason could be, that you have project that depends on several inline plugins all using this plugin. If all plugins contains TestDataConfig.groovy
you will end up with a compile error saying that TestDataConfig.groovy
is duplicated in your classpath.
Using a different name by setting this in application.groovy:
grails.buildtestdata.testDataConfig = "MyTestDataConfig"
With Grails 3, you would add to application.yml:
grails:
buildtestdata:
testDataConfig: "MyTestDataConfig"
and rename your TestDataConfig.groovy
to MyTestDataConfig.groovy
Examples
Once you have the plugin installed, it should be as easy as calling build()
on any of your domain objects in your tests.
If you don’t pass any parameters to build, it will attempt to provide values for all of the properties that it determines have required values based on the constraints. This includes other domain objects.
If you want to specify particular values, you can pass build a map (just like you can to a domain object constructor). The plugin will attempt to create new values for any required properties that you don’t specify in the map.
domainObject.build(myProperty:'myPropertyValue')
The build method default is to attempt to save the populated domain object. You can avoid saving the object first by calling the buildWithoutSave method:
domainObject.buildWithoutSave(myProperty:'myPropertyValue')
Working with Unique Constraints
There currently isn’t built-in support for uniqueness constraints, but it’s pretty easy to get this working using a closure in the config file to generate a series of unique values.
testDataConfig {
sampleData {
'com.foo.Book' {
def i = 1
title = {-> "title${i++}" } // creates "title1", "title2",...
}
}
}
If your test cares about this unique value, you’ll likely want to reset the configuration either directly in your test or in the setUp of the test. Otherwise, other tests could have grabbed the value that you were expecting. To reset the configuration back to the original state, use this:
TestDataConfigurationHolder.reset()
Building Objects and Building Associated Child Objects
When you have a parent object that you want to have specific child objects, here’s one way this can be done.
class Issue {
String title
static hasMany = [pages: Page]
}
class Page {
Integer number
static hasMany = [advertisments: Advertisment]
static belongsTo = [issue: Issue]
}
You can create an issue and then give it pages:
// Build the issue first
def issue = Issue.build()
(1..5).collect { Page.build(issue: issue, number: it) }
// Pages are automatically added to the issue's hasMany pages collection
assertEquals 5, issue.pages.size()
// Don't build extra issues when building pages
assertEquals 1, Issue.list().size()
We don’t want to build the Page objects before we have the Issue otherwise the build method will create dummy issues in the database for each page (and that last assert would fail).
Assigning System Wide Default Values
Logging Information
If you want to see the detail of what objects the plugin is trying to save, you can set the logging level for the DomainTestDataService and the DomainInstanceBuilder to info. Just stick this line in your 'logback.groovy'
logger("grails.buildtestdata.DomainInstanceBuilder", INFO)
If you want to see all of the gory detail about what the plugin is doing as it traverses the object graph and examines each property, set the logging level to debug and add in the property handlers:
logger("grails.buildtestdata", DEBUG)