【问题标题】:Overriding dateCreated for testing in Grails覆盖 dateCreated 以在 Grails 中进行测试
【发布时间】:2011-08-09 15:49:53
【问题描述】:

有什么方法可以在不关闭自动时间戳的情况下覆盖我的域类中 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 没有被更改。

【问题讨论】:

    标签: grails save grails-orm


    【解决方案1】:

    我遇到了类似的问题,并且能够覆盖我的域的 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()
        }
    }
    

    【讨论】:

    • BuildTestData 插件对我没有帮助。即使我在build() 中手动指定dateCreated: someDate,它仍然设置新的日期()。
    【解决方案2】:

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

    【讨论】:

    • 请注意:sessionFactory 被注入到控制器和服务中,因此无需使用servletContext。对于 Grails 2,您可以使用 grailsApplication.mainContext,这使您的解决方案也可以与服务一起使用。
    【解决方案3】:

    我通过简单地设置字段来完成这项工作。诀窍是在首先保存域对象之后执行此操作。我假设 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

    【讨论】:

    • 这仍然适用于 Grails v4。太棒了!
    • 我在 Grails 4 中尝试过这个,但是在更新现有行的 dateCreated 时。这个技巧对我不起作用。所以我在 Groovy 中使用原生查询来更新使用 new Sql(sessionFactory.currentSession.connection()) sql.executeUpdate(myQuery) 的列
    【解决方案4】:

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

    以下是我在集成测试中必要时使用的一个小 sn-p:

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

    【讨论】:

    • 在 grails4 中对我不起作用。我只是得到“没有可用的 'org.grails.orm.hibernate.HibernateDatastore' 类型的合格 bean”
    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

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

      所以我只能建议您覆盖类中dateCreated 字段的设置器,并分配您自己的值。也许甚至元类访问也会起作用,比如

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

      【讨论】:

      • 我尝试了你的建议,不幸的是没有成功:/我用我想出的东西编辑了我的问题。
      • 如果将dateCreated 声明为getter/setter 组合会怎样? Youca 可以删除@,这是为了明确的字段引用。
      【解决方案7】:

      我无法让上述技术发挥作用,对 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 固定装置不适用于单元测试,尽管插件作者将来可能会添加单元测试支持。

      【讨论】:

      • BuildTestData 插件似乎也可以工作,并且非常适合单元测试。看我的回答!
      【解决方案8】:

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

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

      【讨论】:

      • 我使用的是 Grails 2.3.6,但这不起作用。这适用于某些特定版本的 Grails 吗?或者它不适用于集成范围内的测试?
      • 没有错误。什么都没有发生,日期也没有改变。
      • 如果你在事务边界之前读回值,不要忘记刷新会话。
      • 纯 SQL 是我们应得的
      【解决方案9】:

      从 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 目录中,可以在测试中调用,但是我把它放在实际测试中,因为我的应用程序中只有一个类需要它。

      【讨论】:

        【解决方案10】:

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

        static mapping = {
            cache true
            autoTimestamp false
        }
        

        【讨论】:

        • 该问题具体要求关闭自动时间戳。
        猜你喜欢
        • 2012-11-11
        • 2017-07-26
        • 2019-05-11
        • 2017-05-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多