问题
So, I have a base class that I want to extend most (but not all) of my domain classes from. The goal is that I can add the six audit information columns I need to any domain class with a simple extends
. For both creation and updates, I want to log the date, user, and program (based on request URI). It's using a Grails service (called CasService) to find the currently logged on user. The CasService then uses Spring Security and a database call to get the relevant user information for that field.
The trouble is, if I do this, then I'm going to have to Mock the CasService and request object in any unit test that tests a domain that uses these classes. That will also impact unit tests for services and controllers that use these domains. That's going to make unit testing a bit of a pain, and increase boiler plate code, which is what I was trying to avoid.
I'm fishing for better design options, and I'm open to suggestion. My current default is to simply add the same boiler plate to all my domain classes and be done with it. See below for source code and what I've tried so far.
Source
Common Audit Domain Class
package com.mine.common
import grails.util.Holders
import org.springframework.web.context.request.RequestContextHolder
class AuditDomain implements GroovyInterceptable {
def casService = Holders.grailsApplication.mainContext.getBean('casService')
def request = RequestContextHolder?.getRequestAttributes()?.getRequest()
String creator
String creatorProgram
String lastUpdater
String lastUpdateProgram
Date dateCreated
Date lastUpdated
def beforeValidate() {
beforeInsert()
beforeUpdate()
}
def beforeInsert() {
if (this.creator == null) {
this.creator = casService?.getUser() ?: 'unknown'
}
if (this.creatorProgram == null) {
this.creatorProgram = request?.requestURI ?: 'unknown'
}
}
def beforeUpdate() {
this.lastUpdater = casService?.getUser() ?: 'unknown'
this.lastUpdateProgram = request?.requestURI ?: 'unknown'
}
static constraints = {
creator nullable:true, blank: true
lastUpdater nullable:true, blank: true
creatorProgram nullable:true, blank: true
lastUpdateProgram nullable:true, blank: true
}
}
CasService
package com.mine.common
import groovy.sql.Sql
class CasService {
def springSecurityService, sqlService, personService
def getUser() {
if (isLoggedIn()) {
def loginId = springSecurityService.authentication.name.toLowerCase()
def query = "select USER_UNIQUE_ID from some_table where USER_LOGIN = ?"
def parameters = [loginId]
return sqlService.call(query, parameters)
} else {
return null
}
}
def private isLoggedIn() {
if (springSecurityService.isLoggedIn()) {
return true
} else {
log.info "User is not logged in"
return false
}
}
//...
}
What I've Tried
Creating a Test Utilities Class to do the setup logic
I've tried building a class like this:
class AuditTestUtils {
def setup() {
println "Tell AuditDomain to sit down and shut up"
AuditDomain.metaClass.casService = null
AuditDomain.metaClass.request = null
AuditDomain.metaClass.beforeInsert = {}
AuditDomain.metaClass.beforeUpdate = {}
}
def manipulateClass(classToTest) {
classToTest.metaClass.beforeInsert = {println "Yo mama"}
classToTest.metaClass.beforeUpdate = {println "Yo mamak"}
}
}
And then calling it in my Unit Test's setup() and setupSpec() blocks:
def setupSpec() {
def au = new AuditTestUtils()
au.setup()
}
OR
def setupSpec() {
def au = new AuditTestUtils()
au.manipulateClass(TheDomainIAmTesting)
}
No dice. That errors out with a NullPointerException on the CasService as soon as I try to save the domain class that extends the AuditDomain.
java.lang.NullPointerException: Cannot invoke method isLoggedIn() on null object
at com.mine.common.CasService.isLoggedIn(CasService.groovy:127)
at com.mine.common.CasService.getPidm(CasService.groovy:9)
at com.mine.common.AuditDomain.beforeInsert(AuditDomain.groovy:26)
//...
at org.grails.datastore.gorm.GormInstanceApi.save(GormInstanceApi.groovy:161)
at com.mine.common.SomeDomainSpec.test creation(SomeDomainSpec:30)
I'm open to alternate ways of approaching the issue of DRYing the Audit Information out my Domains. I'd settled on inheritance, but there are other ways. Traits aren't available in my environment (Grails 2.3.6), but maybe that's just a reason to get cracking on updating to the latest version.
I'm also open to suggestions about how to test these domains differently. Maybe I should have to contend with the audit columns in the unit tests of every domain class that has them, though I'd rather not. I'm okay with dealing with that in integration tests, but I can unit test the AuditDomain
class easily enough on its own. I'd prefer that unit tests on my domains tested the specific things those domains bring to the table, not the common stuff that they all have.
回答1:
So, ultimately, I've gone with Groovy delegation to meet this need.
The validation logic all lives in
class AuditDomainValidator extends AuditDomainProperties {
def domainClassInstance
public AuditDomainValidator(dci) {
domainClassInstance = dci
}
def beforeValidate() {
def user = defaultUser()
def program = defaultProgram()
if (this.creator == null) {
this.creator = user
}
if (this.creatorProgram == null) {
this.creatorProgram = program
}
this.lastUpdater = user
this.lastUpdateProgram = program
}
private def defaultProgram() {
domainClassInstance.getClass().getCanonicalName()
}
private def defaultUser() {
domainClassInstance.casService?.getUser() ?: 'unknown'
}
}
I created this abstract class to hold the properties while trying various solutions. It could probably be folded into the validator class with no problems, but I'm just lazy enough to leave it in there since it's working.
abstract class AuditDomainProperties {
String creator
String creatorProgram
String lastUpdater
String lastUpdateProgram
Date dateCreated
Date lastUpdated
}
And finally, here's how to implement the validator class in a Grails domain class.
import my.company.CasService
import my.company.AuditDomainValidator
class MyClass {
def casService
@Delegate AuditDomainValidator adv = new AuditDomainValidator(this)
static transients = ['adv']
//...domain class code
static mapping = {
//..domain column mapping
creator column: 'CREATOR'
lastUpdater column: 'LAST_UPADTER'
creatorProgram column: 'CREATOR_PGM'
lastUpdateProgram column: 'LAST_UPDATE_PGM'
dateCreated column: 'DATE_CREATED'
lastUpdated column: 'LAST_UPDATED'
}
}
This approach doesn't work perfectly for Unit and Integration tests, it seems. Trying to access the dateCreated column in either fails with an error that there is no such property for the domain class in question. Thats odd, since running the application works fine. With the Unit tests, I would think it was a mocking issue, but I wouldn't expect that to be a problem in the integration tests.
来源:https://stackoverflow.com/questions/28048179/unit-testing-grails-domains-that-over-extend-a-base-class-that-uses-services