【问题标题】:How to troubleshoot the situation where XUnit tests fail with assertion of messages in NLog target under "Run All"如何解决 XUnit 测试失败并在“全部运行”下的 NLog 目标中断言消息的情况
【发布时间】: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&lt;LogData&gt; 更改为ConcurrentBag&lt;LogData&gt;。然而,这并不能改变问题。问题仍然是消息没有及时到达收集。
  • 对问题进行了重新表述,并将之前的代码示例替换为实际示例(可以运行以重现问题)+问题的屏幕截图。
  • 改进的测试运行得更好,但由于 NLog 本身的异常偶尔会失败(添加屏幕截图)。

【问题讨论】:

  • 可能是共享资源测试中的线程问题。尝试将Messages.Add 包装在lock 中。我敢肯定,如果每个测试都有自己的日志目标实例,那么一切都会正常工作。
  • 即使我将List&lt;LogData&gt; 更改为ConcurrentBag&lt;LogData&gt;,问题仍然存在:消息没有及时到达收集。我想我会求助于在目标的Write() 方法中添加一个触发器,以便最终回调到测试方法并运行断言。
  • 您是否尝试将 StringWriter 附加到 NLog.Common.InternalLogger.LogWriter 并使用 Console.WriteLine 输出结果(应该由 visual studio unit-test-runner 获取)
  • 我改用InternalLogger.LogFile,但无法跟踪NLog本身抛出的异常等故障。但是,如上所述,我的测试设置有所改进。

标签: c# unit-testing continuous-integration nlog xunit.net


【解决方案1】:

上述问题的问题恰好与 VisualStudio 中的 XUnit Runner 有关。尽管我禁用了“不要并行运行测试”,但测试以某种方式并行运行。 @rolf-kristensen 指出另一个 NLog 问题(参考:https://github.com/NLog/NLog/issues/2525)他添加了以下内容:

[assembly: Xunit.CollectionBehavior(DisableTestParallelization = true)]

AssemblyInfo.cs 文件中。 XUnit 的页面上也提到了此配置(参考:https://xunit.github.io/docs/running-tests-in-parallel.html - 更改默认行为)

【讨论】:

  • 这解决了我的问题,但我没有在任何依赖代码中使用 NLog,我使用的是 TPL 数据流。
  • xunit.github.io 的链接坏了。
【解决方案2】:

您显示的代码非常随机,并且关于失败的细节非常随机。所以也许我的建议对你的问题没有意义。

与其直接调用TestLogTarget,不如设置一个日志配置:

var target = new TestLogTarget() { Name = "Test" };
NLog.Config.SimpleConfigurator(target);
var logger = NLog.LogManager.GetCurrentClassLogger();
logger.Info("Hello World");

确保在消息访问权限周围添加lock。通过在按住lock 时发出ToArray()(或在按住lock 时调用Contains

请记住,NLog 是一个全局引擎,在单元测试环境中需要特别努力,其中 test-classes、test-appdomains 经常停止和启动,因此您需要了解您的 unit-test-system 和您的 nlog-system让他们一起工作。

【讨论】:

  • 即使我将List&lt;LogData&gt; 更改为ConcurrentBag&lt;LogData&gt;,问题仍然存在:消息没有及时到达收集。我想我会求助于在目标的Write() 方法中添加一个触发器,以便最终回调到测试方法并运行断言。抱歉,如果我不清楚,但是就失败的原因而言:它是关于(数据结构消息)中存在给定日志消息的断言,该消息假设保存所有日志消息。我也像你在上面做的那样通过 SimpleConfigurator 注册目标(参考:构造函数)
  • 我已经重新表述了问题并添加了可以运行的代码(必须安装依赖项:XUnitFluentAssertionsNLog)以重现问题(请参阅新添加的屏幕截图问题描述)。
  • 嗯,NLog 项目正在使用 xUnit,除了记录之外什么都不做,并验证记录是否正确。
  • 我查看了 github 上的 NLog 测试,但无法进行相同的设置。例如,所有测试都继承自NLogTestBase,并且在其构造函数中,Close()LogManager.Configuration 上被调用。但是,LoggingConfiguration 上没有 Close() 方法。
  • 同调用LogManager.Configuration = null;
猜你喜欢
  • 2021-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-25
  • 2014-01-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多