这是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类,那么我应该在实现约束闭包之前编写失败的测试。
另一方面,在域中设置唯一约束是数据模型配置而不是真实逻辑。而且,保存和验证方法在框架中实现。
答案 0 :(得分: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]
}
}
}
这是我从博客文章中学到的一种技巧。但我现在不记得了。我会寻找它,如果我找到它,我会确定并链接到它。
答案 1 :(得分:1)
我会将您的测试工作集中在可能出错的领域,而不是试图获得100%的覆盖率。
考虑到这一点,我避免测试任何简单声明的东西。没有任何逻辑可以打破,任何测试只会重复声明。很难看出这会如何避免您意外地破坏此功能。
如果您正在编写处理声明的底层库,那么您应该进行写入测试。如果没有,依靠图书馆。当然,如果您不相信图书馆作者能够做到这一点,那么您可以编写测试。这里有一个关于测试努力与奖励的权衡。
答案 2 :(得分:0)
经过一些研究后,我想分享同一个用户类的下一个测试样本,最后回答我自己的问题。
@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的验证方法?从这个角度来看,在单元测试中直接调用约束闭包似乎并不是那么糟糕。
另一方面,Config.groovy中约束闭包和配置闭包有什么区别?我们不测试配置,对吗? 我认为我们不测试配置,因为配置测试就像这个配置的副本(重复我们自己)。 更重要的是,如果今天有人仍然关心这个指标,这种测试甚至不会增加代码覆盖率,因为首次运行集成或功能测试应该运行所有域的所有约束。
最后一件事:这个测试能够在现实生活中捕捉到什么样的错误?
总结一下:在我看来设置像“空白”,“可空”或“唯一”这样的简单约束与应用程序配置非常相似。 我们不应该测试这部分代码,因为如果这样的测试不仅仅是我们约束定义的副本,那么它只能检查框架的逻辑。
我为约束编写了许多单元测试。现在我在重构过程中将它们删除了。我将只留下我自己的验证器逻辑的单元测试。