【问题标题】:C# Static Readonly log4net logger, any way to change logger in Unit Test?C# 静态只读 log4net 记录器,有什么方法可以在单元测试中更改记录器?
【发布时间】:2013-05-23 16:45:27
【问题描述】:

我的班级有这行:

private static readonly ILog log = LogManager.GetLogger(typeof(Prim));

当我进行单元测试时,我无法将 moq 记录器注入此接口,因此我可以计算日志调用次数。

有没有办法做到这一点? Log4net 建议记录器使用静态只读模式。最好的处理方法是什么?

【问题讨论】:

  • Log4net 支持自定义 Appender,因此您应该能够以这种方式了解所有日志记录调用(大多数日志记录框架类似 - 包括带有 Trace 的默认 .Net 之一)

标签: c# unit-testing logging log4net moq


【解决方案1】:

虽然 log4net 推荐这种模式,但没有什么能阻止您在类外实例化记录器并将其注入。大多数 IoC 可以配置为注入同一个实例。这样,您可以为您的单元测试注入一个模拟。

我会推荐一个围绕 LogManager.GetLogger 的包装器,它总是为每种类型返回一个相同的记录器实例:

namespace StackOverflowExample.Moq
{
    public interface ILogCreator
    {
        ILog GetTypeLogger<T>() where T : class;
    }

    public class LogCreator : ILogCreator
    {
        private static readonly IDictionary<Type, ILog> loggers = new Dictionary<Type, ILog>();
        private static readonly object lockObject;

        public ILog GetTypeLogger<T>() where T : class
        {
            var loggerType = typeof (T);
            if (loggers.ContainsKey(loggerType))
            {
                return loggers[typeof (T)];
            }

            lock (lockObject)
            {
                if (loggers.ContainsKey(loggerType))
                {
                    return loggers[typeof(T)];
                }
                var logger = LogManager.GetLogger(loggerType);
                loggers[loggerType] = logger;
                return logger;
            }
        }
    }

    public class ClassWithLogger
    {
        private readonly ILog logger;
        public ClassWithLogger(ILogCreator logCreator)
        {
            logger = logCreator.GetTypeLogger<ClassWithLogger>();
        }

        public void DoSomething()
        {
            logger.Debug("called");
        }
    }

    [TestFixture]
    public class Log4Net
    {
        [Test]
        public void DoSomething_should_write_in_debug_logger()
        {
            //arrange
            var logger = new Mock<ILog>();
            var loggerCreator = Mock.Of<ILogCreator>(
                c =>
                c.GetTypeLogger<ClassWithLogger>() == logger.Object);

            var sut = new ClassWithLogger(loggerCreator);

            //act
            sut.DoSomething();

            //assert
            logger.Verify(l=>l.Debug("called"), Times.Once());
        }
    }
} 

【讨论】:

  • 这是一个有趣的想法,我怀疑如果使用只读静态没有真正的选择。删除只读或静态的后果是什么?
  • readonly 没有被删除,但即使是,也没有任何后果。对于静态,也没有任何后果,只要使用单例记录器,它将基于上述实现。在每次创建 ClassWithLogger 时,工厂工作的性能影响可以忽略不计,但它非常非常小。因此,除非您正在编写设备驱动程序,或者您正在创建 ClassWithLogger 的大量实例,否则没有真正的缺点。昂贵的部分是实际的记录器创建,它被单例减轻了。
【解决方案2】:

您可以使用MemoryAppender 代替模拟记录器。通过这种方式,您可以配置log4net收集内存中的日志事件,并通过GetEvents()获取来进行验证。

我找到了这些有用的例子:

这是第一个:

[SetUp]
public void SetUp()
{
    this.memoryAppender = new MemoryAppender();
    BasicConfigurator.Configure(this.memoryAppender);

    ...
}

[Test]
public void TestSomething()
{
    ...

    // Assert
    Assert.IsFalse(
        this.memoryAppender.GetEvents().Any(le => le.Level == Level.Error),
        "Did not expect any error messages in the logs");
}

还有更详细的:

public static class Log4NetTestHelper
{
    public static string[] RecordLog(Action action)
    {
        if (!LogManager.GetRepository().Configured)
            BasicConfigurator.Configure();
        var logMessages = new List<string>();
        var root = ((log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository()).Root;
        var attachable = root as IAppenderAttachable;

        var appender = new MemoryAppender();
        if (attachable != null)
            attachable.AddAppender(appender);

        try
        {           
            action();
        }
        finally
        {
            var loggingEvents = appender.GetEvents();
            foreach (var loggingEvent in loggingEvents)
            {
                var stringWriter = new StringWriter();
                loggingEvent.WriteRenderedMessage(stringWriter);
                logMessages.Add(string.Format("{0} - {1} | {2}", loggingEvent.Level.DisplayName, loggingEvent.LoggerName, stringWriter.ToString()));
            }
            if (attachable != null)
                attachable.RemoveAppender(appender);
        }

        return logMessages.ToArray();
    }
}

【讨论】:

    【解决方案3】:

    只要把readonly 拿走,Logger 就可以像通常的接口对象一样工作。

    请参阅以下正在测试的简单示例:

    using System;
    using log4net;
    
    namespace StackOverflowExample.Moq
    {
        public class ClassWithLogger
        {
            private static ILog _log;
            //private static readonly ILog log = LogManager.GetLogger(typeof(Prim));
    
        public ClassWithLogger(ILog log)
        {
            _log = log;
        }
    
            public void DoSomething(object para = null)
            {
                try
                {
                    if(para != null)
                    {
                        _log.Debug("called");
                    }
                    else
                    {
                        throw new System.ArgumentException("Parameter cannot be null");
                    }
                }
                catch (Exception ex)
                {
                    _log.Fatal("Exception raised!", ex);
                }
            }
        }
    } 
    

    单元测试代码:

    [TestMethod]
    public void Test_DoSomething_logger()
    {
        //arrange
        var mockLog = new Mock<log4net.ILog>();
        var classWithLogger = new ClassWithLogger(mockLog.Object);
        mockLog.Setup(m => m.Debug(It.IsAny<string>()));
        mockLog.Setup(m => m.Fatal(It.IsAny<string>(), It.IsAny<Exception>()));
    
        //act1
        classWithLogger.DoSomething(new object());
    
        //assert1
        mockLog.Verify(l => l.Debug("called"), Times.Once());
    
        //act2
        classWithLogger.DoSomething();
    
        //assert2
        mockLog.Verify(x => x.Fatal("Exception raised!", It.IsAny<Exception>()));
    }
    

    和主程序调用:

    public void MainProgramCall()
    {
        //......
        ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    
        var classWithLogger = new ClassWithLogger(log);
    
        classWithLogger.DoSomething(new object());
    
        //......
    
        classWithLogger.DoSomething();
    
        //......
    }
    

    【讨论】:

    • 我还想到了一个想法,那就是最好有构造函数注入的记录器。如果将UnityContainer 用于DI,则可以使容器自动解析ILog 参数,从而避免大量样板文件-参见例如我的nuget package
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多