显示导航

Grails Mock Logging 带 Slf4j Test

本指南中,我们将学习如何在 Grails 中模拟和验证日志语句。

作者: Nirav Assar

Grails 版本 3.3.5

1 Grails 培训

Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和提供的!。

2 开始

本指南中,您将通过使用 Slf4j Test 库,学习如何模拟日志记录和验证 Grails 应用程序中的日志事件。

2.1 需要具备的条件

要完成此指南,需要具备以下条件

  • 一些时间

  • 一个体面的文本编辑器或 IDE

  • 已安装 JDK 1.8 或更高版本,并已正确配置了 JAVA_HOME

2.2 指南完成方式

准备时,执行以下操作

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用程序,其中包含一些其他代码以方便您快速入门。

  • complete 一个完成的示例。这是完成指南所提供的步骤并对 initial 文件夹应用这些更改的结果。

要完成此指南,请转到 initial 文件夹

  • cd 进入 grails-guides/grails-mock-logging-slf4j-test/initial

并按照后续部分的说明进行操作。

如果您 cd 进入 grails-guides/grails-mock-logging-slf4j-test/complete,则可以直接转到已完成示例

3 编写应用程序

我们将编写一个简单的应用程序并附带日志记录。对于一个简单的概念,我们将创建一个Person域对象,里面包含一个nameage。应用程序会根据年龄记录针对人员的“友好”建议。通过这个刻意编造的示例,我们将开发测试,用于验证是否发生了正确的日志记录事件。

首先,让我们配置应用程序,以便只记录与练习相关的项目。在logback.groovy中,删除文件底部的以下行:root(ERROR, ['STDOUT']}。然后添加以下代码段。这将在example.grails包中激活记录程序。

grails-app/conf/logback.groovy
logger("example.grails", DEBUG, ['STDOUT'])

3.1 人员域和数据服务

我们将创建一个包含属性nameagePerson域类。

> grails create-domain-class Person

编辑类,使其最终如下所示

grails-app/domain/example/grails/Person.groovy
package example.grails

import groovy.transform.CompileStatic

@CompileStatic
class Person {
    String name
    Integer age

    String toString() {
        "name: $name, age: $age"
    }
}

为了创建和查找学生,我们将使用GORM Data Services,其会自动实现一个接口来提供数据访问逻辑。这可以注入到另一个 Grails 工件(例如,控制器)中。

grails-app/services/example/grails/PersonDataService.groovy
package example.grails

import grails.gorm.services.Service

@Service(Person)
interface PersonDataService {
    Person findPerson(String name)
    Person savePerson(String name, Integer age)
}

3.2 创建人员

我们实现了创建Person的功能。我们可以将PersonDataService注入到PersonController中,以保存人员。在成功保存后,记录一条消息。在保存失败时,捕获异常并记录一条错误消息。为了快速生成 UI,我们已将static scaffold = Person添加到控制器中。

grails-app/controllers/example/grails/PersonController.groovy
package example.grails

import grails.validation.ValidationException
import groovy.transform.CompileStatic

@CompileStatic
class PersonController {

    static scaffold = Person

    PersonDataService personDataService

    def createPerson(String name, Integer age) {
        try {
            Person person = personDataService.savePerson(name, age)
            log.info "person saved successfully: ${person}"
            respond person, view: 'show'
        } catch (ValidationException e) {
            log.error "Error occurred on save!"
            redirect action: "index"
        }
    }
}

3.3 为人员提供建议

PersonController中实现另一个方法,基于传入的名称提供建议。此方法首先会按名称查找人员,然后再使用 POGO(普通 Groovy 对象),称为AgeAdvisor,以记录“友好”建议。

grails-app/controllers/example/grails/PersonController.groovy
    def offerAdvice(String name) {
        AgeAdvisor ageAdvisor = new AgeAdvisor()

        Person person = personDataService.findPerson(name)
        if (person) {
            ageAdvisor.offerAgeAdvice(person.age)
        } else {
            log.error "No person by name ${name} found."
        }
        redirect action: "index"
    }

POGO 很简单,并使用 @Slf4j groovy 注解来建立记录程序。

src/main/groovy/example/grails/AgeAdvisor.groovy
package example.grails

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

@Slf4j (1)
@CompileStatic
class AgeAdvisor {

    void offerAgeAdvice(Integer age) {
        if (age in 0..<30 ) {
            log.info ("You are a young and vibrant!")
            log.info ("Live life to the fullest.")
        } else if (30 <= age) {
            log.warn ("It's all downhill from here, sorry.")
        }
    }
}
1 请注意@Slf4j注解——这是 POGO 所必需的,因为只有 Groovy 工件的记录程序才由 Grails 框架注入。

4使用 Slf4j Test 模拟日志记录

Slf4j Test是 Slf4j 的一种测试实现,可将日志信息存储在内存中并提供用于检索它们的方法,以便验证。它基本上是一种替代实现,并且应该是测试类路径上唯一的一种实现。为实现此目的,需要声明依赖项,并从 testCompile 构建阶段中排除 Logback 依赖项。

build.gradle
dependencies {
    ...
    testCompile 'uk.org.lidalia:slf4j-test:1.1.0'
}
build.gradle
configurations {
    testCompile.exclude group: 'ch.qos.logback', module: 'logback-classic'
}

此外,我们将使用RestBuilder进行测试。添加以下依赖项。

build.gradle
dependencies {
    ...
    testCompile "org.grails:grails-datastore-rest-client"
}

4.1 在控制器中验证日志

我们可以使用 Slf4j Test 验证控制器中是否发生了日志事件。我们将创建一个 Grails 集成测试,以便自动注入数据服务,而不用担心模拟。我们将使用RestBuilder通过 HTTP 与控制器方法进行交互。

对于每个测试,流程都非常简单

  1. 访问测试记录程序。

  2. 调用功能。

  3. 使用测试记录器检索事件。

  4. 根据情景验证事件。

src/integration-test/groovy/example/grails/PersonControllerIntSpec.groovy
package example.grails

import com.google.common.collect.ImmutableList
import grails.gorm.transactions.Rollback
import grails.plugins.rest.client.RestBuilder
import grails.plugins.rest.client.RestResponse
import grails.testing.mixin.integration.Integration
import spock.lang.Shared
import spock.lang.Specification
import uk.org.lidalia.slf4jext.Level
import uk.org.lidalia.slf4jtest.LoggingEvent
import uk.org.lidalia.slf4jtest.TestLogger
import uk.org.lidalia.slf4jtest.TestLoggerFactory

@Integration
@Rollback
class PersonControllerIntSpec extends Specification {

    @Shared
    TestLogger personControllerLogger = TestLoggerFactory.getTestLogger("example.grails.PersonController") (1)
    @Shared
    RestBuilder rest = new RestBuilder()

    def cleanup() {
        TestLoggerFactory.clearAll() (2)
    }

    void "test create person successful logs"() {
        when:"a person is created"
        RestResponse resp = rest.get("http://localhost:${serverPort}/person/createPerson?name=Nirav&age=40")
        ImmutableList<LoggingEvent> loggingEvents = personControllerLogger.getAllLoggingEvents() (3)

        then: "check the logging events"
        resp.status == 200
        loggingEvents.size() == 1 (4)
        loggingEvents[0].message == "person saved successfully: name: Nirav, age: 40" (5)
        loggingEvents[0].level == Level.INFO
    }

}
1 使用记录器名称从 TestLoggerFactory 中检索测试记录器。
2 清除测试之间的内存中记录事件。
3 检索记录事件以便验证。
4 断言记录事件的大小。
5 验证消息及其后的日志级别。

4.2 验证更多日志

添加两个测试以涵盖其他情景。

src/integration-test/groovy/example/grails/PersonControllerIntSpec.groovy
    void "test create person unsuccessful logs"() {
        when:"a person is created, but has a input error"
        RestResponse resp = rest.get("http://localhost:${serverPort}/person/createPerson?name=Bob&age=Twenty")
        ImmutableList<LoggingEvent> loggingEvents = personControllerLogger.getAllLoggingEvents()

        then: "check the logging events"
        resp.status == 200
        loggingEvents.size() == 1
        loggingEvents[0].message == "Error occurred on save!"
        loggingEvents[0].level == Level.ERROR
    }

    void "test offerAdvice to old person"() {
        given: "A person is already created"
        RestResponse resp = rest.get("http://localhost:${serverPort}/person/createPerson?name=John&age=35")
        TestLogger ageAdvisorLogger = TestLoggerFactory.getTestLogger("example.grails.AgeAdvisor") (1)

        when:"we ask for advice"
        resp = rest.get("http://localhost:${serverPort}/person/offerAdvice?name=John")
        ImmutableList<LoggingEvent> loggingEvents = ageAdvisorLogger.getAllLoggingEvents()

        then: "check the logging events"
        resp.status == 200
        loggingEvents.size() == 1
        loggingEvents[0].message == "It's all downhill from here, sorry."
        loggingEvents[0].level == Level.WARN
    }
1 注意,我们正在访问 example.grails.AgeAdvisor 的记录器。工厂能够访问任何记录器来验证活动。

4.3 在 Groovy POGO 中验证日志

Slf4j 测试以同样的方式处理 POGO 对象。在 AgeAdvisor 中,回想使用 @Slf4J 注解建立记录器。这不会改变验证日志事件的方法。

src/test/groovy/example/grails/AgeAdvisorSpec.groovy
package example.grails

import com.google.common.collect.ImmutableList
import example.grails.AgeAdvisor
import spock.lang.Shared
import spock.lang.Specification
import uk.org.lidalia.slf4jext.Level
import uk.org.lidalia.slf4jtest.LoggingEvent
import uk.org.lidalia.slf4jtest.TestLogger
import uk.org.lidalia.slf4jtest.TestLoggerFactory

class AgeAdvisorSpec extends Specification {

    @Shared
    AgeAdvisor ageAdvisor = new AgeAdvisor()
    @Shared
    TestLogger logger = TestLoggerFactory.getTestLogger("example.grails.AgeAdvisor") (1)

    def cleanup() {
        TestLoggerFactory.clear()
    }

    void "verify young age logs"() {

        when: "method is invoked"
        ageAdvisor.offerAgeAdvice(15)
        ImmutableList<LoggingEvent> loggingEvents = logger.getLoggingEvents()

        then: "check the logging events"
        loggingEvents.size() == 2 (2)
        loggingEvents[0].message == "You are a young and vibrant!"
        loggingEvents[0].level == Level.INFO
        loggingEvents[1].message == "Live life to the fullest."
        loggingEvents[1].level == Level.INFO
    }

    void "verify old age logs"() {

        when: "method is invoked"
        ageAdvisor.offerAgeAdvice(31)
        ImmutableList<LoggingEvent> loggingEvents = logger.getLoggingEvents()

        then: "check the logging events"
        loggingEvents.size() == 1
        loggingEvents[0].message == "It's all downhill from here, sorry."
        loggingEvents[0].level == Level.WARN
    }
}
1 在使用 @Slf4j 注解的 POGO 中,我们只需以相同的方式检索记录器。
2 请注意,我们能够验证多个日志事件。

5 运行应用程序

要运行应用程序,请使用 ./gradlew bootRun 命令,它将在 8080 端口上启动应用程序。

要运行测试,请使用 ./gradlew check

6 Grails 帮助

Object Computing, Inc. (OCI) 赞助了本指南的创建。提供各种咨询和支持服务。

OCI 是 Grails 的归宿

认识团队