Overriding dateCreated for testing in Grails
Asked Answered
H

10

22

Is there any way I can override the value of dateCreated field in my domain class without turning off auto timestamping?

I need to test controller and I have to provide specific domain objects with specific creation date but GORM seems to override values I provide.

Edit

My classes look like this:

class Message {

    String content
    String title
    User author

    Date dateCreated
    Date lastUpdated

    static hasMany = [comments : Comment]

    static constraints = {
        content blank: false
        author nullable: false
        title nullable: false, blank: false
    }

    static mapping = {
        tablePerHierarchy false
        tablePerSubclass true
        content type: "text"
        sort dateCreated: 'desc'
    }
}

class BlogMessage extends Message{

    static belongsTo = [blog : Blog]

    static constraints = {
        blog nullable: false
    }

}

I'm using console to shorten things up. The problem which I encountered with Victor's approach is, when I write:

Date someValidDate = new Date() - (20*365)

BlogMessage.metaClass.setDateCreated = {
            Date d ->            
            delegate.@dateCreated = someValidDate
}

I get following exception:

groovy.lang.MissingFieldException: No such field: dateCreated for class: pl.net.yuri.league.blog.BlogMessage

When I tried

Message.metaClass.setDateCreated = {
                Date d ->            
                delegate.@dateCreated = someValidDate
}

Script goes well, but unfortunately dateCreated is not being altered.

Harmonie answered 20/4, 2011 at 16:17 Comment(0)
P
9

I was having a similar issue, and was able to overwrite dateCreated for my domain (in a Quartz Job test, so no @TestFor annotation on the Spec, Grails 2.1.0) by

  • Using the BuildTestData plugin (which we use regularly anyway, it is fantastic)
  • Double-tapping the domain instance with save(flush:true)

For reference, my test:

import grails.buildtestdata.mixin.Build
import spock.lang.Specification
import groovy.time.TimeCategory

@Build([MyDomain])
class MyJobSpec extends Specification {

    MyJob job

    def setup() {
        job = new MyJob()
    }

    void "test execute fires my service"() {
        given: 'mock service'
            MyService myService = Mock()
            job.myService = myService

        and: 'the domains required to fire the job'
            Date fortyMinutesAgo
            use(TimeCategory) {
                fortyMinutesAgo = 40.minutes.ago
            }

            MyDomain myDomain = MyDomain.build(stringProperty: 'value')
            myDomain.save(flush: true) // save once, let it write dateCreated as it pleases
            myDomain.dateCreated = fortyMinutesAgo
            myDomain.save(flush: true) // on the double tap we can now persist dateCreated changes

        when: 'job is executed'
            job.execute()

        then: 'my service should be called'
            1 * myService.someMethod()
    }
}
Pedroza answered 24/10, 2014 at 19:37 Comment(1)
BuildTestData plugin hasn't helped for me. Even when I manually specify dateCreated: someDate in build() it still setting new Date().Kutzer
D
6

Getting a hold of the ClosureEventListener allows you to temporarily disable grails timestamping.

import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes
import org.codehaus.groovy.grails.commons.spring.GrailsWebApplicationContext
import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration
import org.codehaus.groovy.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor
import org.codehaus.groovy.grails.orm.hibernate.support.ClosureEventListener

class FluxCapacitorController {

    def backToFuture = {
        changeTimestamping(new Message(), false)
        Message m = new Message()
        m.dateCreated = new Date("11/5/1955")
        m.save(failOnError: true)
        changeTimestamping(new Message(), true)
    }

    private void changeTimestamping(Object domainObjectInstance, boolean shouldTimestamp) {
        GrailsWebApplicationContext applicationContext = servletContext.getAttribute(GrailsApplicationAttributes.APPLICATION_CONTEXT)
        GrailsAnnotationConfiguration configuration = applicationContext.getBean("&sessionFactory").configuration
        ClosureEventTriggeringInterceptor interceptor = configuration.getEventListeners().saveOrUpdateEventListeners[0]
        ClosureEventListener listener = interceptor.findEventListener(domainObjectInstance)
        listener.shouldTimestamp = shouldTimestamp
    }
}

There may be an easier way to get the applicationContext or Hibernate configuration but that worked for me when running the app. It does not work in an integration test, if anyone figures out how to do that let me know.

Update

For Grails 2 use eventTriggeringInterceptor

private void changeTimestamping(Object domainObjectInstance, boolean shouldTimestamp) {
    GrailsWebApplicationContext applicationContext = servletContext.getAttribute(GrailsApplicationAttributes.APPLICATION_CONTEXT)
    ClosureEventTriggeringInterceptor closureInterceptor = applicationContext.getBean("eventTriggeringInterceptor")
    HibernateDatastore datastore = closureInterceptor.datastores.values().iterator().next()
    EventTriggeringInterceptor interceptor = datastore.getEventTriggeringInterceptor()

    ClosureEventListener listener = interceptor.findEventListener(domainObjectInstance)
    listener.shouldTimestamp = shouldTimestamp
}
Dday answered 27/7, 2011 at 23:16 Comment(1)
Just a note: sessionFactory is injected in controllers and services, so there's no need to use servletContext. For the Grails 2 you can use grailsApplication.mainContext, this makes your solution work with services too.Doited
H
6

I got this working by simply setting the field. The trick was to do that after the domain object has been saved first. I assume that the dateCreated timestamp is set on save and not on object creation.

Something along these lines

class Message {
  String content
  Date dateCreated
}

// ... and in test class

def yesterday = new Date() - 1
def m = new Message( content: 'hello world' )
m.save( flush: true )
m.dateCreated = yesterday
m.save( flush: true )

Using Grails 2.3.6

Handwoven answered 21/3, 2015 at 15:8 Comment(2)
This still works for Grails v4. Excellent!Grindstone
I tried this in Grails 4 in but when updating dateCreated of an existing row. This trick did not work for me. So I used a native query in Groovy to update the column using new Sql(sessionFactory.currentSession.connection()) sql.executeUpdate(myQuery)Newsome
O
6

As of Grails 3 and GORM 6 you can tap into AutoTimestampEventListener to execute a Runnable that temporarily ignores all or select timestamps.

The following is a small snippet I use in my integration tests where this is necessary:

void executeWithoutTimestamps(Class domainClass, Closure closure){
    ApplicationContext applicationContext = Holders.findApplicationContext()
    HibernateDatastore mainBean = applicationContext.getBean(HibernateDatastore)
    AutoTimestampEventListener listener = mainBean.getAutoTimestampEventListener()

    listener.withoutTimestamps(domainClass, closure)
}

Then in your case you could do the following:

executeWithoutTimestamps(BlogMessage, {
    Date someValidDate = new Date() - (20*365)
    BlogMessage message = new BlogMessage()
    message.dateCreated = someValidDate
    message.save(flush: true)
})
Octavia answered 10/4, 2018 at 17:0 Comment(1)
Doesn't work for me in grails4. I just get "No qualifying bean of type 'org.grails.orm.hibernate.HibernateDatastore' available"Grindstone
P
2

I'm using something like this for an initial import/migration.

Taking gabe's post as a starter (which didn't work for me Grails 2.0), and looking at the old source code for ClosureEventTriggeringInterceptor in Grails 1.3.7, I came up with this:

class BootStrap {

    private void changeTimestamping(Object domainObjectInstance, boolean shouldTimestamp) {
        Mapping m = GrailsDomainBinder.getMapping(domainObjectInstance.getClass())
        m.autoTimestamp = shouldTimestamp
    }

    def init = { servletContext ->

        changeTimestamping(new Message(), false)

        def fooMessage = new Message()
        fooMessage.dateCreated = new Date("11/5/1955")
        fooMessage.lastUpdated = new Date()
        fooMessage.save(failOnError, true)

        changeTimestamping(new Message(), true)
    }
}
Pluto answered 27/1, 2012 at 17:27 Comment(0)
P
1

You can try to disable it by setting autoTimestamp = false in the domain class mapping. I doubt about global overriding because the value is taken directly from System.currentTimeMillis() (I'm looking at org.codehaus.groovy.grails.orm.hibernate.support.ClosureEventListener.java).

So I can only suggest that you override a setter for dateCreated field in your class, and assign your own value. Maybe even metaclass access will work, like

Date stubDateCreated
...
myDomainClass.metaClass.setDateCreated = 
    { Date d -> delegate.@dateCreated = stubDateCreated }
Plume answered 21/4, 2011 at 8:36 Comment(2)
I tried your proposal, unfortunately with no success :/ I edited my question with what I came up with.Harmonie
What if you declare dateCreated as a getter/setter combination? Youca can remove @, it's for explicit field reference.Plume
T
1

I couldn't get the above techniques to work, the call to GrailsDomainBinder.getMapping always returned null???

However...

You can use the fixtures plugin to set the dateCreated property on a domain instance

The initial loading will not do it...

fixture {
    // saves to db, but date is set as current date :(
    tryDate( SomeDomain, dateCreated: Date.parse( 'yyyy-MM-dd', '2011-12-25') )
}

but if you follow up with a post handler

post {
    // updates the date in the database :D
    tryDate.dateCreated = Date.parse( 'yyyy-MM-dd', '2011-12-01')
}

Relevant part of the fixtures docs here

AFAIK fixtures don't work for unit testing, although the plugin authors may add unit testing support in the future.

Tollgate answered 12/7, 2012 at 8:51 Comment(1)
BuildTestData plugin also seems to work, and is great for unit tests. See my answer!Pedroza
C
1

A simpler solution is to use a SQL query in your integration test to set it as you please after you initialize your object with the other values you want.

YourDomainClass.executeUpdate(
"""UPDATE YourDomainClass SET dateCreated = :date
WHERE yourColumn = :something""",
[date:yourDate, something: yourThing])
Credent answered 26/5, 2014 at 19:7 Comment(4)
I'm using Grails 2.3.6 and this does not work. Is this applicable to some specific versions of Grails? Or does it not work for tests in integration scope?Handwoven
no errors. Nothing just happens and date is not changed.Handwoven
If you're reading the value back in before the boundary of a transaction, don't forget to flush the session.Credent
Plain SQL is what we deserveKutzer
E
0

As of grails 2.5.1, getMapping() method of GrailsDomainBinder class is not static,non of the above method works as is. However, @Volt0's method works with minor tweaking. Since all of us are trying to do so to make our tests working, instead of placing it in BootStrap, I placed it in actual integration test. Here is my tweak to Volt0's method:

def disableAutoTimestamp(Class domainClass) {
    Mapping mapping = new GrailsDomainBinder().getMapping(domainClass)
    mapping.autoTimestamp = false
}

def enableAutoTimestamp(Class domainClass) {
    Mapping mapping = new GrailsDomainBinder().getMapping(domainClass)
    mapping.autoTimestamp = true
}

And simply call these methods in tests like

disableAutoTimestamp(Domain.class)
//Your DB calls
enableAutoTimestamp(Domain.class)

The above code can also be placed in src directory and can be called in tests however I placed this in actual test as there was only one class in my app where I needed this.

Electrotype answered 2/5, 2016 at 16:37 Comment(0)
S
-2

The easy solution is to add a mapping:

static mapping = {
    cache true
    autoTimestamp false
}
Sciential answered 6/5, 2012 at 4:17 Comment(1)
The question specifically requires not turning off auto timestamping.Guardado

© 2022 - 2024 — McMap. All rights reserved.