如何测试领域类约束?
作者:Sergio del Amo
Grails 版本 3.3.8
1 Grails 培训
Grails 培训 - 由创建和积极维护 Grails 框架的人员开发并提供的!.
2 开始使用
在本指南中,您将学习如何为您的领域类定义约束并在隔离状态下测试这些约束。
2.1 需要准备的东西
要完成本指南,您需要准备以下内容
-
一些时间
-
合适的文本编辑器或 IDE
-
已安装 JDK 1.8 或更高版本并妥善配置了
JAVA_HOME
2.2 如何完成指南
若要开始,请执行以下操作
-
下载并解压源代码
或
Grails 指南存储库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用程序,其中包含一些其他代码,以便为您在开始时提供帮助。 -
complete
一个完整的示例。它是完成指南介绍的步骤并针对initial
文件夹应用这些更改后产生的结果。
要完成指南,请转到 initial
文件夹
-
cd
到grails-guides/grails-test-domain-class-constraints/initial
然后按照下一部分中的说明进行操作。
如果您 cd 到 grails-guides/grails-test-domain-class-constraints/complete ,您可以直接转到完整示例 |
3 编写应用程序
3.1 领域类
创建一个持久性实体来存储酒店实体。在 Grails 中处理持久性的最常见方法是使用Grails 领域类
领域类遵循模型视图控制器 (MVC) 模式中的 M,它表示一个持久性实体,该实体映射到一个底层数据库表中。在 Grails 中,一个领域是一个位于 grails-app/domain 目录中的类。
./grailsw create-domain-class Hotel
CONFIGURE SUCCESSFUL
| Created grails-app/domain/demo/Hotel.groovy
| Created src/test/groovy/demo/HotelSpec.groovy
Hotel
领域类是我们的数据模型。我们定义不同的属性来存储 Hotel
的特性。
package demo
@SuppressWarnings('DuplicateNumberLiteral')
class Hotel {
String name
String url
String email
String about
BigDecimal latitude
BigDecimal longitude
static constraints = {
name blank: false, maxSize: 255
url nullable: true, url: true, maxSize: 255
email nullable: true, email: true, unique: true
about nullable: true
// tag::latitudeCustomValidator[]
latitude nullable: true, validator: { val, obj, errors ->
if ( val == null ) {
return true
}
if (val < -90.0) {
errors.rejectValue('latitude', 'range.toosmall')
return false
}
if (val > 90.0) {
errors.rejectValue('latitude', 'range.toobig')
return false
}
true
}
// end::latitudeCustomValidator[]
longitude nullable: true, validator: { val, obj, errors ->
if ( val == null ) {
return true
}
if (val < -180.0) {
errors.rejectValue('longitude', 'range.toosmall')
return false
}
if (val > 180.0) {
errors.rejectValue('longitude', 'range.toobig')
return false
}
true
}
}
// tag::hotelMapping[]
static mapping = {
about type: 'text'
}
// end::hotelMapping[]
}
我们定义了多个验证约束
约束为 Grails 提供了声明式 DSL 来定义验证规则、模式生成和 CRUD 生成元数据。
Grails 内置了多个可随时使用的约束
如果需要,您可以定义自己的自定义验证器。 |
3.2 单元测试
我们想要单元测试我们的验证约束。
实施 DomainUnitTest
特性。它表明我们正在测试 Grails 制品;领域类。
class HotelSpec extends Specification implements DomainUnitTest<Hotel> {
这是本指南的关键内容
当您将字符串列表传递给 validate 方法时,validate 方法将仅验证您传递的属性。一个领域类可能具有多个属性。这样,您便可以在隔离中测试属性验证。
名称属性约束测试
我们想要使用 VARCHAR(255) 将我们的 name
属性映射到关系数据库中。我们希望此属性为必需,最大字符长度为 255。
void 'test name cannot be null'() {
when:
domain.name = null
then:
!domain.validate(['name'])
domain.errors['name'].code == 'nullable'
}
void 'test name cannot be blank'() {
when:
domain.name = ''
then:
!domain.validate(['name'])
}
void 'test name can have a maximum of 255 characters'() {
when: 'for a string of 256 characters'
String str = 'a' * 256
domain.name = str
then: 'name validation fails'
!domain.validate(['name'])
domain.errors['name'].code == 'maxSize.exceeded'
when: 'for a string of 256 characters'
str = 'a' * 255
domain.name = str
then: 'name validation passes'
domain.validate(['name'])
}
网址属性约束测试
我们想要使用 VARCHAR(255) 将我们的 url
属性映射到关系数据库中。我们希望此属性为有效的网址,或为空或空白值。
@Ignore
void 'test url can have a maximum of 255 characters'() {
when: 'for a string of 256 characters'
String urlprefifx = 'http://'
String urlsufifx = '.com'
String str = 'a' * (256 - (urlprefifx.size() + urlsufifx.size()))
str = urlprefifx + str + urlsufifx
domain.url = str
then: 'url validation fails'
!domain.validate(['url'])
domain.errors['url'].code == 'maxSize.exceeded'
when: 'for a string of 256 characters'
str = "${urlprefifx}${'a' * (255 - (urlprefifx.size() + urlsufifx.size()))}${urlsufifx}"
domain.url = str
then: 'url validation passes'
domain.validate(['url'])
}
@Unroll('Hotel.validate() with url: #value should have returned #expected with errorCode: #expectedErrorCode')
void "test url validation"() {
when:
domain.url = value
then:
expected == domain.validate(['url'])
domain.errors['url']?.code == expectedErrorCode
where:
value | expected | expectedErrorCode
null | true | null
'' | true | null
'http://hilton.com' | true | null
'hilton' | false | 'url.invalid'
}
电子邮件属性约束测试
我们希望 email
为有效的电子邮件,或为空或空白值。
@Unroll('Hotel.validate() with email: #value should have returned #expected with errorCode: #expectedErrorCode')
void "test email validation"() {
when:
domain.email = value
then:
expected == domain.validate(['email'])
domain.errors['email']?.code == expectedErrorCode
where:
value | expected | expectedErrorCode
null | true | null
'' | true | null
'[email protected]' | true | null
'hilton' | false | 'email.invalid'
}
电子邮件验证器确保它小于 255 个字符 |
电子邮件唯一约束测试
我们已向酒店电子邮件地址添加了 unique
约束。
唯一:它将属性约束为数据库级别唯一。唯一是一个持久的调用,并将查询数据库。
您可以使用此类测试测试唯一约束
package demo
import grails.test.hibernate.HibernateSpec
@SuppressWarnings('MethodName')
class HotelEmailUniqueConstraintSpec extends HibernateSpec {
List<Class> getDomainClasses() { [Hotel] }
def "hotel's email unique constraint"() {
when: 'You instantiate a hotel with name and an email address which has been never used before'
def hotel = new Hotel(name: 'Hotel Transilvania', email: '[email protected]')
then: 'hotel is valid instance'
hotel.validate()
and: 'we can save it, and we get back a not null GORM Entity'
hotel.save()
and: 'there is one additional Hotel'
Hotel.count() == old(Hotel.count()) + 1
when: 'instanting a different hotel with the same email address'
def hilton = new Hotel(name: 'Hilton Hotel', email: '[email protected]')
then: 'the hotel instance is not valid'
!hilton.validate(['email'])
and: 'unique error code is populated'
hilton.errors['email']?.code == 'unique'
and: 'trying to save fails too'
!hilton.save()
and: 'no hotel has been added'
Hotel.count() == old(Hotel.count())
}
}
关于属性约束测试
我们希望 about
为 null、空白或文本块。我们不希望 about
被约束为 255 个字符。我们在领域类中使用 mapping
块来指示此特性。
static mapping = {
about type: 'text'
}
以下是一些用来验证 about
约束的测试。
void 'test about can be null'() {
when:
domain.about = null
then:
domain.validate(['about'])
}
void 'test about can be blank'() {
when:
domain.about = ''
then:
domain.validate(['about'])
}
void 'test about can have a more than 255 characters'() {
when: 'for a string of 256 characters'
String str = 'a' * 256
domain.about = str
then: 'about validation passes'
domain.validate(['about'])
}
纬度和经度属性约束测试
我们希望纬度
为空或介于 -90° 到 90° 之间。我们希望经度
为空或介于 -180° 到 180° 之间。
你可能会尝试使用
latitude nullable: true, range: -90..90
longitude nullable: true, range: -180..180
但是,这样的纬度
值 90.1 将使用范围约束成为有效值
设置为一个 Groovy 范围,其中可包含 IntRange 形式的数字、日期或任何实现 Comparable 并提供用于导航的 next 和 previous 方法的对象。
相反,我们使用的是自定义验证。
latitude nullable: true, validator: { val, obj, errors ->
if ( val == null ) {
return true
}
if (val < -90.0) {
errors.rejectValue('latitude', 'range.toosmall')
return false
}
if (val > 90.0) {
errors.rejectValue('latitude', 'range.toobig')
return false
}
true
}
以下为验证纬度
和经度
约束的测试。
@Unroll('Hotel.validate() with latitude: #value should have returned #expected with errorCode: #expectedErrorCode')
void 'test latitude validation'() {
when:
domain.latitude = value
then:
expected == domain.validate(['latitude'])
domain.errors['latitude']?.code == expectedErrorCode
where:
value | expected | expectedErrorCode
null | true | null
0 | true | null
0.5 | true | null
90 | true | null
90.5 | false | 'range.toobig'
-90 | true | null
-180 | false | 'range.toosmall'
180 | false | 'range.toobig'
}
@Unroll('Hotel.longitude() with latitude: #value should have returned #expected with error code: #expectedErrorCode')
void 'test longitude validation'() {
when:
domain.longitude = value
then:
expected == domain.validate(['longitude'])
domain.errors['longitude']?.code == expectedErrorCode
where:
value | expected | expectedErrorCode
null | true | null
0 | true | null
90 | true | null
90.1 | true | null
-90 | true | null
-180 | true | null
180 | true | null
180.1 | false | 'range.toobig'
-180.1 | false | 'range.toosmall'
}
4 测试应用程序
若要运行测试
./grailsw
grails> test-app
grails> open test-report
或
./gradlew check
open build/reports/tests/index.html