Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/simoncropp/xunitcontext
Extends xUnit to expose extra context and simplify logging
https://github.com/simoncropp/xunitcontext
itestoutputhelper xunit
Last synced: 3 days ago
JSON representation
Extends xUnit to expose extra context and simplify logging
- Host: GitHub
- URL: https://github.com/simoncropp/xunitcontext
- Owner: SimonCropp
- License: mit
- Created: 2019-04-10T08:41:04.000Z (over 5 years ago)
- Default Branch: main
- Last Pushed: 2024-10-28T16:31:55.000Z (18 days ago)
- Last Synced: 2024-10-29T17:19:53.840Z (17 days ago)
- Topics: itestoutputhelper, xunit
- Language: C#
- Homepage:
- Size: 1.07 MB
- Stars: 162
- Watchers: 8
- Forks: 16
- Open Issues: 1
-
Metadata Files:
- Readme: readme.md
- Funding: .github/FUNDING.yml
- License: license.txt
- Code of conduct: code_of_conduct.md
Awesome Lists containing this project
README
# XunitContext
[![Build status](https://ci.appveyor.com/api/projects/status/sdg2ni2jhe2o33le/branch/main?svg=true)](https://ci.appveyor.com/project/SimonCropp/XunitContext)
[![NuGet Status](https://img.shields.io/nuget/v/XunitContext.svg)](https://www.nuget.org/packages/XunitContext/)Extends [xUnit](https://xunit.net/) to expose extra context and simplify logging.
Redirects [Trace.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.trace.write), [Debug.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.write), and [Console.Write and Console.Error.Write](https://docs.microsoft.com/en-us/dotnet/api/system.console.write) to [ITestOutputHelper](https://xunit.net/docs/capturing-output). Also provides static access to the current [ITestOutputHelper](https://xunit.net/docs/capturing-output) for use within testing utility methods.
Uses [AsyncLocal](https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1) to track state.
**See [Milestones](../../milestones?state=closed) for release notes.**
## NuGet package
https://nuget.org/packages/XunitContext/
## ClassBeingTested
```cs
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## XunitContextBase
`XunitContextBase` is an abstract base class for tests. It exposes logging methods for use from unit tests, and handle the flushing of logs in its `Dispose` method. `XunitContextBase` is actually a thin wrapper over `XunitContext`. `XunitContext`s `Write*` methods can also be use inside a test inheriting from `XunitContextBase`.
```cs
public class TestBaseSample(ITestOutputHelper output) :
XunitContextBase(output)
{
[Fact]
public void Write_lines()
{
WriteLine("From Test");
ClassBeingTested.Method();var logs = XunitContext.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);
}
}
```
snippet source | anchor## xunit Fixture
In addition to `XunitContextBase` class approach, one is also possible to use `IContextFixture` to gain access to `XunitContext` :
```cs
public class FixtureSample(ITestOutputHelper helper, ContextFixture ctxFixture) :
IContextFixture
{
Context context = ctxFixture.Start(helper);[Fact]
public void Usage()
{
Console.WriteLine("From Test");
Assert.Contains("From Test", context.LogMessages);
}
}
```
snippet source | anchor## Logging
`XunitContext` provides static access to the logging state for tests. It exposes logging methods for use from unit tests, however registration of [ITestOutputHelper](https://xunit.net/docs/capturing-output) and flushing of logs must be handled explicitly.
```cs
public class XunitLoggerSample :
IDisposable
{
[Fact]
public void Usage()
{
XunitContext.WriteLine("From Test");ClassBeingTested.Method();
var logs = XunitContext.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) =>
XunitContext.Register(testOutput);public void Dispose() =>
XunitContext.Flush();
}
```
snippet source | anchor`XunitContext` redirects [Trace.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.trace.write), [Console.Write](https://docs.microsoft.com/en-us/dotnet/api/system.console.write), and [Debug.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.write) in its static constructor.
```cs
Trace.Listeners.Clear();
Trace.Listeners.Add(new TraceListener());
#if (NETFRAMEWORK)
Debug.Listeners.Clear();
Debug.Listeners.Add(new TraceListener());
#else
DebugPoker.Overwrite(
text =>
{
if (string.IsNullOrEmpty(text))
{
return;
}if (text.EndsWith(Environment.NewLine))
{
WriteLine(text.TrimTrailingNewline());
return;
}Write(text);
});
#endif
TestWriter writer = new();
Console.SetOut(writer);
Console.SetError(writer);
```
snippet source | anchorThese API calls are then routed to the correct xUnit [ITestOutputHelper](https://xunit.net/docs/capturing-output) via a static [AsyncLocal](https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1).
### Logging Libs
Approaches to routing common logging libraries to Diagnostics.Trace:
* [Serilog](https://serilog.net/) use [Serilog.Sinks.Trace](https://github.com/serilog/serilog-sinks-trace).
* [NLog](https://github.com/NLog/NLog) use a [Trace target](https://github.com/NLog/NLog/wiki/Trace-target).## Filters
`XunitContext.Filters` can be used to filter out unwanted lines:
```cs
public class FilterSample(ITestOutputHelper output) :
XunitContextBase(output)
{
static FilterSample() =>
Filters.Add(_ => _ != null && !_.Contains("ignored"));[Fact]
public void Write_lines()
{
WriteLine("first");
WriteLine("with ignored string");
WriteLine("last");
var logs = XunitContext.Logs;Assert.Contains("first", logs);
Assert.DoesNotContain("with ignored string", logs);
Assert.Contains("last", logs);
}
}
```
snippet source | anchorFilters 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](https://xunit.net/docs/capturing-output).
* `Context.Write` and `Context.WriteLine`: Write to the current log.
* `Context.LogMessages`: Access to all log message for the current test.
* [Counters](#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`.
* `Context.SourceFile`: Access to the file path for the current test.
* `Context.SourceDirectory`: Access to the directory path for the current test.
* `Context.SolutionDirectory`: The current solution directory. Obtained by walking up the directory tree from `SourceDirectory`.
* `Context.TestException`: Access to the exception if the current test has failed. See [Test Failure](test-failure).
```cs
// ReSharper disable UnusedVariablepublic class ContextSample(ITestOutputHelper output) :
XunitContextBase(output)
{
[Fact]
public void Usage()
{
Context.WriteLine("Some message");var currentLogMessages = Context.LogMessages;
var testOutputHelper = Context.TestOutput;
var currentTest = Context.Test;
var sourceFile = Context.SourceFile;
var sourceDirectory = Context.SourceDirectory;
var solutionDirectory = Context.SolutionDirectory;
var currentTestException = Context.TestException;
}
}
```
snippet source | anchorSome members are pushed down to the be accessible directly from `XunitContextBase`:
```cs
// ReSharper disable UnusedVariablepublic class ContextPushedDownSample(ITestOutputHelper output) :
XunitContextBase(output)
{
[Fact]
public void Usage()
{
WriteLine("Some message");var currentLogMessages = Logs;
var testOutputHelper = Output;
var sourceFile = SourceFile;
var sourceDirectory = SourceDirectory;
var solutionDirectory = SolutionDirectory;
var currentTestException = TestException;
}
}
```
snippet source | anchorContext can accessed via a static API:
```cs
// ReSharper disable UnusedVariablepublic class ContextStaticSample(ITestOutputHelper output) :
XunitContextBase(output)
{
[Fact]
public void StaticUsage()
{
XunitContext.Context.WriteLine("Some message");var currentLogMessages = XunitContext.Context.LogMessages;
var testOutputHelper = XunitContext.Context.TestOutput;
var currentTest = XunitContext.Context.Test;
var sourceFile = XunitContext.Context.SourceFile;
var sourceDirectory = XunitContext.Context.SourceDirectory;
var solutionDirectory = XunitContext.Context.SolutionDirectory;
var currentTestException = XunitContext.Context.TestException;
}
}
```
snippet source | anchor### Current Test
There is currently no API in xUnit to retrieve information on the current test. See issues [#1359](https://github.com/xunit/xunit/issues/1359), [#416](https://github.com/xunit/xunit/issues/416), and [#398](https://github.com/xunit/xunit/issues/398).
To work around this, this project exposes the current instance of `ITest` via reflection.
Usage:
```cs
// ReSharper disable UnusedVariablepublic class CurrentTestSample(ITestOutputHelper output) :
XunitContextBase(output)
{
[Fact]
public void Usage()
{
var currentTest = Context.Test;
// DisplayName will be 'CurrentTestSample.Usage'
var displayName = currentTest.DisplayName;
}[Fact]
public void StaticUsage()
{
var currentTest = XunitContext.Context.Test;
// DisplayName will be 'CurrentTestSample.StaticUsage'
var displayName = currentTest.DisplayName;
}
}
```
snippet source | anchorImplementation:
public partial class Context
{
ITest? test;public ITest Test
{
get
{
InitTest();return test!;
}
}MethodInfo? methodInfo;
public MethodInfo MethodInfo
{
get
{
InitTest();
return methodInfo!;
}
}Type? testType;
public Type TestType
{
get
{
InitTest();
return testType!;
}
}void InitTest()
{
if (test != null)
{
return;
}if (TestOutput == null)
{
throw new(MissingTestOutput);
}#if NET8_0_OR_GREATER
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "test")]
static extern ref ITest GetTest(TestOutputHelper? c);
test = GetTest((TestOutputHelper) TestOutput);
#else
test = (ITest) GetTestMethod(TestOutput)
.GetValue(TestOutput)!;
#endif
var method = (ReflectionMethodInfo) test.TestCase.TestMethod.Method;
var type = (ReflectionTypeInfo) test.TestCase.TestMethod.TestClass.Class;
methodInfo = method.MethodInfo;
testType = type.Type;
}public const string MissingTestOutput = "ITestOutputHelper has not been set. It is possible that the call to `XunitContext.Register()` is missing, or the current test does not inherit from `XunitContextBase`.";
#if !NET8_0_OR_GREATER
static FieldInfo? cachedTestMember;static FieldInfo GetTestMethod(ITestOutputHelper testOutput)
{
if (cachedTestMember != null)
{
return cachedTestMember;
}var testOutputType = testOutput.GetType();
cachedTestMember = testOutputType.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic);
if (cachedTestMember == null)
{
throw new($"Unable to find 'test' field on {testOutputType.FullName}");
}return cachedTestMember;
}
#endif
}
```
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.
```cs
// ReSharper disable UnusedVariable
public static class GlobalSetup
{
[ModuleInitializer]
public static void Setup() =>
XunitContext.EnableExceptionCapture();
}public class TestExceptionSample(ITestOutputHelper output) :
XunitContextBase(output)
{
[Fact(Skip = "Will fail")]
public void Usage() =>
//This tests will fail
Assert.False(true);public override void Dispose()
{
var theExceptionThrownByTest = Context.TestException;
var testDisplayName = Context.Test.DisplayName;
var testCase = Context.Test.TestCase;
base.Dispose();
}
}
```
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 `XunitContextBase` via the constructor.
```cs
public class CustomBase(
ITestOutputHelper testOutput,
[CallerFilePath] string sourceFile = "")
:
XunitContextBase(testOutput, sourceFile);
```
snippet source | anchor### Parameters
Provided the parameters passed to the current test when using a `[Theory]`.
Use cases:
* To derive the [unique test name](#uniquetestname).
* In extensibility scenarios for example [Verify file naming](https://github.com/SimonCropp/Verify/blob/master/docs/naming.md).Usage:
```cs
public class ParametersSample(ITestOutputHelper output) :
XunitContextBase(output)
{
[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 GetData()
{
yield return ["Value1"];
yield return ["Value2"];
}
}
```
snippet source | anchorImplementation:
```cs
static List GetParameters(ITestCase testCase) =>
GetParameters(testCase, testCase.TestMethodArguments);static List GetParameters(ITestCase testCase, object[] arguments)
{
var method = testCase.TestMethod;
var infos = method
.Method.GetParameters()
.ToList();
if (arguments == null || arguments.Length == 0)
{
if (infos.Count == 0)
{
return empty;
}throw NewNoArgumentsDetectedException();
}List items = [];
for (var index = 0; index < infos.Count; index++)
{
items.Add(new(infos[index], arguments[index]));
}return items;
}
```
snippet source | anchor#### Complex parameters
Only core types (string, int, DateTime etc) can use the above automated approach. If a complex type is used the following exception will be thrown
> No arguments detected for method with parameters.
> This is most likely caused by using a parameter that Xunit cannot serialize.
> Instead pass in a simple type as a parameter and construct the complex object inside the test.
> Alternatively; override the current parameters using `UseParameters()` via the current test base class, or via `XunitContext.Current.UseParameters()`.To use complex types override the parameter resolution using `XunitContextBase.UseParameters`:
```cs
public class ComplexParameterSample(ITestOutputHelper output) :
XunitContextBase(output)
{
[Theory]
[MemberData(nameof(GetData))]
public void UseComplexMemberData(ComplexClass arg)
{
UseParameters(arg);
var parameter = Context.Parameters.Single();
var parameterInfo = parameter.Info;
Assert.Equal("arg", parameterInfo.Name);
Assert.Equal(arg, parameter.Value);
}public static IEnumerable GetData()
{
yield return [new ComplexClass("Value1")];
yield return [new ComplexClass("Value2")];
}public class ComplexClass(string value)
{
public string Value { get; } = value;
}
}
```
snippet source | anchor### UniqueTestName
Provided a string that uniquely identifies a test case.
Usage:
```cs
public class UniqueTestNameSample(ITestOutputHelper output) :
XunitContextBase(output)
{
[Fact]
public void Usage()
{
var testName = Context.UniqueTestName;Context.WriteLine(testName);
}
}
```
snippet source | anchorImplementation:
```cs
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($"{name}_");
foreach (var parameter in Parameters)
{
builder.Append($"{parameter.Info.Name}=");
builder.Append(string.Join(",", SplitParams(parameter.Value)));
builder.Append('_');
}builder.Length -= 1;
return builder.ToString();
}static IEnumerable SplitParams(object? parameter)
{
if (parameter == null)
{
yield return "null";
yield break;
}if (parameter is string stringValue)
{
yield return stringValue;
yield break;
}if (parameter is IEnumerable enumerable)
{
foreach (var item in enumerable)
{
foreach (var sub in SplitParams(item))
{
yield return sub;
}
}yield break;
}var toString = parameter.ToString();
if (toString == null)
{
yield return "null";
}
else
{
yield return toString;
}
}
```
snippet source | anchor## Global Setup
Xunit has no way to run code once before any tests executing. So use one of the following:
* [C# 9 Module Initializer](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/module-initializers).
* [Fody Module Initializer](https://github.com/Fody/ModuleInit).
* Having a single base class that all tests inherit from, and place any configuration code in the static constructor of that type.## Icon
[Wolverine](https://thenounproject.com/term/wolverine/18415/) designed by [Mike Rowe](https://thenounproject.com/itsmikerowe/) from [The Noun Project](https://thenounproject.com/).