用Java构建一个可模仿的日志API,正确的方法

用Java构建一个可模仿的日志API,正确的方法,java,testing,junit,mocking,Java,Testing,Junit,Mocking,我一直在阅读编写可测试代码的相关知识,现在正试图通过重构我的日志框架API将其付诸实践。我主要关心的是,它应该1)易于从业务代码调用,2)易于模拟,这样就可以在不调用实际日志的情况下测试业务代码,并且可以断言是否已从所述测试中记录了内容 我已经到了可以测试的地步,但我仍然觉得它可以改进。请容忍我。这就是我目前所拥有的 /* * The public API, which has a mockable internal factory responsible for creating log i

我一直在阅读编写可测试代码的相关知识,现在正试图通过重构我的日志框架API将其付诸实践。我主要关心的是,它应该1)易于从业务代码调用,2)易于模拟,这样就可以在不调用实际日志的情况下测试业务代码,并且可以断言是否已从所述测试中记录了内容

我已经到了可以测试的地步,但我仍然觉得它可以改进。请容忍我。这就是我目前所拥有的

/*
 * The public API, which has a mockable internal factory responsible for creating log implementations.
 */
public final class LoggerManager {
    private static LoggerFactory internalFactory;
    private LoggerManager() {}

    public static SecurityLogger getSecurityLogger() {
        return getLoggerFactory().getSecurityLogger();
    }
    public static SystemErrorLogger getSystemErrorLogger() {
        return getLoggerFactory().getSystemErrorLogger();
    }
    private static LoggerFactory getLoggerFactory() {
        if (internalFactory == null) 
            internalFactory = new LoggerFactoryImpl();
        return internalFactory;
    }
    public static void setLoggerFactory(LoggerFactory aLoggerFactory) {
        internalFactory = aLoggerFactory;
    }
}
/*
 * Factory interface with methods for getting all types of loggers.
 */
public interface LoggerFactory {
    public SecurityLogger getSecurityLogger();
    public SystemErrorLogger getSystemErrorLogger();
    // ... 10 additional log types
}
public final class LoggerFactoryImpl implements LoggerFactory {
    private final SecurityLogger securityLogger = new SecurityLoggerImpl();
    private final SystemErrorLogger systemErrorLogger = new SystemErrorLoggerImpl();

    public SecurityLogger getSecurityLogger() {
        return securityLogger;
    }
    public SystemErrorLogger getSystemErrorLogger() {
        return systemErrorLogger;
    }
}
API在业务代码中的调用方式如下:

LoggerManager.getSystemErrorLogger().log("My really serious error");
然后,我将使用TestLoggerFactory在单元测试中模拟这一点,该工厂创建测试记录器,该记录器只跟踪所有日志调用,并允许例如执行assertNoSystemErrorLogs():

现在这很好,但我仍然觉得好像我错过了什么,它可以使测试更友好。例如,通过使用静态setLoggerFactory,我为所有测试设置了记录器工厂,这意味着一个测试实际上可以影响另一个测试。所以我这里的大问题是,创建这种易于模仿的API的标准方法是什么?某种依赖注入


请注意,这个问题更多地涉及编写一个易于访问和使用的API,该API也易于模拟。我的示例是一个日志框架API这一事实与此无关。

最大的改进可能是使用现有的事实上的标准实现无关API,即slf4j。

而且您的类不是线程安全的
LoggerManager
可以创建多个
LoggerFactoryImpl
实例,我想您正在尝试将其设置为单例。
请注意
getLoggerFactory()

我在评论中被要求通过CDI使用记录器注入提供一个示例。有许多可能的实现,但为了展示如何使用不同的限定符,我选择了这个示例,其中我假设记录器共享公共记录器接口,并且使用了不同的限定符

然而,如果提供10+种可能的日志类型,我会选择使用enum属性的限定符来决定注入哪个记录器

首先定义限定符(安全性、SystemError等),以标记要注入的实现:

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface Security {
}
然后,您必须定义如何创建记录器实现。在某种程度上,这是一个工厂。实现可以依赖于注入点和给定给限定符的值。例如,在这里,我只是将注入的类的类名传递给记录器。 这两种方法用于创建不同的实现。这可以通过将限定符应用于方法来实现

@Singleton
public class LoggerProducer {

    // here perhaps a cache or environment related flags, ...

    @Security
    @Produces
    public Logger getSecurityLogger(InjectionPoint ip) {
        String key = getKeyFromIp(ip);
        return new SecurityLoggerImpl(key);
    }

    @SystemError
    @Produces
    public Logger getSystemErrorLogger(InjectionPoint ip) {
        String key = getKeyFromIp(ip);
        return new SystemErrorLoggerImpl(key);
    }

    private String getKeyFromIp(InjectionPoint ip) {
        return ip.getMember().getDeclaringClass().getCanonicalName();
    }

}
现在,您可以将所需的记录器插入您想要使用它的位置(请参阅CDI配置文件beans.xml)

当进行单元测试时,您仍然必须像模拟其他注入对象一样模拟记录器,使用CDI不会发生任何变化。集成测试时,您可以更改记录器的生成方式/内容

不要把它当成个人的事情,但我不明白为什么要使用10多个不同的记录器,而不使用大多数日志框架和/或模拟框架提供的机制。也许你有很好的理由,知道你所有的选择总是有帮助的


编辑:添加JUnit示例

为了对SampleService进行单元测试,我们使用Mockito创建了以下测试用例:

@RunWith(MockitoJUnitRunner.class)
public class SampleServiceTest {

    @InjectMocks
    private SampleService sample;

    @Mock
    private Logger securityLogger;

    @Test
    public void testDoSomething() {
        doThrow(new RuntimeException("Fail")).when(securityLogger).log(anyString());

        sample.doSomething(); // will fail 
    }

}

测试是这样设置的,当调用Logg.Logo时会引发异常。

如果您正在做JUnit测试,我会考虑使用像McCito这样的模拟框架,而不是实现您自己的可模仿类。同样,在Java EE环境中,您可以使用带有限定符/生产者的CDI来注入不同的记录器,而不是使用静态工厂。我同意第一个答案——尽管每个程序员都喜欢自己创建日志框架——您正在花时间重新发明一个已经存在的轮子。而且你的轮子版本将是专有的,不太复杂,测试更少。当然,它的功能会更少。@Dainesch当然,我可以在单元测试中使用Mockito.mock(LoggerFactory.class)而不是新的TestLoggerFactory(),但这并不会真正改变API的设计,对吗?@GhostCat很公平,但问题更多地与可测试性和编写易于模拟的框架API有关,不管是否创建自己的日志框架。它也可以是另一个框架。@Dainesh您的CDI示例听起来是一个可能的解决方案。请随意在回答中具体说明。是的,我知道,我删除了所有与编写易于模仿和可测试代码的问题无关的代码,以避免问题变得不必要的大。你怎么知道我没有在幕后使用它?问题是如何编写一个易于使用的API,该API可以从业务代码中访问,并且易于模拟,它是一个日志框架还是其他什么都不是重点。那么,您将如何在单元测试中模拟记录器呢?至于多个记录器的原因,我们有许多不同的自定义日志类型,所有日志类型都填充了不同的数据。从包含异常的错误日志到包含参数的请求日志,再到包含会话信息的会话日志等等。但正如我所说,我对创建可测试和可模拟设计的概念更感兴趣。
@Stateless
public class SampleService {

    @Inject
    @Security
    private Logger securityLogger;

    public void doSomething() {
        securityLogger.log("I did something!");
    }

}
@RunWith(MockitoJUnitRunner.class)
public class SampleServiceTest {

    @InjectMocks
    private SampleService sample;

    @Mock
    private Logger securityLogger;

    @Test
    public void testDoSomething() {
        doThrow(new RuntimeException("Fail")).when(securityLogger).log(anyString());

        sample.doSomething(); // will fail 
    }

}