用Java构建可模拟的日志API,正确的方法

时间:2017-08-04 07:24:58

标签: java testing junit mocking

我一直在阅读编写可测试代码的内容,现在我正在尝试通过重构我的日志框架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():

LoggerManager.setLoggerFactory(new TestLoggerFactory());

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

请注意,这个问题更多地与编写易于访问和使用易于模拟的API有关。我的示例是一个日志框架API的事实就是重点。

3 个答案:

答案 0 :(得分:1)

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

答案 1 :(得分:1)

I was asked in the comments to provide a sample using logger injection via CDI. There are many possible implementations, but in order to show how different qualifiers could be used I opted for this sample where I assume that the loggers share the common Logger interface and where different qualifiers are used.

However provided with the case of 10+ possible log types, I would opt for a qualifier using an enum property to decide which logger to inject.

First define your qualifiers (Security, SystemError, ....) to mark which implementation you would want to inject:

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface Security {
}

Then you have to define how you create the logger implementations. It is a factory, sort of. The implementation can depend on the injection point and values given to the qualifiers. Here for example, I just pass the loggers the class name of the class where it is injected. The two methods are used to create the different implementations. That can be achieved by applying the qualifiers to the methods.

@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();
    }

}

Now you can inject your desired logger where you want to use it (see CDI configuration file beans.xml)

@Stateless
public class SampleService {

    @Inject
    @Security
    private Logger securityLogger;

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

}

When unit testing you will still have to mock the logger as you would mock every other injected object, nothing changes using CDI. When integration testing, you could change how/what loggers are produced.

Don't take it personal, but I don't get why to use 10+ different loggers and not use the mechanisms provided by most log frameworks and/or mock frameworks. Perhaps you have good reasons, well, it always helps to know all your options.


Edit: Add JUnit sample

To unit test the SampleService we create the following test case using 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 
    }

}

The test is setup up in such a way, that when logger.log is called an exception is thrown.

答案 2 :(得分:0)

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