C#静态只读log4net记录器,有没有办法在单元测试中更改记录器?

C#静态只读log4net记录器,有没有办法在单元测试中更改记录器?,c#,unit-testing,logging,log4net,moq,C#,Unit Testing,Logging,Log4net,Moq,我的班级有这样一行: private static readonly ILog log = LogManager.GetLogger(typeof(Prim)); 当我进行单元测试时,我不能将moq记录器注入这个接口,以便统计日志调用 有办法做到这一点吗?Log4net建议记录器使用静态只读模式。处理它的最佳方法是什么?虽然log4net推荐这种模式,但没有什么可以阻止您在类外实例化记录器并将其注入。大多数IOC都可以配置为注入一个相同的实例。这样,对于单元测试,您可以注入一个mock 我建议

我的班级有这样一行:

private static readonly ILog log = LogManager.GetLogger(typeof(Prim));
当我进行单元测试时,我不能将moq记录器注入这个接口,以便统计日志调用


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

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

我建议在LogManager.GetLogger周围使用一个包装器,该包装器对于每种类型始终返回一个相同的logger实例:

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());
        }
    }
} 
namespace StackOverflowExample.Moq
{
公共接口ILogCreator
{
ILog GetTypeLogger(),其中T:class;
}
公共类日志创建者:ILogCreator
{
私有静态只读IDictionary Logger=new Dictionary();
私有静态只读对象lockObject;
公共ILog GetTypeLogger(),其中T:class
{
var loggerType=类型(T);
if(loggers.ContainsKey(loggerType))
{
返回记录器[类型(T)];
}
锁定(锁定对象)
{
if(loggers.ContainsKey(loggerType))
{
返回记录器[类型(T)];
}
var logger=LogManager.GetLogger(loggerType);
记录器[记录器类型]=记录器;
返回记录器;
}
}
}
带有记录器的公共类
{
专用只读ILog记录器;
带有记录器的公共类(ILogCreator logCreator)
{
logger=logCreator.GetTypeLogger();
}
公共无效剂量测定法()
{
调试(“调用”);
}
}
[测试夹具]
公共类Log4Net
{
[测试]
public void DoSomething_应_在_debug_logger()中写入
{
//安排
var logger=newmock();
var loggerCreator=Mock.Of(
c=>
c、 GetTypeLogger()==logger.Object);
var sut=带有记录器的新类(loggerCreator);
//表演
sut.DoSomething();
//断言
Verify(l=>l.Debug(“调用”),Times.Once();
}
}
} 

您可以使用
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();
    }
}
公共静态类Log4NetTestHelper
{
公共静态字符串[]记录日志(操作)
{
如果(!LogManager.GetRepository().Configured)
BasicConfigurator.Configure();
var logMessages=新列表();
var root=((log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository()).root;
var attachable=根作为IAAppenderAttachable;
var appender=newmemoryaappender();
if(可附加!=null)
AddAppender(appender);
尝试
{           
动作();
}
最后
{
var loggingEvents=appender.GetEvents();
foreach(loggingEvents中的var loggingEvent)
{
var stringWriter=新的stringWriter();
loggingEvent.WriterEndedMessage(stringWriter);
添加(string.Format(“{0}-{1}|{2}”、loggingEvent.Level.DisplayName、loggingEvent.LoggerName、stringWriter.ToString());
}
if(可附加!=null)
可附加。可拆卸附件(附件);
}
返回logMessages.ToArray();
}
}

只需去掉
只读
,记录器将与普通接口对象一样工作

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

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

Log4net支持自定义附加器,因此您应该能够通过这种方式了解所有日志调用(与大多数日志框架类似-包括带有
跟踪的默认.Net框架)。这是一个有趣的想法,我怀疑如果使用readonly static,没有真正的替代方法。删除readonly或static的后果是什么?readonly不会被删除,但即使被删除,也不会有任何后果。对于静态,只要使用单例记录器,也没有任何后果,它将基于上述实现。使用Logger创建每个类时,工厂的工作对性能的影响可以忽略不计,但影响非常小。因此,除非您正在编写设备驱动程序,或者您正在创建ClassWithLogger的gazzilion实例,否则没有真正的缺点。最昂贵的部分是实际的记录器创建,它通过单例得到了缓解。我还想到了一个主意,最好是使用构造函数注入记录器。如果将
UnityContainer
用于DI,则可以使容器自动解析
ILog
参数,从而避免大量样板文件-例如,请参阅my。
public void MainProgramCall()
{
    //......
    ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

    var classWithLogger = new ClassWithLogger(log);

    classWithLogger.DoSomething(new object());

    //......

    classWithLogger.DoSomething();

    //......
}