Mat*_*zcz 9 groovy unit-testing mocking slf4j spock
我有一个像这样的简单类:
package com.example.howtomocktest
import groovy.util.logging.Slf4j
import java.nio.channels.NotYetBoundException
@Slf4j
class ErrorLogger {
static void handleExceptions(Closure closure) {
try {
closure()
}catch (UnsupportedOperationException|NotYetBoundException ex) {
log.error ex.message
} catch (Exception ex) {
log.error 'Processing exception {}', ex
}
}
}
Run Code Online (Sandbox Code Playgroud)
我想为它写一个测试,这是一个骨架:
package com.example.howtomocktest
import org.slf4j.Logger
import spock.lang.Specification
import java.nio.channels.NotYetBoundException
import static com.example.howtomocktest.ErrorLogger.handleExceptions
class ErrorLoggerSpec extends Specification {
private static final UNSUPPORTED_EXCEPTION = { throw UnsupportedOperationException }
private static final NOT_YET_BOUND = { throw NotYetBoundException }
private static final STANDARD_EXCEPTION = { throw Exception }
private Logger logger = Mock(Logger.class)
def setup() {
}
def "Message logged when UnsupportedOperationException is thrown"() {
when:
handleExceptions {UNSUPPORTED_EXCEPTION}
then:
notThrown(UnsupportedOperationException)
1 * logger.error(_ as String) // doesn't work
}
def "Message logged when NotYetBoundException is thrown"() {
when:
handleExceptions {NOT_YET_BOUND}
then:
notThrown(NotYetBoundException)
1 * logger.error(_ as String) // doesn't work
}
def "Message about processing exception is logged when standard Exception is thrown"() {
when:
handleExceptions {STANDARD_EXCEPTION}
then:
notThrown(STANDARD_EXCEPTION)
1 * logger.error(_ as String) // doesn't work
}
}
Run Code Online (Sandbox Code Playgroud)
ErrorLogger类中的记录器由StaticLoggerBinder提供,所以我的问题是 - 如何让它工作以便那些检查"1*logger.error(_ as String)"可以工作?我找不到在ErrorLogger类中模拟该记录器的正确方法.我已经考虑过反射并以某种方式访问它,此外还有一个使用mockito注入的想法(但如果因为Slf4j注释而在该类中甚至不存在对象的引用,那该怎么办呢!)提前感谢您的所有反馈和建议.
编辑:这是一个测试的输出,甚至1*logger.error(_)不起作用.
Too few invocations for:
1*logger.error() (0 invocations)
Unmatched invocations (ordered by similarity):
Run Code Online (Sandbox Code Playgroud)
Ste*_*nar 21
您需要做的是用模拟替换AST转换log生成的字段@Slf4j.
但是,这并不容易实现,因为生成的代码实际上并不是测试友好的.
快速查看生成的代码会发现它对应于以下内容:
class ErrorLogger {
private final static transient org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(ErrorLogger)
}
Run Code Online (Sandbox Code Playgroud)
由于log声明字段,private final因此用模拟替换值并不容易.它实际上归结为与此处描述的完全相同的问题.此外,此字段的用法包含在isEnabled()方法中,因此例如每次调用log.error(msg)它时都会替换为:
if (log.isErrorEnabled()) {
log.error(msg)
}
Run Code Online (Sandbox Code Playgroud)
那么,如何解决这个问题呢?我建议您在groovy问题跟踪器中注册一个问题,在那里您要求更加测试友好的AST转换实现.但是,这对你现在没什么帮助.
您可以考虑使用几种解决方案.
getLog()向您的ErrorLogger类添加方法,并使用该方法进行访问,而不是直接字段访问.然后你可以操纵它metaClass来覆盖getLog()实现.这种方法的问题在于您必须修改生产代码并添加一个getter,这种方式首先违背了使用目的@Slf4j.我还想指出你ErrorLoggerSpec班上有几个问题.这些都是你已经遇到过的问题所隐藏的,所以当你们表现出来时,你可能会自己想出这些问题.
即使它是一个黑客,我只会提供第一个建议的代码示例,因为第二个建议修改了生产代码.
为了隔离hack,启用简单的重用并避免忘记重置值,我将其写成JUnit规则(也可以在Spock中使用).
import org.junit.rules.ExternalResource
import org.slf4j.Logger
import java.lang.reflect.Field
import java.lang.reflect.Modifier
public class ReplaceSlf4jLogger extends ExternalResource {
Field logField
Logger logger
Logger originalLogger
ReplaceSlf4jLogger(Class logClass, Logger logger) {
logField = logClass.getDeclaredField("log");
this.logger = logger
}
@Override
protected void before() throws Throwable {
logField.accessible = true
Field modifiersField = Field.getDeclaredField("modifiers")
modifiersField.accessible = true
modifiersField.setInt(logField, logField.getModifiers() & ~Modifier.FINAL)
originalLogger = (Logger) logField.get(null)
logField.set(null, logger)
}
@Override
protected void after() {
logField.set(null, originalLogger)
}
}
Run Code Online (Sandbox Code Playgroud)
在修复所有小错误并添加此规则之后,这是规范.代码中注释了更改:
import org.junit.Rule
import org.slf4j.Logger
import spock.lang.Specification
import java.nio.channels.NotYetBoundException
import static ErrorLogger.handleExceptions
class ErrorLoggerSpec extends Specification {
// NOTE: These three closures are changed to actually throw new instances of the exceptions
private static final UNSUPPORTED_EXCEPTION = { throw new UnsupportedOperationException() }
private static final NOT_YET_BOUND = { throw new NotYetBoundException() }
private static final STANDARD_EXCEPTION = { throw new Exception() }
private Logger logger = Mock(Logger.class)
@Rule ReplaceSlf4jLogger replaceSlf4jLogger = new ReplaceSlf4jLogger(ErrorLogger, logger)
def "Message logged when UnsupportedOperationException is thrown"() {
when:
handleExceptions UNSUPPORTED_EXCEPTION // Changed: used to be a closure within a closure!
then:
notThrown(UnsupportedOperationException)
1 * logger.isErrorEnabled() >> true // this call is added by the AST transformation
1 * logger.error(null) // no message is specified, results in a null message: _ as String does not match null
}
def "Message logged when NotYetBoundException is thrown"() {
when:
handleExceptions NOT_YET_BOUND // Changed: used to be a closure within a closure!
then:
notThrown(NotYetBoundException)
1 * logger.isErrorEnabled() >> true // this call is added by the AST transformation
1 * logger.error(null) // no message is specified, results in a null message: _ as String does not match null
}
def "Message about processing exception is logged when standard Exception is thrown"() {
when:
handleExceptions STANDARD_EXCEPTION // Changed: used to be a closure within a closure!
then:
notThrown(Exception) // Changed: you added the closure field instead of the class here
//1 * logger.isErrorEnabled() >> true // this call is NOT added by the AST transformation -- perhaps a bug?
1 * logger.error(_ as String, _ as Exception) // in this case, both a message and the exception is specified
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
4478 次 |
| 最近记录: |