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.
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.
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:
{
"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:
@AutoCleanup Dru dru = Dru.create {
from ('items.json') {
map ('mission.items') {
to Item
}
}
}
{
"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.
@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'
}
{
"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.
@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'
}
{
"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.
@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')
}
{
"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.
@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
}
{
"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.
@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
}
[
{
"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.
@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'
}
[
{
"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:
@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
.
@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
}
{
"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.
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.
@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.
@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
|
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.
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)
@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'
}
[
{
"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).
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)
@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'
}
- 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.
@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
}
{
"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.
Annotation | Effect |
---|---|
|
DynamoDB client is used for given class |
|
Property is used as hash key part of the id |
|
Property is used as hash range part of the id |
|
Property is ignored |
|
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.
@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)
}
[
{
"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
}
]
@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)
.
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.
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
.
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.
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.
@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
}
[
{
"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"
}
}
]
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.
@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
.
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 |