简体   繁体   English

如何解决XUnit测试失败的情况,在“全部运行”下的NLog目标中断言消息

[英]How to troubleshoot the situation where XUnit tests fail with assertion of messages in NLog target under “Run All”

This is my environment: 这是我的环境:

  • Visual Studio 2017 Visual Studio 2017
  • Project's .NET runtime version is 4.6.2 Project的.NET运行时版本是4.6.2
  • XUnit version 2.3.1 XUnit版本2.3.1
  • NLog version 4.4.12 NLog版本4.4.12
  • Fluent Assertions 4.19.4 流利断言4.19.4

This is the problem: 这就是问题:

When I run the tests individually they pass, however when i run time via the "Run All" button in Test Explorer, I get failures and when repeating the running of the subsequent failed tasks, all of them pass eventually. 当我单独运行测试时,它们会通过,但是当我通过测试资源管理器中的“全部运行”按钮运行时,我会遇到故障,当重复运行后续失败的任务时,所有这些都会最终通过。 Also want to point out that I am not running the tests in parallel. 还想指出我没有并行运行测试。 The nature of the tests are such that the code under test emits log information which eventually ends up in a custom NLog Target. 测试的性质使得测试中的代码发出日志信息,最终以自定义NLog目标结束。 Here is an example program which can be run in order to reproduce the problem. 这是一个示例程序,可以运行以重现问题。

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

The above test code runs better. 上面的测试代码运行得更好。 After some earlier troubleshooting by following the messages, it appeared that I was not calling LogManager.ReconfigExistingLoggers(); 按照消息进行一些早期的故障排除后,似乎我没有调用LogManager.ReconfigExistingLoggers(); since the configurations are being created programatically (in the constructors of the test classes). 因为配置是以编程方式创建的(在测试类的构造函数中)。 Here is a note in the source code of LogManager : 这是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.

Afterwards all the tests ran as expected with occasional failures like shown below: 之后所有测试都按预期运行,偶尔出现如下所示的故障:

在此输入图像描述

I am wondering now if there is anything more I should be securing in my test setup or is this rather a bug NLog. 我现在想知道在我的测试设置中是否还有其他更多的东西,或者这是一个NLog的错误。 Any suggestion on how to go about to fix my test setup or, troubleshoot the setup would be most welcome. 任何关于如何修复我的测试设置或对设置进行故障排除的建议都是非常受欢迎的。 Thanks in advance. 提前致谢。


Update 更新

  • Changed List<LogData> to ConcurrentBag<LogData> . List<LogData>更改为ConcurrentBag<LogData> This however does not change the problem. 然而,这并没有改变问题。 Problem remains that messages are not arriving into collection in a timely manner. 问题仍然是消息没有及时到达收集。
  • Reformulated the problem and replaced previous code sample with an actual example (that can be run to reproduce the problem) + screenshots of the problem. 重新设计问题并用一个实际的例子(可以运行以重现问题)替换以前的代码示例+问题的屏幕截图。
  • Improved tests which run better but occasionally fail due to exceptions coming from NLog itself (added screenshot). 改进的测试运行得更好但偶尔因NLog本身的异常而失败(添加截图)。

The problem with above issue happens to be with XUnit Runner in VisualStudio. 上述问题恰好出现在VisualStudio中的XUnit Runner中。 Eventhough i had 'DO NOT RUN TESTS IN PARALLEL' disabled, the tests were running in parallel somehow. 尽管我已经“禁止并行测试”,但测试仍以某种方式并行运行。 @rolf-kristensen pointed in another NLog issue (Ref: https://github.com/NLog/NLog/issues/2525 ) that he had added the following: @ rolf-kristensen指出另一个NLog问题(参考: https//github.com/NLog/NLog/issues/2525 ),他已经添加了以下内容:

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

in the AssemblyInfo.cs file. AssemblyInfo.cs文件中。 This configuration is also mentioned on XUnit's page (Ref: https://xunit.github.io/docs/running-tests-in-parallel.html - Changing Default Behavior) 此配置也在XUnit的页面上提到(参考: https//xunit.github.io/docs/running-tests-in-parallel.html - 更改默认行为)

Very random code you have shown, and very random details about what is failing. 你已经展示了非常随机的代码,以及关于什么是失败的非常随机的细节。 So maybe my suggestions doesn't make sense to your problem. 所以也许我的建议对你的问题没有意义。

Instead of calling TestLogTarget directly, then you should setup a logging configuration: 您应该设置日志记录配置,而不是直接调用TestLogTarget

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

Make sure to add a lock around the access to the Messages. 确保围绕对消息的访问添加lock Either by making a ToArray() while holding the lock (or calling Contains while holding the lock ) 通过在持有lock同时制作ToArray() (或在持有lock同时调用Contains

Remember that NLog is a global engine, which require special effort in a unit testing environment where test-classes, test-appdomains are stopped and started frequently, so you need to know your unit-test-system and your nlog-system to make them work together. 请记住,NLog是一个全局引擎,需要在单元测试环境中特别努力,其中测试类,测试应用程序域被停止并经常启动,因此您需要了解您的单元测试系统和您的nlog系统来制作它们一起工作。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM