【发布时间】:2018-01-16 05:30:37
【问题描述】:
这是我的环境:
- Visual Studio 2017
- 项目的 .NET 运行时版本为 4.6.2
- XUnit 2.3.1 版
- NLog 版本 4.4.12
- Fluent 断言 4.19.4
这是问题所在:
当我单独运行测试时,它们通过了,但是当我通过测试资源管理器中的“全部运行”按钮运行时,我遇到了失败,并且在重复运行后续失败的任务时,它们最终都通过了。还想指出我没有并行运行测试。测试的性质是,被测代码发出日志信息,最终以自定义 NLog 目标结束。这是一个示例程序,可以运行它来重现问题。
using FluentAssertions;
using NLog;
using NLog.Common;
using NLog.Config;
using NLog.Targets;
using System;
using System.Collections.Concurrent;
using System.IO;
using Xunit;
namespace LoggingTests
{
[Target("test-target")]
public class TestTarget : TargetWithLayout
{
public ConcurrentBag<string> Messages = new ConcurrentBag<string>();
public TestTarget(string name)
{
Name = name;
}
protected override void Write(LogEventInfo logEvent)
{
Messages.Add(Layout.Render(logEvent));
}
}
class Loggable
{
private Logger _logger;
public Loggable()
{
_logger = LogManager.GetCurrentClassLogger();
}
private void Log(LogLevel level,
Exception exception,
string message,
params object[] parameters)
{
LogEventInfo log_event = new LogEventInfo();
log_event.Level = level;
log_event.Exception = exception;
log_event.Message = message;
log_event.Parameters = parameters;
log_event.LoggerName = _logger.Name;
_logger.Log(log_event);
}
public void Debug(string message)
{
Log(LogLevel.Debug,
null,
message,
null);
}
public void Error(string message)
{
Log(LogLevel.Error,
null,
message,
null);
}
public void Info(string message)
{
Log(LogLevel.Info,
null,
message,
null);
}
public void Fatal(string message)
{
Log(LogLevel.Fatal,
null,
message,
null);
}
}
public class Printer
{
public delegate void Print(string message);
private Print _print_function;
public Printer(Print print_function)
{
_print_function = print_function;
}
public void Run(string message_template,
int number_of_times)
{
for (int i = 0; i < number_of_times; i++)
{
_print_function($"{message_template} - {i}");
}
}
}
public abstract class BaseTest
{
protected string _target_name;
public BaseTest(LogLevel log_level)
{
if (LogManager.Configuration == null)
{
LogManager.Configuration = new LoggingConfiguration();
InternalLogger.LogLevel = LogLevel.Debug;
InternalLogger.LogFile = Path.Combine(Environment.CurrentDirectory,
"nlog_debug.txt");
}
// Register target:
_target_name = GetType().Name;
Target.Register<TestTarget>(_target_name);
// Create Target:
TestTarget t = new TestTarget(_target_name);
t.Layout = "${message}";
// Add Target to configuration:
LogManager.Configuration.AddTarget(_target_name,
t);
// Add a logging rule pertaining to the above target:
LogManager.Configuration.AddRule(log_level,
log_level,
t);
// Because configuration has been modified programatically, we have to reconfigure all loggers:
LogManager.ReconfigExistingLoggers();
}
protected void AssertTargetContains(string message)
{
TestTarget target = (TestTarget)LogManager.Configuration.FindTargetByName(_target_name);
target.Messages.Should().Contain(message);
}
}
public class TestA : BaseTest
{
public TestA() : base(LogLevel.Info)
{
}
[Fact]
public void SomeTest()
{
int number_of_times = 100;
(new Printer((new Loggable()).Info)).Run(GetType().Name,
number_of_times);
for (int i = 0; i < number_of_times; i++)
{
AssertTargetContains($"{GetType().Name} - {i}");
}
}
}
public class TestB : BaseTest
{
public TestB() : base(LogLevel.Debug)
{
}
[Fact]
public void SomeTest()
{
int number_of_times = 100;
(new Printer((new Loggable()).Debug)).Run(GetType().Name,
number_of_times);
for (int i = 0; i < number_of_times; i++)
{
AssertTargetContains($"{GetType().Name} - {i}");
}
}
}
public class TestC : BaseTest
{
public TestC() : base(LogLevel.Error)
{
}
[Fact]
public void SomeTest()
{
int number_of_times = 100;
(new Printer((new Loggable()).Error)).Run(GetType().Name,
number_of_times);
for (int i = 0; i < number_of_times; i++)
{
AssertTargetContains($"{GetType().Name} - {i}");
}
}
}
public class TestD : BaseTest
{
public TestD() : base(LogLevel.Fatal)
{
}
[Fact]
public void SomeTest()
{
int number_of_times = 100;
(new Printer((new Loggable()).Fatal)).Run(GetType().Name,
number_of_times);
for (int i = 0; i < number_of_times; i++)
{
AssertTargetContains($"{GetType().Name} - {i}");
}
}
}
}
上面的测试代码运行得更好。在按照消息进行一些较早的故障排除后,我似乎没有调用LogManager.ReconfigExistingLoggers();,因为配置是以编程方式创建的(在测试类的构造函数中)。这里是LogManager的源码中的一个注释:
/// Loops through all loggers previously returned by GetLogger.
/// and recalculates their target and filter list. Useful after modifying the configuration programmatically
/// to ensure that all loggers have been properly configured.
之后,所有测试都按预期运行,偶尔会出现如下所示的失败:
我现在想知道我是否应该在我的测试设置中保护更多内容,或者这是否是一个错误 NLog。任何有关如何修复我的测试设置或对设置进行故障排除的建议都将受到欢迎。提前致谢。
更新
- 将
List<LogData>更改为ConcurrentBag<LogData>。然而,这并不能改变问题。问题仍然是消息没有及时到达收集。 - 对问题进行了重新表述,并将之前的代码示例替换为实际示例(可以运行以重现问题)+问题的屏幕截图。
- 改进的测试运行得更好,但由于 NLog 本身的异常偶尔会失败(添加屏幕截图)。
【问题讨论】:
-
可能是共享资源测试中的线程问题。尝试将
Messages.Add包装在lock中。我敢肯定,如果每个测试都有自己的日志目标实例,那么一切都会正常工作。 -
即使我将
List<LogData>更改为ConcurrentBag<LogData>,问题仍然存在:消息没有及时到达收集。我想我会求助于在目标的Write()方法中添加一个触发器,以便最终回调到测试方法并运行断言。 -
您是否尝试将 StringWriter 附加到 NLog.Common.InternalLogger.LogWriter 并使用 Console.WriteLine 输出结果(应该由 visual studio unit-test-runner 获取)
-
我改用
InternalLogger.LogFile,但无法跟踪NLog本身抛出的异常等故障。但是,如上所述,我的测试设置有所改进。
标签: c# unit-testing continuous-integration nlog xunit.net