显示导航

如何测试领域类约束?

作者: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 文件夹

  • cdgrails-guides/grails-test-domain-class-constraints/initial

然后按照下一部分中的说明进行操作。

如果您 cdgrails-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 的特性。

/grails-app/domain/demo/Hotel.groovy
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 制品;领域类。

/src/test/groovy/demo/HotelSpec.groovy
class HotelSpec extends Specification implements DomainUnitTest<Hotel> {

这是本指南的关键内容

当您将字符串列表传递给 validate 方法时,validate 方法将仅验证您传递的属性。一个领域类可能具有多个属性。这样,您便可以在隔离中测试属性验证。

名称属性约束测试

我们想要使用 VARCHAR(255) 将我们的 name 属性映射到关系数据库中。我们希望此属性为必需,最大字符长度为 255。

/src/test/groovy/demo/HotelSpec.groovy
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 属性映射到关系数据库中。我们希望此属性为有效的网址,或为空或空白值。

/src/test/groovy/demo/HotelSpec.groovy
@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 为有效的电子邮件,或为空或空白值。

/src/test/groovy/demo/HotelSpec.groovy
@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 约束。

唯一:它将属性约束为数据库级别唯一。唯一是一个持久的调用,并将查询数据库。

您可以使用此类测试测试唯一约束

/src/test/groovy/demo/HotelEmailUniqueConstraintSpec.groovy
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 块来指示此特性。

/grails-app/domain/demo/Hotel.groovy
static mapping = {
    about type: 'text'
}

以下是一些用来验证 about 约束的测试。

/src/test/groovy/demo/HotelSpec.groovy
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 方法的对象。

相反,我们使用的是自定义验证

/grails-app/domain/demo/Hotel.groovy
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
}

以下为验证纬度经度约束的测试。

/src/test/groovy/demo/HotelSpec.groovy
@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