ribbon

Dru is Data Reconstruction Utility which helps to create and maintain test data based on real-life production data as it is for example often easier to grab production data of web application as JSON than trying to create selective export from one or more data stores.

The quality of tests depends on the quality of the test data being used. It is important to keep the test data aligned with the production data as much as it is possible. This was relatively easy in the time of relational databases' dominance as test data can be set up with database dump but now, when the data required for the test can be stored in multiple data the safest way to load test data is to use your own data persistence layer or underlying framework. Dru comes with out of box support for Plain Old Java Objects (POJOs), GORM and AWS DynamoDB. It can consume JSON or YAML files as data sources.

Dru is designed to load complex data models. References by ids are translated into associations even the identity in newly created data store is not the same as original one. For example if you have entity Item with id 5 and entity ItemComment with property itemId with value 5 then the loaded ItemComment entity will have itemId property set to the actual id of the loaded Item e.g. 1.

1. Installation

Dru is available in JCenter. At the moment, you can use any of POJO, GORM or DynamoDB modules your project.

Gradle Installation
repositories {
    mavenCentral()
}

dependencies {
    // load just simple implementation with POJO client and reflection based parser
    testImplementation "com.agorapulse:dru:0.8.1"

    // and pick any client
    testImplementation "com.agorapulse:dru-client-dynamodb:0.8.1"
    testImplementation "com.agorapulse:dru-client-gorm:0.8.1"
    testImplementation "com.agorapulse:dru-client-micronaut-data:0.8.1"

    // and pick any parser
    testImplementation "com.agorapulse:dru-parser-json:0.8.1"
    testImplementation "com.agorapulse:dru-parser-sql:0.8.1"
    testImplementation "com.agorapulse:dru-parser-yaml:0.8.1"
}

2. Setup

Dru provides Closable interface. Calling close at the end of the test will guarantee that fresh data are loaded for the next test. If you are using Spock then you can use @AutoCleaenup annotation on the field to call the close method automatically.

Simple Specification
package avl

import com.agorapulse.dru.Dru
import spock.lang.AutoCleanup
import spock.lang.Specification

/**
 * Test loading item.
 */
class ItemSpec extends Specification {


    @AutoCleanup Dru dru = Dru.create {                                                 (1)
        from ('item.json') {                                                            (2)
            map { to Item }                                                             (3)
        }
    }

    void 'entities can be access from the data set'() {
        expect:
            dru.findAllByType(Item).size() == 1                                         (4)
        when:
            Item item = dru.findByTypeAndOriginalId(Item, ID)                           (5)
        then:
            item
            item.name == 'PX-41'                                                        (6)
            item.description == "The PX-41 is a very dangerous mutator engineered in the top secret PX-Labs, located in the Arctic Circle. It is capable of turning any living things in the world into a purple, furry, indestructible, mindless, killing machine that is so dangerous that it can destroy anything in its path."
            item.tags.contains('superpowers')
    }

    void 'all data are used'() {
        expect:
            dru.report.empty                                                            (7)
    }

    private static final String ID = '050e4fcf-158d-4f44-9b8b-a6ba6809982e:PX-41'
}
1 Prepare the data loading plan
2 Load the content of items.json
3 Map the root element to Item entity
4 Loaded entity is available by its type
5 Entity can be loaded by its original id
6 Properties are loaded as expected
7 Check whether all properties from the source has been used

You can take a look at the item.json file containing the test data:

item.json
{
  "id": "050e4fcf-158d-4f44-9b8b-a6ba6809982e",
  "name": "PX-41",
  "description": "The PX-41 is a very dangerous mutator engineered in the top secret PX-Labs, located in the Arctic Circle. It is capable of turning any living things in the world into a purple, furry, indestructible, mindless, killing machine that is so dangerous that it can destroy anything in its path.",
  "tags": [
    "mutator",
    "monsters",
    "superpowers"
  ]
}

The file must be located inside a folder of same name as the class where the source was defined i.g avl/ItemSpec/item.json, resp. src/test/resources/avl/ItemSpec/item.json for Gradle project.

3. Source Mapping

You can map directly to the root object or array or to any path inside the source you need:

Complex Path
@AutoCleanup Dru dru = Dru.create {
    from ('items.json') {
        map ('mission.items') {
            to Item
        }
    }
}
items.json
{
  "mission": {
    "items": [
      {
        "id": "050e4fcf-158d-4f44-9b8b-a6ba6809982e",
        "name": "PX-41",
        "description": "The PX-41 is a very dangerous mutator engineered in the top secret PX-Labs, located in the Arctic Circle. It is capable of turning any living things in the world into a purple, furry, indestructible, mindless, killing machine that is so dangerous that it can destroy anything in its path.",
        "tags": [
          "mutator",
          "monsters",
          "superpowers"
        ]
      }
    ]
  }
}

4. Property and Type Mapping

For basic use cases when the source exactly fits the entity properties there is no need for additional mappings.

4.1. Default Values

You can set a default value for a property. The object passed as argument to the closure is the map obtained from the source.

Default Value
@AutoCleanup Dru dru = Dru.create {
    from ('item.json') {
        map {
            to (Item) {
                defaults {
                    description = "Description for $it.name"
                }
            }
        }
    }
}

void 'entities can be access from the data set'() {
    when:
        Item item = dru.findByTypeAndOriginalId(Item, ID)
    then:
        item
        item.name == 'PX-41'
        item.description == 'Description for PX-41'
}
item.json
{
  "id": "050e4fcf-158d-4f44-9b8b-a6ba6809982e",
  "name": "PX-41",
  "tags": [
    "mutator",
    "monsters",
    "superpowers"
  ]
}

4.2. Overriding Properties

You can override any value coming from the source. The object passed as argument to the closure is the map obtained from the source. Contrary to defaults, the value is set to overridden value even it is present in the source.

Overriding Properties
@AutoCleanup Dru dru = Dru.create {
    from ('item.json') {
        map {
            to (Item) {
                overrides {
                    description = "Description for $it.name"
                }
            }
        }
    }
}

void 'entities can be access from the data set'() {
    when:
        Item item = dru.findByTypeAndOriginalId(Item, ID)
    then:
        item
        item.name == 'PX-41'
        item.description == 'Description for PX-41'
}
item.json
{
  "id": "050e4fcf-158d-4f44-9b8b-a6ba6809982e",
  "name": "PX-41",
  "description": "The PX-41 is a very dangerous mutator engineered in the top secret PX-Labs, located in the Arctic Circle. It is capable of turning any living things in the world into a purple, furry, indestructible, mindless, killing machine that is so dangerous that it can destroy anything in its path.",
  "tags": [
    "mutator",
    "monsters",
    "superpowers"
  ]
}

4.3. Aliasing Properties

You can alias properties with different names in the source and in the entity.

Aliasing Properties
@AutoCleanup Dru dru = Dru.create {
    from ('item.json') {
        map {
            to (Item) {
                map('desc') {
                    to (description: String)
                }
            }
        }
    }
}

void 'entities can be access from the data set'() {
    when:
        Item item = dru.findByTypeAndOriginalId(Item, ID)
    then:
        item
        item.name == 'PX-41'
        item.description == "The PX-41 is a very dangerous mutator engineered in the top secret PX-Labs, located in the Arctic Circle. It is capable of turning any living things in the world into a purple, furry, indestructible, mindless, killing machine that is so dangerous that it can destroy anything in its path."
        item.tags.contains('superpowers')
}
item.json
{
  "id": "050e4fcf-158d-4f44-9b8b-a6ba6809982e",
  "name": "PX-41",
  "desc": "The PX-41 is a very dangerous mutator engineered in the top secret PX-Labs, located in the Arctic Circle. It is capable of turning any living things in the world into a purple, furry, indestructible, mindless, killing machine that is so dangerous that it can destroy anything in its path.",
  "tags": [
    "mutator",
    "monsters",
    "superpowers"
  ]
}

4.4. Ignoring Properties

If you want to be sure that every information from the source is persisted you can access MissingPropertiesReport object from Dru instance. The report contains list of properties which hasn’t been matched. If you explicitly ignore a property for example because it is derived it will not appear in the report.

Ignoring Properties
@AutoCleanup Dru dru = Dru.create {
    from ('item.json') {
        map {
            to (Item) {
                ignore 'owner'
            }
        }
    }
}

void 'owner does is not present in the report'() {
    when:
        dru.load()
    then:
        dru.report.empty
}
item.json
{
  "id": "050e4fcf-158d-4f44-9b8b-a6ba6809982e",
  "name": "PX-41",
  "description": "The PX-41 is a very dangerous mutator engineered in the top secret PX-Labs, located in the Arctic Circle. It is capable of turning any living things in the world into a purple, furry, indestructible, mindless, killing machine that is so dangerous that it can destroy anything in its path.",
  "owner": "Unknown",
  "tags": [
    "mutator",
    "monsters",
    "superpowers"
  ]
}

4.5. Conditional Type Mapping

You can add condition to type mappings to map to different entities based on source properties.

Conditional Mapping
@AutoCleanup Dru dru = Dru.create {
    from ('persons.json') {
        map {
            to (Agent) {
                when { it.type == 'agent' }
                defaults { securityLevel = 1 }
            }
            to (Villain) {
                when { it.type == 'villain' }
            }
        }
    }
}

void 'entities are mapped to proper types'() {
    expect:
        dru.findAllByType(Agent).size() == 1
        dru.findAllByType(Villain).size() == 1
}
persons.json
[
  {
    "id": 12345,
    "name": "Felonius Gru",
    "bio": "Born from the family with long line of villainy and formerly the world's greatest villain.",
    "type": "agent"
  },
  {
    "id": 247,
    "name": "El Macho",
    "bio": "A former renowned, nearly superhuman-level strong bank robber who now wants to dominate the world",
    "type": "villain"
  }
]

4.6. Nested Type Mapping

You can nest type mapping to maps complex hierarchical structures.

Nested Mapping
@AutoCleanup Dru dru = Dru.create {
    from ('agents.json') {
        map {
            to (Agent) {
                map ('manager') {
                    to (Agent) {
                        defaults { securityLevel = 1 }
                    }
                }
            }
        }
    }
}

void 'nested properties are mapped'() {
    expect:
        dru.findAllByType(Agent).size() == 2
        dru.findByTypeAndOriginalId(Agent, 12345).manager.name == 'Silas Ramsbottom'
}
agents.json
[
  {
    "id": 12345,
    "name": "Felonius Gru",
    "bio": "Born from the family with long line of villainy and formerly the world's greatest villain.",
    "securityLevel": 2,
    "manager": {
      "id": 101,
      "name": "Silas Ramsbottom"
    }
  }
]

You can declare the type mapping at top level so it applies to every occurrence of given type wherever in the tree:

Top Level Mapping
@AutoCleanup Dru reuse = Dru.create {
    from ('agents.json') {
        map {
            to (Agent) {
                map ('manager') {
                    to (Agent)
                }
            }
        }
    }

    any (Agent) {
        defaults { securityLevel = 1 }
    }
}

4.7. Partial Retrieval

You can assign just a particular property of the loaded entity, usually an id.

Partial Retrieval
@AutoCleanup Dru dru = Dru.create {
    from ('missionLogEntry.json') {
        map {
            to (MissionLogEntry) {
                map ('agent') {
                    to (agentId: Agent) {
                        just { id }
                        defaults {
                            securityLevel = 1
                        }
                    }
                }
            }
        }
    }
}

void 'mission log entry has agent id assigned'() {
    expect:
        dru.findByType(Agent)
        dru.findByType(MissionLogEntry).agentId == 1
}
missionLogEntry.json
{
  "mission": 7,
  "date": "2013-07-05T01:23:22Z",
  "type": "started",
  "description": "Mission started by Silas Ramsbottom",
  "agent": {
    "id": 101,
    "name": "Silas Ramsbottom"
  }
}

5. Data Sets

Data set is unit of reuse in Dru. Data set can contain multiple sources and mappings. The sources are evaluated relatively to the class in which the data set is defined. You usually defined one data set for mapping an entity and other to load the source to maximise reuse.

Agents Data Set
package avl

import com.agorapulse.dru.Dru
import com.agorapulse.dru.PreparedDataSet

/**
 * Agents data set.
 */
class AgentsDataSet {
    public static final PreparedDataSet agentsMapping = Dru.prepare {                   (1)
        any (Agent) {
            map ('manager') {
                to (Agent)
            }
            defaults {
                securityLevel = 1
            }
        }
    }

    public static final PreparedDataSet agents = Dru.prepare {                          (2)
        include agentsMapping                                                           (3)
        from ('agents.json') {
            map {
                to (Agent)
            }
        }
    }
}
1 Define data set for agents mapping
2 Define data set for agents data
3 Include data set for agents mapping

You can use method include to include any existing data set or you can use method load to load data set into existing data set.

Using Data Set
@AutoCleanup Dru dru = Dru.create {
    include AgentsDataSet.agents
}

void 'agents get loaded from data set using prepare and include'() {
    expect:
        dru.findAllByType(Agent).size() == 2
        dru.findByTypeAndOriginalId(Agent, 12345).manager.name == 'Silas Ramsbottom'
}

void 'agents get loaded from data set using load'() {
    given:
        DataSet dataSet = Dru.create(this).load(AgentsDataSet.agents)
    expect:
        dataSet.findAllByType(Agent).size() == 2
        dataSet.findByTypeAndOriginalId(Agent, 12345).manager.name == 'Silas Ramsbottom'
}

If additional logic needs to be executed when the data set is loaded or changed significantly then you can use whenLoaded hook. You can trigger the hooks manually using loaded method of the data set.

Using Data Set Hooks
@AutoCleanup Dru dru = Dru.create {
    from ('AGENTS') {
        map {
            to (Agent) {
                map ('manager') {
                    to (Agent) {
                        defaults { securityLevel = 1 }
                    }
                }
            }
        }
    }
}

void 'calling when loaded hook'() {
    when:
        int count = 0
        dru.load {
            whenLoaded {
                count++
            }
        }
    then:
        count == 1                                                                  (1)
    when:
        dru.loaded()
    then:
        count == 2                                                                  (2)
}
1 First call to the hook is triggered immediately as we are defining the hook inside load method
2 Second call to the hook is triggered manually using loaded method
Dru is also a data set with special behaviour. It gets cleared after every test method run.

6. Parsers

Dru loads all parsers available on the classpath automatically. Which client is used is determined by the name of the source.

6.1. Reflection

Reflection parser is the simples parser. It searches for property of given name in the class where the data set is defined. This is a default parser if any other does not support given name.

Using Reflection Parser
private static final List<Map<String, Object>> AGENTS = [
    [
        id           : 12345,
        name         : 'Felonius Gru',
        bio          : 'Born from the family with long line of villainy and formerly the world\'s greatest villain.',
        securityLevel: 2,
        manager      : [
            id  : 101,
            name: 'Silas Ramsbottom'
        ]
    ]
]

@AutoCleanup Dru dru = Dru.create {
    from ('AGENTS') {
        map {
            to (Agent) {
                map ('manager') {
                    to (Agent) {
                        defaults { securityLevel = 1 }
                    }
                }
            }
        }
    }
}

void 'nested properties are mapped'() {
    expect:
        dru.findAllByType(Agent).size() == 2
        dru.findByTypeAndOriginalId(Agent, 12345).manager.name == 'Silas Ramsbottom'
}

6.2. JSON

JSON parser parses JSON files to combination of maps and lists. The source files must end with .json to get parsed and they must be contained in directory with the same name as the reference class (unit test or data set)

Using JSON Parser
@AutoCleanup Dru dru = Dru.create {
    from ('agents.json') {
        map {
            to (Agent) {
                map ('manager') {
                    to (Agent) {
                        defaults { securityLevel = 1 }
                    }
                }
            }
        }
    }
}

void 'nested properties are mapped'() {
    expect:
        dru.findAllByType(Agent).size() == 2
        dru.findByTypeAndOriginalId(Agent, 12345).manager.name == 'Silas Ramsbottom'
}
agents.json
[
  {
    "id": 12345,
    "name": "Felonius Gru",
    "bio": "Born from the family with long line of villainy and formerly the world's greatest villain.",
    "securityLevel": 2,
    "manager": {
      "id": 101,
      "name": "Silas Ramsbottom"
    }
  }
]

6.3. SQL

SQL parser runs provided SQL scripts. The results SELECT statements are loaded as map of a following structure:

[
    table_name: [
      [column_1: value_1, column_2: value_2, ...],
      ...
    ],
    ...
]
Some database implementation such as H2 returns the table and column names all-upper-case. You can see the results when you set com.agorapulse.dru.sql logger to DEBUG.

The source files must end with .sql to get parsed and they must be contained in a directory with the same name as the reference class (unit test or data set).

Using SQL Parser
package com.agorapulse.dru.parser.sql

import com.agorapulse.dru.Dru
import org.h2.jdbcx.JdbcDataSource
import spock.lang.Specification

import javax.sql.DataSource

class BasicSqlParserSpec extends Specification implements DataSourceProvider {          (1)

    DataSource dataSource = new JdbcDataSource(                                         (2)
        URL: 'jdbc:h2:mem:default',
        user: 'sa',
        password: 'sa',
    )

    Dru dru = Dru.create {
        from 'books.sql', {                                                             (3)
            map 'BOOK', {                                                               (4)
                to Book, {
                    map 'PAGES', { to (pages: Integer) }                                (5)
                    map 'TITLE', { to (title: String) }
                }
            }
        }
    }

    void setup() {
        dru.load()
    }

    void 'load using sql into object'() {
        when:
            List<Book> books = dru.findAllByType(Book)                                  (6)
        then:
            books.size() == 2
            books.first().title == 'It'
            books.first().pages == 1116
            books.last().title == 'The Shining'
            books.last().pages == 659
    }

}
1 The unit test class must implement DataSourceProvider
2 The data source definition
3 Use a SQL script to populate data. The mapping can be omitted if the script contains no selects e.g. from 'books.sql'.
4 You can map a table to an object
5 You can map a column to a property
6 Use Dru’s DataSet methods to obtain the objects loaded

6.4. YAML

YAML parser parses YAML files to combination of maps and lists. The source files must end with .yml or .yaml to get parsed and they must be contained in directory with the same name as the reference class (unit test or data set)

Using YAML Parser
@AutoCleanup Dru dru = Dru.create {
    from ('agents.yml') {
        map {
            to (Agent) {
                map ('manager') {
                    to (Agent) {
                        defaults { securityLevel = 1 }
                    }
                }
            }
        }
    }
}

void 'nested properties are mapped'() {
    expect:
        dru.findAllByType(Agent).size() == 2
        dru.findByTypeAndOriginalId(Agent, 12345).manager.name == 'Silas Ramsbottom'
}
agents.yml
- id: 12345
  name: Felonius Gru
  bio: Born from the family with long line of villainy and formerly the world's greatest
    villain.
  securityLevel: 2
  manager:
    id: 101
    name: Silas Ramsbottom

7. Clients

Dru loads all clients available on the classpath automatically if they support the unit test where Dru instance is defined.

7.1. POJO

POJO client is default fallback client which loads data into Plain Old Java Objects. POJO client is able to recognize associations but it is unable to load other sides of bidirectional relations.

Using POJO Client
@AutoCleanup Dru dru = Dru.create {
    from ('library.json') {
        map {
            to (Library)
        }
    }
}

void 'library is loaded'() {
    expect:
        dru.findAllByType(Library).size() == 1
        dru.findAllByType(Book).size() == 2
}
library.json
{
  "name": "National Library",
  "books": [
    {
      "title": "It",
      "author": "Stephen King"
    },
    {
      "title": "Leviathan Wakes",
      "author": "James S. A. Corey"
    }
  ]
}

7.2. DynamoDB

DynamoDB client is extension to POJO client which understands DynamoDB data mapping annotations (see bellow). The client is used if @DynamoDBTable annotation is present on the class.

Table 1. DynamoDB Annotations
Annotation Effect

@DynamoDBTable

DynamoDB client is used for given class

@DynamoDBHashKey

Property is used as hash key part of the id

@DynamoDBRangeKey

Property is used as hash range part of the id

@DynamoDBIgnore

Property is ignored

@DynamoDBMarshalling

Property is marked as embedded

DynamoDB client determines the hash and range properly from the class so you can later retrieve the entity from the data set.

Using DynamoDB Client
@AutoCleanup Dru dru = Dru.create {
    from ('missionLogEntry.json') {
        map {
            to MissionLogEntry
        }
    }
}

void 'mission log entry has agent id assigned'() {
    given:
        String id = DynamoDB.getOriginalId(MissionLogEntry, 7, '2013-07-05T01:23:22Z')
    expect:
        dru.findByType(MissionLogEntry)
        dru.findByTypeAndOriginalId(MissionLogEntry, id)
}
library.json
[
  {
    "missionId": 7,
    "date": "2013-07-05T01:23:22Z",
    "type": "started",
    "description": "Mission started by Silas Ramsbottom",
    "agentId": 101
  },
  {
    "missionId": 7,
    "date": "2013-07-06T01:23:22Z",
    "type": "succeeded",
    "description": "Mission succeeded by Silas Ramsbottom",
    "agentId": 101
  }
]
MissionLogEntry.groovy
@DynamoDBTable(tableName = "MissionLogEntry")
class MissionLogEntry {

    @DynamoDBHashKey
    Long missionId

    @DynamoDBRangeKey
    Date date

    MissionLogEntryType type

    String description

    @DynamoDBMarshalling(marshallerClass = ExtMarshaller)
    Map<String, Object> ext
}

You can create DynamoDBMapper based on the data in the data set using DynamoDB.createMapper(dataSet).

Using DynamoDBMapper
void 'use dynamodb mapper'() {
    when: "DynamoDB mapper is created from data set"
        DynamoDBMapper mapper = DynamoDB.createMapper(dru)
        Date date = new DateTime('2013-07-05T01:23:22Z').toDate()
        Long missionId = 7

    then: "loaded entities can be queried by this mapper"
        mapper.load(MissionLogEntry, missionId, date)
        mapper.load(new MissionLogEntry(missionId: missionId, date: date))
        mapper.query(MissionLogEntry,
            new DynamoDBQueryExpression<MissionLogEntry>().withHashKeyValues(new MissionLogEntry(missionId: missionId))
        ).size() == 2

    and: "the can be also deleted using this mapper"
        mapper.delete(mapper.load(new MissionLogEntry(missionId: missionId, date: date)))
        mapper.query(MissionLogEntry,
            new DynamoDBQueryExpression<MissionLogEntry>().withHashKeyValues(new MissionLogEntry(missionId: missionId))
        ).size() == 1

    when: "new entities are saved using this mapper"
        Date now = new Date()
        mapper.save(new MissionLogEntry(missionId: 7, date: now))

    then: "they are available in the data set"
        dru.findAllByType(MissionLogEntry).find { it.missionId == 7 && it.date == now}

}

If you are using Grails AWS SDK DynamoDB Plugin you can inject such DynamoDBMapper into AbstractDBService to get instance of the service working against the data set.

Using DynamoDBMapper with Grails Plugin
void 'use grails service'() {
    when:
        MissionLogEntryDBService service = new MissionLogEntryDBService()
        service.mapper = DynamoDB.createMapper(dru)
    then:
        service.query(7).count == 2
}

The Dru’s implementation of DynamoDBMapper provides limited query and scan capabilities. You can query by hash keys and range keys and you can scan with filter. For additional more complex queries you need to implement your own logic using DruDynamoDBMapper callback onQuery and onScan.

Using Avanced Queries and Scans
void 'advanced dynamodb mapper'() {
    when: "DynamoDB mapper is created from data set"
        DruDynamoDBMapper mapper = DynamoDB.createMapper(dru)
        mapper.onQuery(MissionLogEntry) { MissionLogEntry entry, DynamoDBQueryExpression<MissionLogEntry> query, DynamoDBMapperConfig config ->
            return entry.agentId == 101
        }
    then:
        mapper.query(MissionLogEntry, buildCompexQuery()).size() == 2

}

You also so emulate failing batch using onBatchWrite method.

Failing Batch Items
void 'fail some writes'() {
    when:
        DruDynamoDBMapper mapper = DynamoDB.createMapper(dru)
        mapper.onBatchWrite { Iterable<MissionLogEntry> toSave, Iterable<MissionLogEntry> toDelete ->
            [new DynamoDBMapper.FailedBatch(exception: new AmazonClientException("Failed!"))]
        }
        List<DynamoDBMapper.FailedBatch> failed = mapper.batchSave(new MissionLogEntry(missionId: 7, date: new Date()))
    then:
        failed
        failed.size() == 1
        failed[0].exception instanceof AmazonClientException
}

7.3. GORM

GORM uses the Grails Object Relational Mapping to import entities into test in-memory storage. It automatically mocks all the entities involved so there is no need to call to mockDomains method.

GORM client is unable to set the id of the entities to the original value. The original value is replaced wherever it is obvious from the mapping to the actual generated id.

7.3.1. Unit Tests

Your unit tests must implement DataTest trait if you want to take advantage of using Dru with GORM.

Using GORM Client
@AutoCleanup Dru dru = Dru.create {
    from ('agents.json') {
        map {
            to (Agent) {
                map ('manager') {
                    to (Agent)
                }
            }
        }
    }

    any (Agent) {
        defaults { securityLevel = 1 }
    }
}

void 'entities can be accessed from data set and using GORM methods'() {
    expect:
        dru.findAllByType(Agent).size() == 2
        dru.findByTypeAndOriginalId(Agent, 12345).manager.name == 'Silas Ramsbottom'
    and:
        Agent.count() == 2
        Agent.findByName('Silas Ramsbottom').id != 12345
}
agents.json
[
  {
    "id": 12345,
    "name": "Felonius Gru",
    "bio": "Born from the family with long line of villainy and formerly the world's greatest villain.",
    "securityLevel": 2,
    "manager": {
      "id": 101,
      "name": "Silas Ramsbottom"
    }
  }
]
Agent.groovy
class Agent extends Person implements WithSecurityLevel {
    String name
    String bio

    Long securityLevel

    static hasOne = [manager: Agent]

    static constraints = {
        securityLevel nullable: false
        bio nullable: true
    }
}

7.3.2. Integration Tests

In you integration tests you no longer need to implement DataTest to get Dru working but dru.load() needs to be run from a scope which has Hibernate session attached, e.g. inside withNewSession closure.

Using GORM Client in Integration Test
@Rollback
@Integration                                                                            (1)
class GormIntegrationSpec extends Specification {

    @AutoCleanup Dru dru = Dru.create {
        from ('agents.json') {
            map {
                to (Agent) {
                    map ('manager') {
                        to (Agent)
                    }
                }
            }
        }

        any (Agent) {
            defaults { securityLevel = 1 }
        }
    }

    void setup() {
        Agent.withNewSession { dru.load() }                                             (2)
    }

    void 'entities can be accessed from data set and using GORM methods'() {
        expect:
            dru.findAllByType(Agent).size() == 2
            dru.findByTypeAndOriginalId(Agent, 12345).manager.name == 'Silas Ramsbottom'
        and:
            Agent.withNewSession { Agent.count() } == 2
            Agent.withNewSession { Agent.findByName('Silas Ramsbottom').id } != 12345
    }
}
1 Test no longer implment DataTest
2 Dru needs to load data within Hibernate session

7.4. Micronaut Data

Dru can help you set up Micronaut Data JPA and JDBC entities if you make your test class implementing io.micronaut.context.ApplicationContextProvider.

Using Micronaut Data Client
package dru.micronaut.example.jdbc

import com.agorapulse.dru.Dru
import io.micronaut.context.ApplicationContext
import io.micronaut.context.ApplicationContextProvider
import io.micronaut.test.annotation.MicronautTest
import spock.lang.AutoCleanup
import spock.lang.Specification

import javax.inject.Inject

@MicronautTest
class BookDataSpec extends Specification implements ApplicationContextProvider {        (1)

    private static final List<Map<String, Object>> BOOKS = [                            (2)
        [
            id    : 12345,
            title : 'It',
            pages : 1116,
            author: [id: 666],
        ],
        [
            id    : 12666,
            title : 'The Shining',
            pages : 659,
            author: [id: 666],
        ],
    ]

    @AutoCleanup Dru dru = Dru.create {                                                 (3)
        from 'BOOKS', {
            map {
                to Book
            }
        }
    }

    @Inject ApplicationContext applicationContext                                       (4)
    @Inject BookRepository bookRepository                                               (5)

    void setup() {
        dru.load()                                                                      (6)
    }

    void 'load books'() {
        expect:
            bookRepository.count() == 2                                                 (7)
    }

}
1 The specification class must implement ApplicationContextProvider
2 The inline data definition (could be also load from JSON or YAML using a particular parsers)
3 Mapping BOOK data to the Book entity
4 Injecting the ApplicationContext applicationContext field to satisfy the ApplicationContextProvider interface
5 Injecting the repository bean
6 Loading the data before each test
7 Verifying the data was loaded in the test method