Skip to content

Commit

Permalink
Pass the transient object as the 2nd closure parameter to TDC
Browse files Browse the repository at this point in the history
  • Loading branch information
longwa committed May 31, 2018
1 parent 54f01a7 commit 69b8204
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 50 deletions.
77 changes: 44 additions & 33 deletions docs/src/asciidoc/testdataconfig.adoc
Expand Up @@ -66,6 +66,34 @@ It follows the normal groovy config file pattern and you can also override thing

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`:
```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`.

NOTE: 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.
```groovy
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:
```groovy
Expand All @@ -86,44 +114,27 @@ void setup() {
}
```

=== Test Specific Config
It's also possible to specify a config that is used during a particular test. Just set the sampleData property on the `TestDataConfigurationHolder` to a map containing your configuration values, where the key in the map is the Domain class (with package) and the value is another map where the keys are property names and the values are the value to assign to the property.

Using a static value:
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:
```groovy
def hotelNameAlwaysHilton = ['example.Hotel': [name: "Hilton"]]
TestDataConfigurationHolder.sampleData = hotelNameAlwaysHilton

def hilton = Hotel.build()
assertEquals "Hilton", hilton.name
'config.Hotel': {
name: { ->
i++ % 2 == 0 ? "Holiday Inn" : "Hilton"
},
faxNumber: { values ->
"Fax number for $values.name"
}
}
```

Using a closure for dynamic values:
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:
```groovy
def i = 0
def hotelNameAlternates = [
('config.Hotel'): [name: {->
i++ % 2 == 0 ? "Holiday Inn" : "Hilton"
}]
]
TestDataConfigurationHolder.sampleData = hotelNameAlternates

def holidayInn = Hotel.build()
assertEquals "Holiday Inn", holidayInn.name

def hilton = Hotel.build()
assertEquals "Hilton", hilton.name

def backToHolidayInn = Hotel.build()
assertEquals "Holiday Inn", backToHolidayInn.name
'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.

NOTE: 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`.

After using this method, you'll likely want to call `TestDataConfigurationHolder.reset()` to put the configuration back to the normal config file.
```groovy
void cleanup() {
TestDataConfigurationHolder.reset()
}
```

=== Specifying Additional Dependencies
Occasionally it is necessary to build another object manually in your `TestDataConfig` file. Usually this will look something like this:
Expand Down
@@ -1,5 +1,7 @@
package base

import bookstore.Author
import bookstore.Book
import config.Hotel
import grails.buildtestdata.TestDataBuilder
import grails.buildtestdata.TestDataConfigurationHolder
Expand Down Expand Up @@ -106,6 +108,27 @@ class TestDataConfigTests extends Specification implements TestDataBuilder {
hilton.faxNumber == "Fax number for Hilton"
}

void testConfigClosureWithPassedInstanceValue() {
TestDataConfigurationHolder.sampleData = [
('bookstore.Author'): [books: { values, obj ->
[
build([save: false], Book, [author: obj, title: 'James']),
build([save: false], Book, [author: obj, title: 'Kevin'])
]
}]
]
when:
def author = build(Author, [name: "Aaron"])

then:
author.books
author.books.each {
assert it.author == author
}
author.books.find { it.title == 'James' }
author.books.find { it.title == 'Kevin' }
}

void cleanupSpec() {
TestDataConfigurationHolder.reset()
}
Expand Down
Expand Up @@ -63,8 +63,8 @@ class TestDataConfigurationHolder {
config.getSuppliedPropertyValue(propertyValues, domainName, propertyName)
}

static Map<String, Object> getPropertyValues(String domainName, Set<String> propertyNames, Map<String, Object> propertyValues = [:]) {
config.getPropertyValues(domainName, propertyNames, propertyValues)
static Map<String, Object> getPropertyValues(String domainName, Object newInstance, Set<String> propertyNames, Map<String, Object> propertyValues = [:]) {
config.getPropertyValues(domainName, newInstance, propertyNames, propertyValues)
}

/**
Expand Down Expand Up @@ -149,24 +149,31 @@ class TestDataConfiguration {
getConfigFor(domainName)?.keySet() ?: [] as Set<String>
}

Object getSuppliedPropertyValue(Map<String, Object> propertyValues, String domainName, String propertyName) {
Object getSuppliedPropertyValue(Map<String, Object> propertyValues, String domainName, String propertyName, Object newInstance = null) {
if (!sampleData[domainName]) {
throw new IllegalArgumentException("Sample data for $domainName does not exist")
}

// Fetch both and either invoke the closure or just return raw values
Object value = sampleData[domainName][propertyName]
if (value instanceof Closure) {
Closure block = value as Closure
return block.maximumNumberOfParameters > 0 ? block.call(propertyValues) : block.call()
if (!(value instanceof Closure)) {
return value
}

value
Closure block = value as Closure
switch(block.maximumNumberOfParameters) {
case 1:
return block.call(propertyValues)
case 2:
return block.call(propertyValues, newInstance)
default:
return block.call()
}
}

Map<String, Object> getPropertyValues(String domainName, Set<String> propertyNames, Map<String, Object> propertyValues = [:]) {
Map<String, Object> getPropertyValues(String domainName, Object newInstance, Set<String> propertyNames, Map<String, Object> propertyValues = [:]) {
for (propertyName in propertyNames) {
propertyValues[propertyName] = getSuppliedPropertyValue(propertyValues, domainName, propertyName)
propertyValues[propertyName] = getSuppliedPropertyValue(propertyValues, domainName, propertyName, newInstance)
}
return propertyValues
}
Expand Down
Expand Up @@ -50,9 +50,10 @@ class PogoDataBuilder implements DataBuilder {
return ctx.target
}

// TODO - Implement new initialProps design
// Map initialProps = BuildTestDataApi.initialPropsResolver.getInitialProps(target)
Map initialProps = findMissingConfigValues(ctx.data)
// Create a new empty instance
def instance = getNewInstance()

Map initialProps = findMissingConfigValues(ctx.data, instance)
if (initialProps) {
if (ctx.data) {
ctx.data = [:] + initialProps + ctx.data
Expand All @@ -61,21 +62,18 @@ class PogoDataBuilder implements DataBuilder {
ctx.data = [:] + initialProps
}
}
def instance = getNewInstance()

if (ctx.data) {
dataBinder.bind(instance, new SimpleMapDataBindingSource(ctx.data))
}

instance
}

Map<String, Object> findMissingConfigValues(Map propValues) {
Map<String, Object> findMissingConfigValues(Map propValues, Object newInstance) {
Set<String> missingProperties = TestDataConfigurationHolder.getConfigPropertyNames(targetClass.name) - propValues.keySet()
TestDataConfigurationHolder.getPropertyValues(targetClass.name, missingProperties, propValues)
TestDataConfigurationHolder.getPropertyValues(targetClass.name, newInstance, missingProperties, propValues)
}


def getNewInstance() {
if (List.isAssignableFrom(targetClass)) {
[] as List
Expand Down

0 comments on commit 69b8204

Please sign in to comment.