Переопределение датыСоздано для тестирования в Grails
Можно ли каким-либо образом переопределить значение поля 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
}
}
Я использую консоль, чтобы сократить все. Проблема, с которой я столкнулась с подходом Виктора, - это когда я пишу:
Date someValidDate = new Date() - (20*365)
BlogMessage.metaClass.setDateCreated = {
Date d ->
[email protected] = someValidDate
}
Я получаю следующее исключение:
groovy.lang.MissingFieldException: No such field: dateCreated for class: pl.net.yuri.league.blog.BlogMessage
Когда я попробовал
Message.metaClass.setDateCreated = {
Date d ->
[email protected] = someValidDate
}
Script идет хорошо, но, к сожалению, dateCreated
не изменяется.
Ответы
Ответ 1
Захват 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
У меня была аналогичная проблема, и мне удалось перезаписать dateCreated для моего домена (в тесте Quartz Job, поэтому аннотация @TestFor на Spec, Grails 2.1.0) на
- Использование плагина BuildTestData (который мы используем регулярно в любом случае, это фантастика)
- Двойное нажатие на экземпляр домена с сохранением (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()
}
}
Ответ 3
Я получил эту работу, просто установив поле. Хитрость заключалась в том, чтобы сделать это после того, как объект домена был сохранен первым. Я предполагаю, что timestamp 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
Ответ 4
Я использую что-то вроде этого для первоначального импорта/миграции.
Взяв gabe post в качестве стартера (который не работал у меня Grails 2.0) и, глядя на старый исходный код ClosureEventTriggeringInterceptor в Grails 1.3.7, я придумал следующее:
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)
}
}
Ответ 5
Вы можете отключить его, установив autoTimestamp = false
в сопоставлении класса домена. Я сомневаюсь в глобальном переопределении, потому что значение берется непосредственно из System.currentTimeMillis()
(я смотрю org.codehaus.groovy.grails.orm.hibernate.support. ClosureEventListener.java).
Поэтому я могу только предложить вам переопределить сеттер для поля dateCreated
в вашем классе и присвоить собственное значение. Возможно, даже доступ к метаклассам будет работать, например
Date stubDateCreated
...
myDomainClass.metaClass.setDateCreated =
{ Date d -> [email protected] = stubDateCreated }
Ответ 6
Я не мог заставить эти методы работать, вызов 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')
}
Соответствующая часть документов документации здесь
Приборы AFAIK не работают для модульного тестирования, хотя авторы плагинов могут добавить поддержку модульного тестирования в будущем.
Ответ 7
Более простым решением является использование SQL-запроса в вашем тесте интеграции, чтобы установить его, как вам будет угодно, после инициализации объекта другими вашими значениями.
YourDomainClass.executeUpdate(
"""UPDATE YourDomainClass SET dateCreated = :date
WHERE yourColumn = :something""",
[date:yourDate, something: yourThing])
Ответ 8
Как и в случае с grails 2.5.1, метод getMapping() класса GrailsDomainBinder не является статическим, а не выше описанный выше метод работает как есть. Однако метод @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
Простым решением является добавление отображения:
static mapping = {
cache true
autoTimestamp false
}