单元测试(over?)扩展使用服务的基类的Grails域 [英] Unit testing Grails domains that (over?) extend a base class that uses services

查看:141
本文介绍了单元测试(over?)扩展使用服务的基类的Grails域的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



问题是,如果我这样做,那么我会去必须在测试使用这些类的域的单元测试中模拟CasService和请求对象。这也会影响使用这些域的服务和控制器的单元测试。这会让单元测试带来一点痛苦,并增加锅炉板代码,这正是我试图避免的。



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





通用审计域类



  package com.mine.common 

导入grails.util.Holders
导入组织。 springframework.web.context.request.RequestContextHolder
$ b $ class AuditDomain实现GroovyInterceptable {
$ b $ def casService = Holders.grailsApplication.mainContext.getBean('casService')
def request = RequestContextHolder?.getRequestAttributes()?getRequest()

字符串创建者
字符串creatorProgram
字符串lastUpdater
字符串lastUpdateProgram

日期dateCreated
Date lastUpdated

def beforeValidate(){
beforeInsert()
beforeUpdate()
}

def beforeInsert() ){
if(this.creator == null){
this.creator = casService?.getUser()?:'unknown'
}

i f(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可空:true,空白:true
creatorProgram可空:true,空白:true
lastUpdateProgram可空:true,




CasService



 包com.mine.common 

导入groovy.sql.Sql

类CasService {
def springSecurityService,sqlService,personService
$ b $ def getUser(){
if(isLoggedIn()){
def loginId = springSecurityService.authentication.name.toLowerCase()
def查询=从some_table中选择USER_UNIQUE_ID,其中USER_LOGIN =?
def parameters = [loginId]
return sqlService.call(query,parameters)
} else {
return null
}
}

def private isLoggedIn(){
if(springSecurityService.isLoggedIn()){
return true
} else {
log.info用户未登录
return false
}
}

// ...
}



我试过了什么



创建一个测试实用程序类来完成设置逻辑



我试过建立这样一个类:

  class AuditTestUtils {

def setup(){
println告诉AuditDomain坐下并闭嘴
AuditDomain.metaClass.casService = null
AuditDomain.metaClass.request = null
AuditDomain .metaClass.beforeInsert = {}
AuditDomain.metaClass.beforeUpdate = {}
}

def manipulateClass(classToTest){
classToTest.metaClass.beforeInsert = {printlnYo mama}
classToTest.metaClass.beforeUpdate = {printlnYo mamak}
}
}

然后在我的单元测试中调用它setup()和setupSpec()块:

  def setupSpec(){
def au = new AuditTestUtils()
au.setup()
}

OR

  def setupSpec(){
def au = new AuditTestUtils()
au.manipulateClass(TheDomainIAmTesting)
}

没有骰子。只要我尝试保存扩展AuditDomain的域类,就会在CasService上发生NullPointerException异常。

  java。 lang.NullPointerException:无法调用com.mine.common.CasService.isLoggedIn(CasService.groovy:127)上的空对象
上的方法isLoggedIn(); 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创建(SomeDomainSpec:30)

我可以采用其他方式处理将我的域名审核信息干扰的问题。我已经解决了继承,但还有其他方法。特征在我的环境中是不可用的(Grails 2.3.6),但也许这只是更新到最新版本的一个原因。



我也是接受有关如何以不同方式测试这些域的建议。也许我应该在每个具有它们的域类的单元测试中与审核列进行抗衡,尽管我宁愿不这样做。我可以在集成测试中处理这个问题,但我可以自己单独测试 AuditDomain 类。我更喜欢在我的域名上进行单元测试,测试了这些域名为表格带来的具体情况,而不是它们都具有的常见信息。

所以,最终,我已经和Groovy代表一起去满足这个需求。



验证逻辑全部存在于

  class AuditDomainValidator extends AuditDomainProperties {
$ b $ def domainClassInstance
$ b $ public AuditDomainValidator(dci){
domainClassInstance = $ d

$ b $ def beforeValidate(){
def user = defaultUser()
def program = defaultProgram()
if(this.creator = = null){
this.creator = user
}

if(this.creatorProgram == null){
this.creatorProgram = program
}
this.lastUpdater =用户
this.lastUpdateProgram =程序
}

私人def defau ltprogram(){
domainClassInstance.getClass()。getCanonicalName()
}

private def defaultUser(){
domainClassInstance.casService?.getUser()?: 'unknown'
}
}

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

 抽象类AuditDomainProperties {
字符串创建者
字符串creatorProgram
字符串lastUpdater
字符串lastUpdateProgram

日期dateCreated
last lastUpdated
}

最后,下面是如何在Grails域类中实现验证器类。

  import my.company.CasService 
import my.company.AuditDomainValidator
$ b $ class MyClass {
def casService

@Delegate AuditDomainValidator adv = new AuditDomainValidator(this)
static transients = ['adv']
//...domain class code

static mapping = {
// ..域列映射
创建者列:'CREATOR'
lastUpdater列:'LAST_UPADTER'
crea torProgram列:'CREATOR_PGM'
lastUpdateProgram列:'LAST_UPDATE_PGM'
dateCreated列:'DATE_CREATED'
lastUpdated列:'LAST_UPDATED'
}
}

看起来,这种方法对单元和集成测试来说并不完美。尝试访问dateCreated列中的任何一个都会失败,并且出现错误,表示该类域没有此类属性。这很奇怪,因为运行应用程序工作正常。通过单元测试,我认为这是一个模拟问题,但我不认为这是集成测试中的问题。


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.

解决方案

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.

这篇关于单元测试(over?)扩展使用服务的基类的Grails域的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆