显示导航

如何测试域类约束?

作者: Sergio del Amo

Grails 版本 5.0.1

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 域类

域类满足 Model View Controller (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 方法只会验证你传递的属性。域类可能具有许多属性。这允许你隔离测试属性验证。

Name 属性约束测试

我们希望将我们的name属性映射到具有 VARCHAR(255) 的关系数据库中。我们希望此属性为必需属性,最大长度为 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'])
}

Url 属性约束测试

我们希望将我们的url属性映射到具有 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 属性约束测试

我们希望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 个字符

Email 唯一约束测试

我们已经向酒店的电子邮箱地址添加了一个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'])
}

纬度和经度属性约束测试

我们希望 latitude 为 null 或 -90 至 90 度之间的值。我们希望 longitude 为 null 或 -180 至 180 度之间的值。

你可能想使用

latitude nullable: true, range: -90..90
longitude nullable: true, range: -180..180

但是,使用 范围约束,例如 latitude 为 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
}

以下是验证 latitudelongitude 约束的测试。

/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