Skip to content

topalach/XunitLogger

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

XunitLogger

Build status NuGet Status NuGet Status

Extends xUnit to simplify logging.

Redirects Trace.Write, Debug.Write, and Console.Write and Console.Error.Write to ITestOutputHelper. Also provides static access to the current ITestOutputHelper for use within testing utility methods.

Uses AsyncLocal to track state.

Contents

NuGet package

https://nuget.org/packages/XunitLogger/

ClassBeingTested

using System;
using System.Diagnostics;

static class ClassBeingTested
{
    public static void Method()
    {
        Trace.WriteLine("From Trace");
        Console.WriteLine("From Console");
        Debug.WriteLine("From Debug");
        Console.Error.WriteLine("From Console Error");
    }
}

snippet source / anchor

XunitLoggingBase

XunitLoggingBase is an abstract base class for tests. It exposes logging methods for use from unit tests, and handle the flushing of longs in its Dispose method. XunitLoggingBase is actually a thin wrapper over XunitLogging. XunitLoggings Write* methods can also be use inside a test inheriting from XunitLoggingBase.

using Xunit;
using Xunit.Abstractions;

public class TestBaseSample  :
    XunitLoggingBase
{
    [Fact]
    public void Write_lines()
    {
        WriteLine("From Test");
        ClassBeingTested.Method();

        var logs = XunitLogging.Logs;

        Assert.Contains("From Test", logs);
        Assert.Contains("From Trace", logs);
        Assert.Contains("From Debug", logs);
        Assert.Contains("From Console", logs);
        Assert.Contains("From Console Error", logs);
    }

    public TestBaseSample(ITestOutputHelper output) :
        base(output)
    {
    }
}

snippet source / anchor

XunitLogging

XunitLogging provides static access to the logging state for tests. It exposes logging methods for use from unit tests, however registration of ITestOutputHelper and flushing of logs must be handled explicitly.

using System;
using Xunit;
using Xunit.Abstractions;

public class XunitLoggerSample :
    IDisposable
{
    [Fact]
    public void Usage()
    {
        XunitLogging.WriteLine("From Test");

        ClassBeingTested.Method();

        var logs = XunitLogging.Logs;

        Assert.Contains("From Test", logs);
        Assert.Contains("From Trace", logs);
        Assert.Contains("From Debug", logs);
        Assert.Contains("From Console", logs);
        Assert.Contains("From Console Error", logs);
    }

    public XunitLoggerSample(ITestOutputHelper testOutput)
    {
        XunitLogging.Register(testOutput);
    }

    public void Dispose()
    {
        XunitLogging.Flush();
    }
}

snippet source / anchor

XunitLogging redirects Trace.Write, Console.Write, and Debug.Write in its static constructor.

Trace.Listeners.Clear();
Trace.Listeners.Add(new TraceListener());
#if (NETSTANDARD)
DebugPoker.Overwrite(
    text =>
    {
        if (string.IsNullOrEmpty(text))
        {
            return;
        }

        if (text.EndsWith(Environment.NewLine))
        {
            WriteLine(text.TrimTrailingNewline());
            return;
        }

        Write(text);
    });
#else
Debug.Listeners.Clear();
Debug.Listeners.Add(new TraceListener());
#endif
var writer = new TestWriter();
Console.SetOut(writer);
Console.SetError(writer);

snippet source / anchor

These API calls are then routed to the correct xUnit ITestOutputHelper via a static AsyncLocal.

Filters

XunitLogger.Filters can be used to filter out unwanted lines:

using Xunit;
using Xunit.Abstractions;
using XunitLogger;

public class FilterSample :
    XunitLoggingBase
{
    static FilterSample()
    {
        Filters.Add(x => x != null && !x.Contains("ignored"));
    }

    [Fact]
    public void Write_lines()
    {
        WriteLine("first");
        WriteLine("with ignored string");
        WriteLine("last");
        var logs = XunitLogging.Logs;

        Assert.Contains("first", logs);
        Assert.DoesNotContain("with ignored string", logs);
        Assert.Contains("last", logs);
    }

    public FilterSample(ITestOutputHelper output) :
        base(output)
    {
    }
}

snippet source / anchor

Filters are static and shared for all tests.

Context

For every tests there is a contextual API to perform several operations.

  • Context.TestOutput: Access to ITestOutputHelper.
  • Context.Write and Context.WriteLine: Write to the current log.
  • Context.LogMessages: Access to all log message for the current test.
  • Counters: Provide access in predicable and incrementing values for the following types: Guid, Int, Long, UInt, and ULong.
  • Context.Test: Access to the current ITest.

There is also access via a static API.

using Xunit;
using Xunit.Abstractions;

public class ContextSample  :
    XunitLoggingBase
{
    [Fact]
    public void Usage()
    {
        var currentGuid = Context.CurrentGuid;

        var nextGuid = Context.NextGuid();

        Context.WriteLine("Some message");

        var currentLogMessages = Context.LogMessages;

        var testOutputHelper = Context.TestOutput;

        var currentTest = Context.Test;
    }

    [Fact]
    public void StaticUsage()
    {
        var currentGuid = XunitLogging.Context.CurrentGuid;

        var nextGuid = XunitLogging.Context.NextGuid();

        XunitLogging.Context.WriteLine("Some message");

        var currentLogMessages = XunitLogging.Context.LogMessages;

        var testOutputHelper = XunitLogging.Context.TestOutput;

        var currentTest = XunitLogging.Context.Test;
    }

    public ContextSample(ITestOutputHelper output) :
        base(output)
    {
    }
}

snippet source / anchor

Current Test

There is currently no API in xUnit to retrieve information on the current test. See issues #1359, #416, and #398.

To work around this, this project exposes the current instance of ITest via reflection.

Usage:

using Xunit;
using Xunit.Abstractions;

public class CurrentTestSample :
    XunitLoggingBase
{
    [Fact]
    public void Usage()
    {
        var currentTest = Context.Test;
        // DisplayName will be 'TestNameSample.Usage'
        var displayName = currentTest.DisplayName;
    }

    [Fact]
    public void StaticUsage()
    {
        var currentTest = XunitLogging.Context.Test;
        // DisplayName will be 'TestNameSample.StaticUsage'
        var displayName = currentTest.DisplayName;
    }

    public CurrentTestSample(ITestOutputHelper output) :
        base(output)
    {
    }
}

snippet source / anchor

Implementation:

using System;
using System.Reflection;
using Xunit.Abstractions;

namespace XunitLogger
{
    public partial class Context
    {
        ITest? test;

        static FieldInfo? cachedTestMember;

        public ITest Test
        {
            get => test ??= (ITest) GetTestMethod().GetValue(TestOutput);
        }

        public static string MissingTestOutput = "ITestOutputHelper has not been set. It is possible that the call to `XunitLogging.Register()` is missing, or the current test does not inherit from `XunitLoggingBase`.";

        FieldInfo GetTestMethod()
        {
            if (TestOutput == null)
            {
                throw new Exception(MissingTestOutput);
            }

            if (cachedTestMember != null)
            {
                return cachedTestMember;
            }

            var testOutputType = TestOutput.GetType();
            cachedTestMember = testOutputType.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic);
            if (cachedTestMember == null)
            {
                throw new Exception($"Unable to find 'test' field on {testOutputType.FullName}");
            }

            return cachedTestMember;
        }
    }
}

snippet source / anchor

Test Failure

When a test fails it is expressed as an exception. The exception can be viewed by enabling exception capture, and then accessing Context.TestException. The TestException will be null if the test has passed.

One common case is to perform some logic, based on the existence of the exception, in the Dispose of a test.

public class TestExceptionSample :
    XunitLoggingBase
{
    static TestExceptionSample()
    {
        //Should be called once at appdomain startup
        XunitLogging.EnableExceptionCapture();
    }

    [Fact]
    public void Usage()
    {
        //This tests will fail
        Assert.False(true);
    }

    public TestExceptionSample(ITestOutputHelper output) :
        base(output)
    {
    }

    public override void Dispose()
    {
        var theExceptionThrownByTest = Context.TestException;
        var testDisplayName = Context.Test.DisplayName;
        var testCase = Context.Test.TestCase;
        base.Dispose();
    }
}

snippet source / anchor

Counters

Provide access to predicable and incrementing values for the following types: Guid, Int, Long, UInt, and ULong.

Non Test Context usage

Counters can also be used outside of the current test context:

var current = Counters.CurrentGuid;

var next = Counters.NextGuid();

var counter = new GuidCounter();
var localCurrent = counter.Current;
var localNext = counter.Next();

snippet source / anchor

Implementation

using System;

namespace XunitLogger
{
    public partial class Context
    {
        GuidCounter GuidCounter = new GuidCounter();
        IntCounter IntCounter = new IntCounter();
        LongCounter LongCounter = new LongCounter();
        UIntCounter UIntCounter = new UIntCounter();
        ULongCounter ULongCounter = new ULongCounter();

        public uint CurrentUInt
        {
            get => UIntCounter.Current;
        }

        public int CurrentInt
        {
            get => IntCounter.Current;
        }

        public long CurrentLong
        {
            get => LongCounter.Current;
        }

        public ulong CurrentULong
        {
            get => ULongCounter.Current;
        }

        public Guid CurrentGuid
        {
            get => GuidCounter.Current;
        }

        public uint NextUInt()
        {
            return UIntCounter.Next();
        }

        public int NextInt()
        {
            return IntCounter.Next();
        }

        public long NextLong()
        {
            return LongCounter.Next();
        }

        public ulong NextULong()
        {
            return ULongCounter.Next();
        }

        public Guid NextGuid()
        {
            return GuidCounter.Next();
        }
    }
}

snippet source / anchor

using System;

namespace XunitLogger
{
    public static class Counters
    {
        static GuidCounter GuidCounter = new GuidCounter();
        static IntCounter IntCounter = new IntCounter();
        static LongCounter LongCounter = new LongCounter();
        static UIntCounter UIntCounter = new UIntCounter();
        static ULongCounter ULongCounter = new ULongCounter();

        public static uint CurrentUInt
        {
            get => UIntCounter.Current;
        }

        public static int CurrentInt
        {
            get => IntCounter.Current;
        }

        public static long CurrentLong
        {
            get => LongCounter.Current;
        }

        public static ulong CurrentULong
        {
            get => ULongCounter.Current;
        }

        public static Guid CurrentGuid
        {
            get => GuidCounter.Current;
        }

        public static uint NextUInt()
        {
            return UIntCounter.Next();
        }

        public static int NextInt()
        {
            return IntCounter.Next();
        }

        public static long NextLong()
        {
            return LongCounter.Next();
        }

        public static ulong NextULong()
        {
            return ULongCounter.Next();
        }

        public static Guid NextGuid()
        {
            return GuidCounter.Next();
        }
    }
}

snippet source / anchor

using System;
using System.Threading;

namespace XunitLogger
{
    public class GuidCounter
    {
        int current;

        public Guid Current
        {
            get => IntToGuid(current);
        }

        public Guid Next()
        {
            var value = Interlocked.Increment(ref current);
            return IntToGuid(value);
        }

        static Guid IntToGuid(int value)
        {
            var bytes = new byte[16];
            BitConverter.GetBytes(value).CopyTo(bytes, 0);
            return new Guid(bytes);
        }
    }
}

snippet source / anchor

using System.Threading;

namespace XunitLogger
{
    public class LongCounter
    {
        long current;

        public long Current
        {
            get => current;
        }

        public long Next()
        {
            return Interlocked.Increment(ref current);
        }
    }
}

snippet source / anchor

Base Class

When creating a custom base class for other tests, it is necessary to pass through the source file path to XunitLoggingBase via the constructor.

public class CustomBase :
    XunitLoggingBase
{
    public CustomBase(
        ITestOutputHelper testOutput,
        [CallerFilePath] string sourceFile = "") :
        base(testOutput, sourceFile)
    {
    }
}

snippet source / anchor

Parameters

Provided the parameters passed to the current test when using a [Theory].

Usage:

using System.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Abstractions;

public class ParametersSample :
    XunitLoggingBase
{
    [Theory]
    [MemberData(nameof(GetData))]
    public void Usage(string arg)
    {
        var parameter = Context.Parameters.Single();
        var parameterInfo = parameter.Info;
        Assert.Equal("arg", parameterInfo.Name);
        Assert.Equal(arg, parameter.Value);
    }

    public static IEnumerable<object[]> GetData()
    {
        yield return new object[] {"Value1"};
        yield return new object[] {"Value2"};
    }

    public ParametersSample(ITestOutputHelper output) :
        base(output)
    {
    }
}

snippet source / anchor

Implementation:

static List<Parameter> GetParameters(ITestCase testCase)
{
    var arguments = testCase.TestMethodArguments;
    if (arguments == null || !arguments.Any())
    {
        return empty;
    }

    var items = new List<Parameter>();

    var method = testCase.TestMethod;
    var infos = method.Method.GetParameters().ToList();
    for (var index = 0; index < infos.Count; index++)
    {
        items.Add(new Parameter(infos[index], arguments[index]));
    }

    return items;
}

snippet source / anchor

UniqueTestName

Provided a string that uniquely identifies a test case.

Usage:

using Xunit;
using Xunit.Abstractions;

public class UniqueTestNameSample :
    XunitLoggingBase
{
    [Fact]
    public void Usage()
    {
        var currentGuid = Context.UniqueTestName;

        Context.WriteLine(currentGuid);
    }

    public UniqueTestNameSample(ITestOutputHelper output) :
        base(output)
    {
    }
}

snippet source / anchor

Implementation:

string GetUniqueTestName(ITestCase testCase)
{
    var method = testCase.TestMethod;
    var name = $"{method.TestClass.Class.ClassName()}.{method.Method.Name}";
    if (!Parameters.Any())
    {
        return name;
    }

    var builder = new StringBuilder();
    foreach (var parameter in Parameters)
    {
        builder.Append($"{parameter.Info.Name}=");
        if (parameter.Value == null)
        {
            builder.Append("null_");
            continue;
        }

        builder.Append($"{parameter.Value}_");
    }

    builder.Length -= 1;

    return $"{name}_{builder}";
}

snippet source / anchor

Logging Libs

Approaches to routing common logging libraries to Diagnostics.Trace:

Xunit.ApprovalTests

The default behavior of ApprovalTests uses the StackTrace to derive the current test and hence compute the name of the approval file. This has several drawbacks/issues:

  • Fragility: Deriving the test name from a stack trace is dependent on several things to be configured correctly. Optimization must be disabled to avoid in-lining and debug symbols enabled and parsable.
  • Performance impact: Computing a stack trace is a relatively expensive operation. Disabling optimization also impacts performance

Xunit.ApprovalTests avoids these problems by using the current xUnit context to derive the approval file name.

NuGet package

https://nuget.org/packages/Xunit.ApprovalTests/

Usage

Usage is done via inheriting from a base class XunitApprovalBase

public class Sample :
    XunitApprovalBase
{
    [Fact]
    public void Simple()
    {
        Approvals.Verify("SimpleResult");
    }

    public Sample(ITestOutputHelper testOutput) :
        base(testOutput)
    {
    }

snippet source / anchor

xUnit Theory

xUnit Theories are supported.

[Theory]
[InlineData("Foo")]
[InlineData(9)]
[InlineData(true)]
public void Theory(object value)
{
    Approvals.Verify(value);
}

snippet source / anchor

Will result in the following .approved. files:

  • Sample.Theory_value=Foo.approved.txt
  • Sample.Theory_value=9.approved.txt
  • Sample.Theory_value=True.approved.txt

AsEnvironmentSpecificTest

ApprovalTests NamerFactory.AsEnvironmentSpecificTest is supported.

[Fact]
public void AsEnvironmentSpecificTest()
{
    using (NamerFactory.AsEnvironmentSpecificTest(() => "Foo"))
    {
        Approvals.Verify("Value");
    }
}

snippet source / anchor

Will result in the following .approved. file:

  • Sample.AsEnvironmentSpecificTest_Foo.approved.txt

UseApprovalSubdirectory

ApprovalTests [UseApprovalSubdirectory] is supported.

[Fact]
[UseApprovalSubdirectory("SubDir")]
public void InSubDir()
{
    Approvals.Verify("SimpleResult");
}

snippet source / anchor

Will result in the following .approved. file:

  • SubDir\Sample.InSubDir.approved.txt

ForScenario

ApprovalTests ApprovalResults.ForScenario is supported.

[Fact]
public void ForScenarioTest()
{
    using (ApprovalResults.ForScenario("Name"))
    {
        Approvals.Verify("Value");
    }
}

snippet source / anchor

Will result in the following .approved. file:

  • Sample.ForScenarioTest_ForScenario.Name.approved.txt

Base Class

When creating a custom base class for other tests, it is necessary to pass through the source file path to XunitApprovalBase via the constructor.

public class CustomBase :
    XunitApprovalBase
{
    public CustomBase(
        ITestOutputHelper testOutput,
        [CallerFilePath] string sourceFile = "") :
        base(testOutput, sourceFile)
    {
    }
}

snippet source / anchor

Release Notes

See closed milestones.

Icon

Wolverine designed by Mike Rowe from The Noun Project.

About

Extends xUnit to simplify logging

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 100.0%