4

I've read http://xunit.github.io/docs/capturing-output.html and it seems to apply to making my test output specific message during the test running but I would really like to be able to capture the log4net output that is already integrated into the classes I am testing.

In the past i have set up log4net to use a TraceLogger and the test framework was able to associate the output with the test. (different testing framework). How can I somehow associate log4net output to the Xunit IOutputHelper?

Jeff Martin
  • 10,812
  • 7
  • 48
  • 74

2 Answers2

4

This was the answer i came up with

This is a class I can make my test class inherit from:

   public class LogOutputTester:IDisposable
    {
        private readonly IAppenderAttachable _attachable;
        private TestOutputAppender _appender;

        protected LogOutputTester(ITestOutputHelper output)
        {
            log4net.Config.XmlConfigurator.Configure(); 
            var root = ((log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository()).Root;
            _attachable = root;

            _appender = new TestOutputAppender(output);
            if (_attachable != null)
                _attachable.AddAppender(_appender);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            _attachable.RemoveAppender(_appender);
        }
    }

this is a custom Appender I made referenced in the above helper:

 public class TestOutputAppender : AppenderSkeleton
    {
        private readonly ITestOutputHelper _xunitTestOutputHelper;

        public TestOutputAppender(ITestOutputHelper xunitTestOutputHelper)
        {
            _xunitTestOutputHelper = xunitTestOutputHelper;
            Name = "TestOutputAppender";
            Layout = new PatternLayout("%-5p %d %5rms %-22.22c{1} %-18.18M - %m%n");
        }

        protected override void Append(LoggingEvent loggingEvent)
        {
            _xunitTestOutputHelper.WriteLine(RenderLoggingEvent(loggingEvent));
        }
    }

This could be customized more to take a custom layout or whatever...

Finally - i just make my test class inherit from this helper:

   public class MyTestClass:LogOutputTester
    {
        public EdgeClientTests(ITestOutputHelper output):base(output)
        {
        }
    ...

You could give your tests direct access to the output object too...

Jeff Martin
  • 10,812
  • 7
  • 48
  • 74
  • 1
    you may need to do some setup in your app.config with log4net. My LogOutputTester does the log4net configure, but that may not be enough (especially if you want your normal appenders) – Jeff Martin Apr 27 '15 at 22:57
  • 1
    I believe there is still a problem with this - if there are 2 or more instances of LogOutputTester, as there may be with parallel tests, then there are 2 or more corresponding appenders. Thus, log output as result of one test will leak into the other. Alternative solution using diagnostic contexts to filter correct log messages: https://github.com/damianh/CapturingLogOutputWithXunit2AndParallelTests#capturing-test-specific-log-output-when-using-xunit-2x-parallel-testing –  May 18 '15 at 09:40
  • Thanks for that link - i'll look into it. you are right - i haven't done a ton of testing with this and its just the first thing i came up with - given no other answers (hint, hint) - its the one i went with... if there was an alternative Answer to this question - i could give it credit... – Jeff Martin May 20 '15 at 17:09
3

Based on solution described here https://github.com/damianh/CapturingLogOutputWithXunit2AndParallelTests#capturing-test-specific-log-output-when-using-xunit-2x-parallel-testing, i've rewrited it using log4net. May be it will helpful to somebody.

public static class LogHelper
    {
        private static readonly Subject<LoggingEvent> LogEventSubject = new Subject<LoggingEvent>();
        private const string CaptureCorrelationIdKey = "EventId";
        private static readonly ILayout layout;

        static LogHelper()
        {
            XmlConfigurator.Configure();
            var root = ((log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository()).Root;
            IAppenderAttachable attachable = root;
            var appender = new XunitAppender(logEvent => LogEventSubject.OnNext(logEvent));
            attachable?.AddAppender(appender);
            layout = appender.Layout;
            appender.ActivateOptions();
        }

        public static IDisposable Capture(ITestOutputHelper testOutputHelper)
        {

            var captureId = Guid.NewGuid();
            Func<LoggingEvent, bool> filter = logEvent =>
                logEvent.GetProperties().Contains(CaptureCorrelationIdKey) &&
                logEvent.LookupProperty(CaptureCorrelationIdKey).ToString() == captureId.ToString();

            var subscription = LogEventSubject.Where(filter).Subscribe(logEvent =>
            {
                using (var writer = new StringWriter())
                {
                    layout.Format(writer, logEvent);
                    testOutputHelper.WriteLine(writer.ToString());
                }
            });

            ThreadContext.Properties[CaptureCorrelationIdKey] = captureId.ToString();

            return new DisposableAction(() =>
            {
                subscription.Dispose();
                ThreadContext.Properties.Clear();
            });
        }

        private class DisposableAction : IDisposable
        {
            private readonly Action _action;

            public DisposableAction(Action action)
            {
                _action = action;
            }

            public void Dispose()
            {
                _action();
            }
        }

        public sealed class XunitAppender : AppenderSkeleton
        {
            private readonly Action<LoggingEvent> _action;

            public XunitAppender(Action<LoggingEvent> action)
            {
                _action = action;
                Name = "XunitAppender";
                Layout = new PatternLayout("%date %-5level %logger - %message");
            }

            protected override void Append(LoggingEvent loggingEvent)
            {
                _action(loggingEvent);
            }
        }
    }
nolmit
  • 31
  • 1