Grails 域标准验证器:我应该测试还是不测试?

Posted

技术标签:

【中文标题】Grails 域标准验证器:我应该测试还是不测试?【英文标题】:Grails domain standard validators: should I test or not? 【发布时间】:2013-10-23 19:44:58 【问题描述】:

这是一个 grails 应用程序中的简单域类:

class User 
    String username

    static constraints = 
        username unique: true
    

我的问题是:我应该编写单元测试来检查用户名字段是否唯一吗?

@Test
void cannotCreateMoreThanOneUserWithTheSameUsername() 
    new User(username: 'john').save()

    def secondUser = new User(username: 'john')
    assert !secondUser.validate()

我很怀疑,因为:

如果我按照 TDD 原则编写 User 类,那么我应该在实现约束关闭之前编写失败测试。

另一方面,在域中设置唯一约束与其说是真正的逻辑,不如说是一种数据模型配置。更重要的是,save 和 validate 方法都是在框架中实现的。

【问题讨论】:

【参考方案1】:

在我看来,单元测试 CRUD 方法不值得花时间,因为 Grails 开发人员已经对这些方法进行了全面测试。另一方面,单元测试约束很重要,因为约束可能会在应用程序的生命周期中发生变化,并且您希望确保能够捕捉到这些变化。您永远不知道可能需要修改哪些业务逻辑来支持上述更改。我喜欢为此使用 Spock,典型的约束测试看起来像这样:

@TestFor(User)
class UserSpec extends ConstraintUnitSpec 

  def setup() 
    mockForConstraintsTests(User, [new User(username: 'username', emailAddress: 'email@email.com')])
  

  @Unroll("test user all constraints #field is #error")
  def "test user all constraints"() 
    when:
    def obj = new User("$field": val)

    then:
    validateConstraints(obj, field, error)

    where:
    error      | field                 | val
    'blank'    | 'username'            | ' '
    'nullable' | 'username'            | null
    'unique'   | 'username'            | 'username'
    'blank'    | 'password'            | ' '
    'nullable' | 'password'            | null
    'maxSize'  | 'password'            | getLongString(65)
    'email'    | 'emailAddress'        | getEmail(false)
    'unique'   | 'emailAddress'        | 'email@email.com'
    'blank'    | 'firstName'           | ' '
    'nullable' | 'firstName'           | null
    'maxSize'  | 'firstName'           | getLongString(51)
    'blank'    | 'lastName'            | ' '
    'nullable' | 'lastName'            | null
    'maxSize'  | 'lastName'            | getLongString(151)
    'nullable' | 'certificationStatus' | null
  

这是 ConstraintUnitSpec 基类:

abstract class ConstraintUnitSpec extends Specification 

  String getLongString(Integer length) 
    'a' * length
  

  String getEmail(Boolean valid) 
    valid ? "test@wbr.com" : "test@w"
  

  String getUrl(Boolean valid) 
    valid ? "http://www.google.com" : "http:/ww.helloworld.com"
  

  String getCreditCard(Boolean valid) 
    valid ? "4111111111111111" : "41014"
  

  void validateConstraints(obj, field, error) 


    def validated = obj.validate()

    if (error && error != 'valid') 
      assert !validated
      assert obj.errors[field]
      assert error == obj.errors[field]
     else 
      assert !obj.errors[field]
    
  

这是我从博客文章中学到的一种技术。但我现在想不起来了。我会寻找它,如果我找到它,我会确定并链接到它。

【讨论】:

感谢您的有趣回复。我没有询问 CRUD 测试。我询问了验证器设置测试。这样的测试不是重复实现吗?我的意思是,如果您忘记更改代码中的某些内容(让我们说空白:false),您也可能忘记更新您的测试。有什么区别? 如果您使用我描述的方法,并且您忘记添加约束,测试将失败。如果验证失败,上面的测试模式实际上会通过。那是关键。您正在测试以确保您的域将触发约束(验证)错误。 @Gregg 你是从这个博客上得到它的吗? christianoestreich.com/2012/11/… @corky_bantam 很有可能。【参考方案2】:

我会将您的测试工作集中在可能出错的区域,而不是试图获得 100% 的覆盖率。

考虑到这一点,我避免测试任何简单的声明。你没有逻辑可以打破,任何测试都只是重复声明。很难看出这将如何使您免于意外破坏此功能。

如果您正在编写处理声明的底层库,那么您应该编写测试。如果没有,请依赖库。当然,如果您不相信库的作者能做到这一点,那么您可以编写测试。这里需要权衡测试工作与奖励。

【讨论】:

这是关于 SO 这类问题的问题。 OP 不想测试约束。所以他选择了能让他安心的答案。没有真正正确的错误答案,只是说测试越多越好。 @Gregg 如果您完全测试了所有内容,我想您的错误会更少。但代价是什么?因此,最终它总是会归结为只有 OP 才能决定的权衡,除非我们能够准确地量化一大堆业务级别的结果,例如上市时间、声誉、错误成本等。跨度> @Gregg 问题在于测试代码的维护(我在几个项目中测试了几乎所有的约束)。问题是关于哲学/理念和最佳实践。【参考方案3】:

经过更多研究,我想分享下一个针对同一用户类的测试样本,并最终回答我自己的问题。

@Test
void usernameIsUnique() 
    def mock = new ConstraintsMock()
    User.constraints.delegate = mock
    User.constraints.call()
    assert mock.recordedUsernameUniqueConstraint == true


class ConstraintsMock 
    Boolean recordedUsernameUniqueConstraint = null

    def username =  Map m ->
        recordedUsernameUniqueConstraint = m.unique
        assert m.unique
    

这是一个非常幼稚的测试样本。与其说是一种行为,不如说是对实现的测试,在我看来这是不好的。 但它真的与问题中的测试样本不同吗?

第一件事:我们要测试什么逻辑?约束闭包的真正逻辑是什么?它只是为我们要配置的每个字段调用 gorm 的动态方法,并将配置作为参数传递。 那么为什么不在测试中调用这个闭包呢?为什么我要调用 save 方法?为什么我要调用gorm 的validate 方法?从这个角度来看,在单元测试中直接调用约束闭包似乎不是什么坏主意。

另一方面,Config.groovy 中的约束闭包和配置闭包有什么区别?我们不测试配置,是吗? 我认为我们不测试配置,因为配置测试就像此配置的副本(重复我们自己)。 更重要的是,如果今天有人仍然关心这个指标,这种测试甚至不会增加代码覆盖率,因为第一次运行集成或功能测试应该运行所有域的所有约束。

最后一件事:这个测试在现实生活中能够捕捉到什么样的错误?

总结一下:在我看来设置“空白”、“可空”或唯一性等简单约束与应用程序配置非常相似。 我们不应该测试这部分代码,因为如果这样的测试不仅仅是我们的约束定义的抄本,它可能只检查框架的逻辑。

我为约束编写了许多单元测试。现在我在重构期间将它们删除。我将只留下我自己的验证器逻辑的单元测试。

【讨论】:

以上是关于Grails 域标准验证器:我应该测试还是不测试?的主要内容,如果未能解决你的问题,请参考以下文章

在 Intellij Idea 中运行 grails 2.1.3 测试:Spock 测试中出现奇怪错误:无法添加域类 [class x.y.Z]。它不是域

覆盖 dateCreated 以在 Grails 中进行测试

Grails 域:允许字符串为空,但不允许为空

如何使用 @Build 为 grails 集成测试创建多种类型的测试数据

grails - 找不到域类

应该对原始数据还是拆分数据执行交叉验证分数?