2

这是 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 方法是在框架中实现的。

4

3 回答 3

6

在我看来,单元测试 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]
    }
  }
}

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

于 2013-10-15T21:03:46.997 回答
1

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

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

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

于 2013-10-16T08:50:14.590 回答
0

经过更多研究后,我想为同一个 User 类分享下一个测试样本,最后回答我自己的问题。

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

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

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

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

于 2013-10-18T21:57:53.253 回答