单元测试(超过?)扩展使用服务的基础 class 的 Grails 域
Unit testing Grails domains that (over?) extend a base class that uses services
所以,我有一个基础 class,我想从中扩展大部分(但不是全部)我的域 classes。目标是我可以使用简单的 extends
将我需要的六个审计信息列添加到任何域 class。对于创建和更新,我想记录日期、用户和程序(基于请求 URI)。它使用 Grails 服务(称为 CasService)来查找当前登录的用户。 CasService 然后使用 Spring 安全和数据库调用来获取该字段的相关用户信息。
问题是,如果我这样做,那么我将不得不在测试使用这些 classes 的域的任何单元测试中模拟 CasService 和请求对象。这也会影响使用这些域的服务和控制器的单元测试。这将使单元测试变得有点痛苦,并增加样板代码,这是我试图避免的。
我正在寻找更好的设计选项,我愿意接受建议。我当前的默认设置是简单地将相同的样板添加到我的所有域 classes 并完成它。请参阅下面的源代码和我到目前为止所做的尝试。
来源
公共审计域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
}
}
//...
}
我试过的
创建测试实用程序Class 以执行设置逻辑
我试过像这样构建 class:
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"}
}
}
然后在我的单元测试的 setup() 和 setupSpec() 块中调用它:
def setupSpec() {
def au = new AuditTestUtils()
au.setup()
}
或
def setupSpec() {
def au = new AuditTestUtils()
au.manipulateClass(TheDomainIAmTesting)
}
没有骰子。一旦我尝试保存扩展 AuditDomain 的域 class,就会在 CasService 上出现 NullPointerException 错误。
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)
我愿意接受其他方法来解决从我的域中删除审计信息的问题。我选择了 ,但还有其他方法。 Traits 在我的环境 (Grails 2.3.6) 中不可用,但也许这只是努力更新到最新版本的一个原因。
我也乐于接受有关如何以不同方式测试这些域的建议。也许我 应该 必须应对每个域 class 的单元测试中的审计列,尽管我宁愿不这样做。我可以在集成测试中处理这个问题,但我可以很容易地自行对 AuditDomain
class 进行单元测试。我更希望对我的域进行单元测试来测试这些域带给 table 的特定内容,而不是它们都具有的常见内容。
所以,最终,我已经 Groovy 委派来满足这一需求。
验证逻辑都存在于
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'
}
}
我创建了这个摘要 class 以在尝试各种解决方案时保留属性。它可以毫无问题地折叠到验证器 class 中,但我只是懒得把它留在那儿,因为它正在工作。
abstract class AuditDomainProperties {
String creator
String creatorProgram
String lastUpdater
String lastUpdateProgram
Date dateCreated
Date lastUpdated
}
最后,这里是如何在 Grails 域 class 中实现验证器 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'
}
}
这种方法似乎不能完美地用于单元和集成测试。尝试访问其中任何一个中的 dateCreated 列都会失败,并出现错误,即所讨论的域 class 没有这样的 属性。这很奇怪,因为 运行 该应用程序工作正常。对于单元测试,我认为这是一个模拟问题,但我不认为这会成为集成测试中的问题。
所以,我有一个基础 class,我想从中扩展大部分(但不是全部)我的域 classes。目标是我可以使用简单的 extends
将我需要的六个审计信息列添加到任何域 class。对于创建和更新,我想记录日期、用户和程序(基于请求 URI)。它使用 Grails 服务(称为 CasService)来查找当前登录的用户。 CasService 然后使用 Spring 安全和数据库调用来获取该字段的相关用户信息。
问题是,如果我这样做,那么我将不得不在测试使用这些 classes 的域的任何单元测试中模拟 CasService 和请求对象。这也会影响使用这些域的服务和控制器的单元测试。这将使单元测试变得有点痛苦,并增加样板代码,这是我试图避免的。
我正在寻找更好的设计选项,我愿意接受建议。我当前的默认设置是简单地将相同的样板添加到我的所有域 classes 并完成它。请参阅下面的源代码和我到目前为止所做的尝试。
来源
公共审计域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
}
}
//...
}
我试过的
创建测试实用程序Class 以执行设置逻辑
我试过像这样构建 class:
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"}
}
}
然后在我的单元测试的 setup() 和 setupSpec() 块中调用它:
def setupSpec() {
def au = new AuditTestUtils()
au.setup()
}
或
def setupSpec() {
def au = new AuditTestUtils()
au.manipulateClass(TheDomainIAmTesting)
}
没有骰子。一旦我尝试保存扩展 AuditDomain 的域 class,就会在 CasService 上出现 NullPointerException 错误。
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)
我愿意接受其他方法来解决从我的域中删除审计信息的问题。我选择了
我也乐于接受有关如何以不同方式测试这些域的建议。也许我 应该 必须应对每个域 class 的单元测试中的审计列,尽管我宁愿不这样做。我可以在集成测试中处理这个问题,但我可以很容易地自行对 AuditDomain
class 进行单元测试。我更希望对我的域进行单元测试来测试这些域带给 table 的特定内容,而不是它们都具有的常见内容。
所以,最终,我已经 Groovy 委派来满足这一需求。
验证逻辑都存在于
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'
}
}
我创建了这个摘要 class 以在尝试各种解决方案时保留属性。它可以毫无问题地折叠到验证器 class 中,但我只是懒得把它留在那儿,因为它正在工作。
abstract class AuditDomainProperties {
String creator
String creatorProgram
String lastUpdater
String lastUpdateProgram
Date dateCreated
Date lastUpdated
}
最后,这里是如何在 Grails 域 class 中实现验证器 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'
}
}
这种方法似乎不能完美地用于单元和集成测试。尝试访问其中任何一个中的 dateCreated 列都会失败,并出现错误,即所讨论的域 class 没有这样的 属性。这很奇怪,因为 运行 该应用程序工作正常。对于单元测试,我认为这是一个模拟问题,但我不认为这会成为集成测试中的问题。