【问题标题】:Unit testing Grails domains that (over?) extend a base class that uses services单元测试 Grails 域(过度?)扩展了使用服务的基类
【发布时间】:2015-01-20 14:45:41
【问题描述】:

所以,我有一个基类,我想从中扩展我的大部分(但不是全部)域类。目标是我可以使用简单的extends 将我需要的六个审计信息列添加到任何域类。对于创建和更新,我想记录日期、用户和程序(基于请求 URI)。它使用 Grails 服务(称为 CasService)来查找当前登录的用户。 CasService 然后使用 Spring Security 和数据库调用来获取该字段的相关用户信息。

问题是,如果我这样做,那么我将不得不在任何测试使用这些类的域的单元测试中模拟 CasService 和请求对象。这也将影响使用这些域的服务和控制器的单元测试。这会让单元测试有点痛苦,并且会增加样板代码,这是我试图避免的。

我正在寻找更好的设计选择,我愿意接受建议。我当前的默认设置是简单地将相同的样板添加到我的所有域类并完成它。请参阅下面的源代码以及到目前为止我尝试过的内容。

来源

通用审计域类

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 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 的域类,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)

我愿意接受替代方法来解决将审计信息从我的域中干燥的问题。我选择了inheritance,但还有其他方法。 Traits 在我的环境中不可用(Grails 2.3.6),但也许这只是更新到最新版本的一个原因。

我也愿意接受有关如何以不同方式测试这些域的建议。也许我应该必须在每个拥有它们的域类的单元测试中处理审计列,尽管我宁愿不这样做。我可以在集成测试中处理这个问题,但我可以很容易地对 AuditDomain 类进行单元测试。我希望我的域上的单元测试测试这些域带来的具体内容,而不是它们都拥有的常见内容。

【问题讨论】:

    标签: unit-testing grails inheritance groovy grails-orm


    【解决方案1】:

    所以,我最终选择了 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'
        }
    }
    

    我创建了这个抽象类来保存属性,同时尝试各种解决方案。它可能可以毫无问题地折叠到验证器类中,但我只是懒得把它放在那里,因为它正在工作。

    abstract class AuditDomainProperties {
        String creator
        String creatorProgram
        String lastUpdater
        String lastUpdateProgram
    
        Date dateCreated
        Date lastUpdated
    }
    

    最后,这是在 Grails 域类中实现验证器类的方法。

    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 列都会失败,并出现错误,即所讨论的域类没有此类属性。这很奇怪,因为运行应用程序工作正常。对于单元测试,我认为这是一个模拟问题,但我不认为这是集成测试中的问题。

    【讨论】:

    • 附带说明,我发现您的 Domain 委托的 POGO 类也不实现 GroovyInterceptable 接口很重要。原因是,GroovyInterceptable 从 groovy.lang.Object 继承了 getProperty() 方法,这意味着当某些 Grails 标记(例如 g:select)尝试在您的域对象上使用 getProperty() 方法时,该方法调用将被委托到您的 POGO。这可能会导致一些奇怪的、不一致的问题,例如带有神秘 java.lang.Void 条目的 g:select 列表,您的域类的实例应该在其中。
    猜你喜欢
    • 2019-08-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-14
    相关资源
    最近更新 更多