根据DDIY(Don't Do It Yourself)原则,如果程序需要日志功能,log4net是一个很好的选择。
但在整合log4net的过程中,我们该怎么做不影响代码的可测试性。
以下通过一个简单的例子来探讨一下这个问题。
此示例会用到以下开源库
- log4net (http://logging.apache.org/log4net/)
- NUnit (http://nunit.org/)
- Autofac (http://autofac.org/)
- Autofac.log4net (https://github.com/erangil2/autofac.log4net)
- Moq (https://github.com/moq/moq4)
方式一:static readonly
此种方式类不会暴露ILog接口给外部,如果不需要模拟ILog时,这样的方式其实也挺好的。
(注:我们不会引入一个中间层来抽象log4net的调用。因为我觉得log4net就是比较好的选择。软件切换到其他日志库的可能性几乎为零。)
Calculator1.cs
using log4net;
namespace T01Log4net
{
public class Calculator1
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(Calculator1));
public int Add(int addend1, int addend2)
{
Logger.Info($"Adding {addend1} and {addend2}...");
var result = addend1 + addend2;
Logger.Info($"Sum is {result}");
return result;
}
}
}
Calculator1Tests.cs
using NUnit.Framework;
namespace T01Log4net.Tests
{
[TestFixture]
public class Calculator1Tests
{
[TestCase(0, 0, 0)]
[TestCase(-1, 1, 0)]
[TestCase(-1, -2, -3)]
[TestCase(1, 2, 3)]
public void Add_TwoNumbers_ShouldReturnExpectedSum(int addend1, int addend2, int expected)
{
var calculator1 = new Calculator1();
var actual = calculator1.Add(addend1, addend2);
Assert.That(actual, Is.EqualTo(expected));
}
}
}
方式二:constructor injection
这种方式的好处是我们可以测试ILog的行为。但怎样让每个类注入一个和自己类型相关的ILog呢?
Autofac提供了一种简单的实现方式。我们待会看看怎么实现。
Calculator2.cs
using System;
using log4net;
namespace T01Log4net
{
public class Calculator2
{
private readonly ILog _logger;
public Calculator2(ILog logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public int Add(int addend1, int addend2)
{
_logger.Info($"Adding {addend1} and {addend2}...");
var result = addend1 + addend2;
_logger.Info($"Sum is {result}");
return result;
}
}
}
Calculator2Tests.cs
using log4net;
using Moq;
using NUnit.Framework;
namespace T01Log4net.Tests
{
[TestFixture]
public class Calculator2Tests
{
private Mock<ILog> _loggerMock;
private Calculator2 _calculator2;
[SetUp]
public void SetUp()
{
_loggerMock = new Mock<ILog>();
_calculator2 = new Calculator2(_loggerMock.Object);
}
[TestCase(0, 0, 0)]
[TestCase(-1, 1, 0)]
[TestCase(-1, -2, -3)]
[TestCase(1, 2, 3)]
public void Add_TwoNumbers_ShouldReturnExpectedSum(int addend1, int addend2, int expected)
{
var actual = _calculator2.Add(addend1, addend2);
Assert.That(actual, Is.EqualTo(expected));
}
[Test]
public void Add_TwoNumbers_ShouldLogThemAndSum()
{
_calculator2.Add(1, 2);
_loggerMock.Verify(l => l.Info("Adding 1 and 2..."), Times.Once);
_loggerMock.Verify(l => l.Info("Sum is 3"), Times.Once);
}
}
}
我们一般不会测试ILog。如果测试ILog的行为,应该属于“过度测试”的一种。以上示例只是演示这种可能性。
App中的整合
Bootstrapper.cs
using Autofac;
using Autofac.log4net;
namespace T01Log4net.ConsoleApp
{
public class Bootstrapper
{
public IContainer Bootstrap()
{
var builder = new ContainerBuilder();
builder.RegisterType<Calculator1>();
builder.RegisterType<Calculator2>();
var loggingModule = new Log4NetModule
{
ConfigFileName = "log4net.config",
ShouldWatchConfiguration = true
};
builder.RegisterModule(loggingModule);
return builder.Build();
}
}
}
log4net.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
</configSections>
<log4net>
<root>
<level value="ALL" />
<appender-ref ref="allAppender" />
<appender-ref ref="warnAppender" />
</root>
<appender name="allAppender" type="log4net.Appender.FileAppender">
<appendToFile value="false" />
<encoding value="utf-8"/>
<file value="log\all.log" />
<immediateFlush value="false" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date{HH:mm:ss,fff} [%2thread] %-5level %-20.20logger{1} - %message%newline" />
</layout>
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<threshold value="ALL" />
</appender>
<appender name="warnAppender" type="log4net.Appender.FileAppender">
<appendToFile value="false" />
<encoding value="utf-8"/>
<file value="log\warn.log" />
<immediateFlush value="false" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date{HH:mm:ss,fff} [%2thread] %-5level %-20.20logger{1} - %message%newline" />
</layout>
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<threshold value="WARN" />
</appender>
</log4net>
</configuration>
Program.cs
using Autofac;
namespace T01Log4net.ConsoleApp
{
class Program
{
static void Main(string[] args)
{
using (var container = GetContainer())
{
var calculator1 = container.Resolve<Calculator1>();
calculator1.Add(1, 2);
var calculator2 = container.Resolve<Calculator2>();
calculator2.Add(3, 4);
}
}
private static IContainer GetContainer()
{
var bootstrapper = new Bootstrapper();
return bootstrapper.Bootstrap();
}
}
}
以下是产生的日志信息