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 如何完成指南
要开始学习,请执行以下操作
-
下载并解压缩源代码
或
-
克隆 Git 存储库
git clone https://github.com/grails-guides/grails-mock-logging-slf4j-test.git
Grails 指南存储库包含两个文件夹
-
初始
初始项目。通常是具有附加代码以让你快速上手的简单 Grails 应用程序。 -
完成
一个完整的示例。完成指南所述步骤并对初始
文件夹应用这些更改的结果。
要完成本指南,请转到 初始
文件夹
-
cd
进入grails-guides/grails-mock-logging-slf4j-test/initial
并且按照下一部分中的说明进行操作。
cd 到 grails-guides/grails-mock-logging-slf4j-test/complete 中,你就可以直接进入到已完成的示例 |
3 编写应用程序
我们准备编写一个简单的带有日志的应用程序。为了简单起见,我们将创建一个包含 name
和 age
的 Person
域对象。该应用程序会根据年龄为该人员记录“友好”建议。通过此虚构的示例,我们将开发测试,以验证是否会发生正确的日志事件。
首先,让我们配置应用程序,使其仅记录与练习相关的项目。在 logback.groovy
中,删除文件底部的行:root(ERROR, ['STDOUT'])
。然后添加下面的代码段。这会激活包 example.grails
中的记录器。
logger("example.grails", DEBUG, ['STDOUT'])
3.1 Person 域和数据服务
我们将使用属性 name
和 age
创建一个 Person
域类。
> grails create-domain-class Person
编辑类使其最终如下所示
package example.grails
import groovy.transform.CompileStatic
@CompileStatic
class Person {
String name
Integer age
String toString() {
"name: $name, age: $age"
}
}
为了创建和查找学生,我们将使用 GORM 数据服务,它会自动实现一个界面来提供数据访问逻辑。此界面可以注入到另一个 Grails 制品(例如控制器)中。
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。
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 对象)记录“友好”建议。
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 注解来建立记录器。
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 构建阶段中排除。
dependencies {
...
testCompile 'uk.org.lidalia:slf4j-test:1.1.0'
}
configurations {
testCompile.exclude group: 'ch.qos.logback', module: 'logback-classic'
}
此外,我们将使用 HttpClient
进行测试。在下面添加此依赖项。
dependencies {
...
testCompile "io.micronaut:micronaut-http-client"
}
4.1 验证控制器中的日志
我们可以用 Slf4j Test 验证日志事件发生在带有 Slf4j Test 的控制器中。我们将会创建一个 Grails 集成测试,使数据服务可以自动注入,而无需担心模拟。我们将使用 RestBuilder
通过 HTTP 与控制器方法进行交互。
每个测试的流程十分直接
-
访问测试日志记录器。
-
调用该功能。
-
使用测试日志记录器检索事件。
-
按照场景验证事件。
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 验证更多日志
添加另外两个测试,以涵盖其他场景。
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
注解建立。这不会改变验证日志事件的方法。
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
。