Skip to content

Commit

Permalink
TUnit Support (#202)
Browse files Browse the repository at this point in the history
* TUnit Support

* Update TUnit

---------

Co-authored-by: Tom Longhurst <[email protected]>
Co-authored-by: PascalSenn <[email protected]>
  • Loading branch information
3 people authored Dec 20, 2024
1 parent 5fa1367 commit 72cf492
Show file tree
Hide file tree
Showing 37 changed files with 2,236 additions and 6 deletions.
2 changes: 2 additions & 0 deletions src/Snapshooter.TUnit/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Snapshooter.TUnit.Tests")]
25 changes: 25 additions & 0 deletions src/Snapshooter.TUnit/Snapshooter.TUnit.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(CCResourceProjectProps)" Condition="Exists('$(CCResourceProjectProps)')" />

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<AssemblyName>Snapshooter.TUnit</AssemblyName>
<RootNamespace>Snapshooter.TUnit</RootNamespace>
<PackageId>Snapshooter.TUnit</PackageId>
<Description>
TUnit Snapshooter is a flexible snapshot testing tool for .Net unit tests with TUnit.
It creates and asserts snapshots (json format) within TUnit unit tests.
</Description>
<IsTestProject>false</IsTestProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Snapshooter\Snapshooter.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="TUnit.Assertions" Version="0.3.20" />
<PackageReference Include="TUnit.Core" Version="0.3.20" />
</ItemGroup>

</Project>
397 changes: 397 additions & 0 deletions src/Snapshooter.TUnit/Snapshot.cs

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions src/Snapshooter.TUnit/SnapshotExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System;

namespace Snapshooter.TUnit
{
public static class SnapshotExtension
{
/// <summary>
/// Creates a json snapshot of the given object and compares it with the
/// already existing snapshot of the test.
/// If no snapshot exists, a new snapshot will be created from the current result
/// and saved under a certain file path, which will shown within the test message.
/// </summary>
/// <param name="currentResult">The object to match.</param>
/// <param name="matchOptions">
/// Additional compare actions, which can be applied during the snapshot comparison
/// </param>
public static void MatchSnapshot(
this object currentResult,
Func<MatchOptions, MatchOptions> matchOptions = null)
{
var cleanedObject = currentResult.RemoveUnwantedWrappers();
Snapshot.Match(cleanedObject, matchOptions);
}

/// <summary>
/// Creates a json snapshot of the given object and compares it with the
/// already existing snapshot of the test.
/// If no snapshot exists, a new snapshot will be created from the current result
/// and saved under a certain file path, which will shown within the test message.
/// </summary>
/// <param name="currentResult">The object to match.</param>
/// <param name="snapshotNameExtension">
/// The snapshot name extension will extend the generated snapshot name with
/// this given extensions. It can be used to make a snapshot name even more
/// specific.
/// Example:
/// Generated Snapshotname = 'NumberAdditionTest'
/// Snapshot name extension = '5', '6', 'Result', '11'
/// Result: 'NumberAdditionTest_5_6_Result_11'
/// </param>
/// <param name="matchOptions">
/// Additional compare actions, which can be applied during the snapshot comparison
/// </param>
public static void MatchSnapshot(
this object currentResult,
SnapshotNameExtension snapshotNameExtension,
Func<MatchOptions, MatchOptions> matchOptions = null)
{
var cleanedObject = currentResult.RemoveUnwantedWrappers();
Snapshot.Match(cleanedObject, snapshotNameExtension, matchOptions);
}

/// <summary>
/// Creates a json snapshot of the given object and compares it with the
/// already existing snapshot of the test.
/// If no snapshot exists, a new snapshot will be created from the current result
/// and saved under a certain file path, which will shown within the test message.
/// </summary>
/// <param name="currentResult">The object to match.</param>
/// <param name="snapshotName">
/// The name of the snapshot. If not set, then the snapshotname
/// will be evaluated automatically from the TUnit test name.
/// </param>
/// <param name="matchOptions">
/// Additional compare actions, which can be applied during the snapshot comparison
/// </param>
public static void MatchSnapshot(
this object currentResult,
string snapshotName,
Func<MatchOptions, MatchOptions> matchOptions = null)
{
var cleanedObject = currentResult.RemoveUnwantedWrappers();
Snapshot.Match(cleanedObject, snapshotName, matchOptions);
}

/// <summary>
/// Creates a json snapshot of the given object and compares it with the
/// already existing snapshot of the test.
/// If no snapshot exists, a new snapshot will be created from the current result
/// and saved under a certain file path, which will shown within the test message.
/// </summary>
/// <param name="currentResult">The object to match.</param>
/// <param name="snapshotName">
/// The name of the snapshot. If not set, then the snapshotname
/// will be evaluated automatically from the TUnit test name.
/// </param>
/// <param name="snapshotNameExtension">
/// The snapshot name extension will extend the generated snapshot name with
/// this given extensions. It can be used to make a snapshot name even more
/// specific.
/// Example:
/// Generated Snapshotname = 'NumberAdditionTest'
/// Snapshot name extension = '5', '6', 'Result', '11'
/// Result: 'NumberAdditionTest_5_6_Result_11'
/// </param>
/// <param name="matchOptions">
/// Additional compare actions, which can be applied during the snapshot comparison.
/// </param>
public static void MatchSnapshot(
this object currentResult,
string snapshotName,
SnapshotNameExtension snapshotNameExtension,
Func<MatchOptions, MatchOptions> matchOptions = null)
{
var cleanedObject = currentResult.RemoveUnwantedWrappers();
Snapshot.Match(cleanedObject, snapshotName, snapshotNameExtension, matchOptions);
}

/// <summary>
/// Creates a json snapshot of the given object and compares it with the
/// already existing snapshot of the test.
/// If no snapshot exists, a new snapshot will be created from the current result
/// and saved under a certain file path, which will shown within the test message.
/// </summary>
/// <param name="currentResult">The object to match.</param>
/// <param name="snapshotFullName">
/// The full name of a snapshot with folder and file name.
/// To get a SnapshotFullName use Snapshot.FullName(). </param>
/// <param name="matchOptions">
/// Additional compare actions, which can be applied during the snapshot comparison.
/// </param>
public static void MatchSnapshot(
this object currentResult,
SnapshotFullName snapshotFullName,
Func<MatchOptions, MatchOptions> matchOptions = null)
{
var cleanedObject = currentResult.RemoveUnwantedWrappers();
Snapshot.Match(cleanedObject, snapshotFullName, matchOptions);
}
}
}
26 changes: 26 additions & 0 deletions src/Snapshooter.TUnit/TUnitAssert.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Snapshooter.Core;
using TUnit.Assertions.Extensions;
using TAssert = TUnit.Assertions.Assert;

namespace Snapshooter.TUnit
{
/// <summary>
/// The NunitAssert instance compares two strings with the TUnit assert utility.
/// </summary>
public class TUnitAssert : IAssert
{
/// <summary>
/// Asserts the expected snapshot and the actual snapshot
/// with the TUnit assert utility.
/// </summary>
/// <param name="expectedSnapshot">The expected snapshot.</param>
/// <param name="actualSnapshot">The actual snapshot.</param>
public void Assert(string expectedSnapshot, string actualSnapshot)
{
// TUnit assertions use an async syntax but this interface is restricted to synchronous calls
#pragma warning disable TUnitAssertions0002
TAssert.That(actualSnapshot).IsEqualTo(expectedSnapshot).GetAwaiter().GetResult();
#pragma warning restore TUnitAssertions0002
}
}
}
108 changes: 108 additions & 0 deletions src/Snapshooter.TUnit/TUnitSnapshotFullNameReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Snapshooter.Core;
using Snapshooter.Exceptions;
using Snapshooter.Extensions;
using TUnit.Core;

namespace Snapshooter.TUnit
{
/// <summary>
/// A TUnit snapshot full name reader is responsible to get the information
/// for the snapshot file from a TUnit test.
/// </summary>
public class TUnitSnapshotFullNameReader : ISnapshotFullNameReader
{
/// <summary>
/// Evaluates the snapshot full name information.
/// </summary>
/// <returns>The full name of the snapshot.</returns>
public SnapshotFullName ReadSnapshotFullName()
{
SnapshotFullName snapshotFullName = null;
StackFrame[] stackFrames = new StackTrace(true).GetFrames();
foreach (StackFrame stackFrame in stackFrames)
{
MethodBase method = stackFrame.GetMethod();
if (IsTUnitTestMethod(method))
{
snapshotFullName = new SnapshotFullName(
GetCurrentSnapshotName(),
stackFrame.GetFileName().GetDirectoryName());

break;
}

MethodBase asyncMethod = EvaluateAsynchronousMethodBase(method);
if (IsTUnitTestMethod(asyncMethod))
{
snapshotFullName = new SnapshotFullName(
GetCurrentSnapshotName(),
stackFrame.GetFileName().GetDirectoryName());

break;
}
}

if (snapshotFullName == null)
{
throw new SnapshotTestException(
"The snapshot full name could not be evaluated. " +
"This error can occur, if you use the snapshot match " +
"within a async test helper child method. To solve this issue, " +
"use the Snapshot.FullName directly in the unit test to " +
"get the snapshot name, then reach this name to your " +
"Snapshot.Match method.");
}

snapshotFullName = LiveUnitTestingDirectoryResolver
.CheckForSession(snapshotFullName);

return snapshotFullName;
}

private static bool IsTUnitTestMethod(MemberInfo method)
{
return method?.GetCustomAttributes(typeof(TestAttribute)).Any() ?? false;
}

private static MethodBase EvaluateAsynchronousMethodBase(MemberInfo method)
{
Type methodDeclaringType = method?.DeclaringType;
Type classDeclaringType = methodDeclaringType?.DeclaringType;

MethodInfo actualMethodInfo = null;
if (classDeclaringType != null)
{
IEnumerable<MethodInfo> selectedMethodInfos =
from methodInfo in classDeclaringType.GetMethods()
let stateMachineAttribute = methodInfo
.GetCustomAttribute<AsyncStateMachineAttribute>()
where stateMachineAttribute != null &&
stateMachineAttribute.StateMachineType == methodDeclaringType
select methodInfo;

actualMethodInfo = selectedMethodInfos.SingleOrDefault();
}

return actualMethodInfo;

}

private static string GetCurrentSnapshotName()
{
TestContext currentTestContext = TestContext.Current!;

var typeName = currentTestContext.TestDetails.ClassType.Name;
var methodName = currentTestContext.TestDetails.TestName;
var parameters = SnapshotNameExtension.Create(currentTestContext.TestDetails.TestMethodArguments).ToParamsString();

return $"{typeName}.{methodName}{parameters}";
}
}
}
29 changes: 29 additions & 0 deletions src/Snapshooter.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snapshooter.Tests.Data", ".
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snapshooter.Xunit.Tests", "..\test\Snapshooter.Xunit.Tests\Snapshooter.Xunit.Tests.csproj", "{3C7A875E-7B9C-45E6-93E1-E952F08758B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snapshooter.TUnit", "Snapshooter.TUnit\Snapshooter.TUnit.csproj", "{A17E038B-5283-4784-A302-BC3D64E5764A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snapshooter.TUnit.Tests", "..\test\Snapshooter.TUnit.Tests\Snapshooter.TUnit.Tests.csproj", "{8B65FDBB-A430-406E-8992-1B4474D99358}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -198,6 +202,30 @@ Global
{3C7A875E-7B9C-45E6-93E1-E952F08758B4}.Release|x64.Build.0 = Release|Any CPU
{3C7A875E-7B9C-45E6-93E1-E952F08758B4}.Release|x86.ActiveCfg = Release|Any CPU
{3C7A875E-7B9C-45E6-93E1-E952F08758B4}.Release|x86.Build.0 = Release|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|x64.ActiveCfg = Debug|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|x64.Build.0 = Debug|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|x86.ActiveCfg = Debug|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|x86.Build.0 = Debug|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|Any CPU.Build.0 = Release|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|x64.ActiveCfg = Release|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|x64.Build.0 = Release|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|x86.ActiveCfg = Release|Any CPU
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|x86.Build.0 = Release|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|x64.ActiveCfg = Debug|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|x64.Build.0 = Debug|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|x86.ActiveCfg = Debug|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|x86.Build.0 = Debug|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|Any CPU.Build.0 = Release|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|x64.ActiveCfg = Release|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|x64.Build.0 = Release|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|x86.ActiveCfg = Release|Any CPU
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -211,6 +239,7 @@ Global
{616F100E-A562-4E17-805A-8755B9D4D1AA} = {F9DFF684-4ACF-45E4-B23E-E8928DE0C9FE}
{A9A09C8D-E9D1-45CC-80F1-3C8DDF8F2600} = {F9DFF684-4ACF-45E4-B23E-E8928DE0C9FE}
{3C7A875E-7B9C-45E6-93E1-E952F08758B4} = {F9DFF684-4ACF-45E4-B23E-E8928DE0C9FE}
{8B65FDBB-A430-406E-8992-1B4474D99358} = {F9DFF684-4ACF-45E4-B23E-E8928DE0C9FE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2F64A2AB-ACA2-4E2D-B7E2-B87E93C66A24}
Expand Down
8 changes: 8 additions & 0 deletions test/Snapshooter.Json.Tests/Snapshooter.Json.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
<ProjectReference Include="..\Snapshooter.Tests.Data\Snapshooter.Tests.Data.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<None Update="**\__snapshots__\*.snap">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
Expand Down
21 changes: 21 additions & 0 deletions test/Snapshooter.TUnit.Tests/Snapshooter.TUnit.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(CCTestProjectProps)" Condition="Exists('$(CCTestProjectProps)')" />

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<AssemblyName>Snapshooter.TUnit.Tests</AssemblyName>
<RootNamespace>Snapshooter.TUnit.Tests</RootNamespace>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Snapshooter.TUnit\Snapshooter.TUnit.csproj" />
<ProjectReference Include="..\Snapshooter.Tests.Data\Snapshooter.Tests.Data.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="TUnit" Version="0.3.20" />
</ItemGroup>

</Project>
Loading

0 comments on commit 72cf492

Please sign in to comment.