覆盖dateCreated以在Grails中进行测试

时间:2011-04-20 16:17:37

标签: grails save gorm

有没有办法在不关闭自动时间戳的情况下覆盖域类中dateCreated字段的值?

我需要测试控制器,我必须提供具有特定创建日期的特定域对象,但GORM似乎覆盖了我提供的值。

修改

我的课程如下:

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
    }

}

我正在使用控制台来缩短范围。当我写作时,我遇到的问题是Victor的方法:

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

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

我得到以下例外:

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

当我尝试

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

脚本运行良好,但不幸的是dateCreated没有被改变。

10 个答案:

答案 0 :(得分:7)

我遇到了类似的问题,并且能够覆盖我的域的dateCreated(在Quartz Job测试中,所以没有对Spec,Grails 2.1.0进行@TestFor注释)

  • 使用BuildTestData插件(无论如何我们经常使用它,这太棒了)
  • 使用save(flush:true)
  • 双击域实例

供参考,我的测试:

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()
    }
}

答案 1 :(得分:6)

获取ClosureEventListener可以暂时禁用grails时间戳。

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
    }
}

可能有一种更简单的方法来获取applicationContext或Hibernate配置,但在运行应用程序时这对我有用。它不适用于集成测试,如果有人知道如何做到这一点让我知道。

<强>更新

对于Grails 2,使用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
}

答案 2 :(得分:5)

我通过简单设置字段来完成这项工作。诀窍是在首先保存域对象后执行此操作。我假设dateCreated时间戳是在保存时设置的,而不是在创建对象时设置的。

沿着这些方向的东西

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 )

使用Grails 2.3.6

答案 3 :(得分:2)

我正在使用这样的东西进行初始导入/迁移。

将gabe的帖子作为启动器(对我来说不适用于Grails 2.0),并查看Grails 1.3.7中ClosureEventTriggeringInterceptor的旧源代码,我想出了这个:

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)
    }
}

答案 4 :(得分:2)

从Grails 3和GORM 6开始,您可以使用AutoTimestampEventListener来执行暂时忽略所有或选择时间戳的Runnable

以下是我在集成测试中使用的小片段,这是必要的:

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

    listener.withoutTimestamps(domainClass, closure)
}

然后在您的情况下,您可以执行以下操作:

executeWithoutTimestamps(BlogMessage, {
    Date someValidDate = new Date() - (20*365)
    BlogMessage message = new BlogMessage()
    message.dateCreated = someValidDate
    message.save(flush: true)
})

答案 5 :(得分:1)

您可以尝试通过在域类映射中设置autoTimestamp = false来禁用它。我怀疑全局覆盖是因为价值是直接来自System.currentTimeMillis()(我正在看org.codehaus.groovy.grails.orm.hibernate.support.ClosureEventListener.java)。

因此,我只能建议您在班级中覆盖dateCreated字段的setter,并指定自己的值。也许甚至元类访问都可以,比如

Date stubDateCreated
...
myDomainClass.metaClass.setDateCreated = 
    { Date d -> delegate.@dateCreated = stubDateCreated }

答案 6 :(得分:1)

我无法使用上述技术,对GrailsDomainBinder.getMapping的调用总是返回null ???

...然而

您可以使用fixtures插件在域实例

上设置dateCreated属性

初始加载不会这样做......

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

但如果您跟进帖子处理程序

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灯具不适用于单元测试,但插件作者可能会在未来添加单元测试支持。

答案 7 :(得分:1)

更简单的解决方案是在集成测试中使用SQL查询,在使用所需的其他值初始化对象后,根据需要设置它。

YourDomainClass.executeUpdate(
"""UPDATE YourDomainClass SET dateCreated = :date
WHERE yourColumn = :something""",
[date:yourDate, something: yourThing])

答案 8 :(得分:0)

从Grails 2.5.1开始,GrailsDomainBinder类的getMapping()方法不是静态的,非上述方法的工作原理。但是,@ Volt0的方法适用于小调整。由于我们所有人都试图这样做以使我们的测试工作,而不是将它放在BootStrap中,我把它放在实际的集成测试中。这是我对Volt0方法的调整:

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
}

只需在像

这样的测试中调用这些方法
disableAutoTimestamp(Domain.class)
//Your DB calls
enableAutoTimestamp(Domain.class)

上面的代码也可以放在src目录中,可以在测试中调用,但我把它放在实际测试中,因为我的应用程序中只有一个类需要这个。

答案 9 :(得分:-2)

简单的解决方案是添加映射:

static mapping = {
    cache true
    autoTimestamp false
}