显示导航

Grails 使用 Slf4j 测试模拟日志记录

在此指南中,我们将了解如何在 Grails 中模拟和验证日志语句。

作者: Nirav Assar

Grails 版本 4.0.1

1 Grails 培训

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

2 入门

在此指南中,你将学习如何通过使用 Slf4j 测试 库在 Grails 应用程序中模拟日志记录和验证日志事件。

2.1 你需要什么

要完成本指南,你需要以下内容

  • 一些时间

  • 一个不错的文本编辑器或 IDE

  • 安装了 JDK 1.8 或更高版本,并适当地配置了 JAVA_HOME

2.2 如何完成指南

要开始学习,请执行以下操作

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

  • 初始 初始项目。通常是具有附加代码以让你快速上手的简单 Grails 应用程序。

  • 完成 一个完整的示例。完成指南所述步骤并对 初始 文件夹应用这些更改的结果。

要完成本指南,请转到 初始 文件夹

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

并且按照下一部分中的说明进行操作。

cdgrails-guides/grails-mock-logging-slf4j-test/complete 中,你就可以直接进入到已完成的示例

3 编写应用程序

我们准备编写一个简单的带有日志的应用程序。为了简单起见,我们将创建一个包含 nameagePerson 域对象。该应用程序会根据年龄为该人员记录“友好”建议。通过此虚构的示例,我们将开发测试,以验证是否会发生正确的日志事件。

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

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

3.1 Person 域和数据服务

我们将使用属性 nameage 创建一个 Person 域类。

> 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 数据服务,它会自动实现一个界面来提供数据访问逻辑。此界面可以注入到另一个 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 中,以保存一个人员。在成功保存后,记录一条消息。在保存失败时,捕获异常并记录一条错误消息。我们已将 static scaffold = Person 添加到控制器中,以便快速提供 UI。

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 中实现另一个方法,该方法将根据传递的名称提供建议。此方法会首先按名称查找一个人,然后使用名为 AgeAdvisor 的 POGO(纯 Groovy 对象)记录“友好”建议。

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 需要此注解,因为只有 Grails Framework 注入的 Grails 制品才有记录器。

4 使用 Slf4j 测试模拟日志

Slf4j Test 是 Slf4j 的一个测试实现,它会将日志消息存储在内存中,并提供方法来检索这些消息以便进行验证。它基本上是替代实现,并且应该是测试类路径中的唯一实现。要实现此目的,请声明依赖项,还应将 Logback 依赖项从 testCompile 构建阶段中排除。

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

此外,我们将使用 HttpClient 进行测试。在下面添加此依赖项。

build.gradle
dependencies {
    ...
    testCompile "io.micronaut:micronaut-http-client"
}

4.1 验证控制器中的日志

我们可以用 Slf4j Test 验证日志事件发生在带有 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.testing.mixin.integration.Integration
import grails.testing.spock.OnceBefore
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
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
    HttpClient client

    @OnceBefore
    void init() {
        String baseUrl = "http://localhost:$serverPort"
        this.client  = HttpClient.create(baseUrl.toURL())
    }

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

    void "test create person successful logs"() {
        when:"a person is created"
        HttpResponse<Map> resp = client.toBlocking().exchange(HttpRequest.GET("/person/createPerson?name=Nirav&age=40"), Map)
        ImmutableList<LoggingEvent> loggingEvents = personControllerLogger.getAllLoggingEvents() (3)

        then: "check the logging events"
        resp.status == HttpStatus.OK
        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"
        HttpResponse<Map> resp = client.toBlocking().exchange(HttpRequest.GET("/person/createPerson?name=Bob&age=Twenty"), Map)
        ImmutableList<LoggingEvent> loggingEvents = personControllerLogger.getAllLoggingEvents()

        then: "check the logging events"
        resp.status == HttpStatus.OK
        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"
        HttpResponse<Map> resp = client.toBlocking().exchange(HttpRequest.GET("/person/createPerson?name=John&age=35"), Map)
        TestLogger ageAdvisorLogger = TestLoggerFactory.getTestLogger("example.grails.AgeAdvisor") (1)

        when:"we ask for advice"
        resp = client.toBlocking().exchange(HttpRequest.GET("/person/offerAdvice?name=John"), Map)
        ImmutableList<LoggingEvent> loggingEvents = ageAdvisorLogger.getAllLoggingEvents()

        then: "check the logging events"
        resp.status == HttpStatus.OK
        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 Test 以相同的方式处理 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 之家

了解团队