Skip to content

Commit 72cf492

Browse files
thomhurstmsm-tomlonghurstPascalSenn
authored
TUnit Support (#202)
* TUnit Support * Update TUnit --------- Co-authored-by: Tom Longhurst <[email protected]> Co-authored-by: PascalSenn <[email protected]>
1 parent 5fa1367 commit 72cf492

File tree

37 files changed

+2236
-6
lines changed

37 files changed

+2236
-6
lines changed

src/Snapshooter.TUnit/AssemblyInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
using System.Runtime.CompilerServices;
2+
[assembly: InternalsVisibleTo("Snapshooter.TUnit.Tests")]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="$(CCResourceProjectProps)" Condition="Exists('$(CCResourceProjectProps)')" />
3+
4+
<PropertyGroup>
5+
<TargetFrameworks>net8.0</TargetFrameworks>
6+
<AssemblyName>Snapshooter.TUnit</AssemblyName>
7+
<RootNamespace>Snapshooter.TUnit</RootNamespace>
8+
<PackageId>Snapshooter.TUnit</PackageId>
9+
<Description>
10+
TUnit Snapshooter is a flexible snapshot testing tool for .Net unit tests with TUnit.
11+
It creates and asserts snapshots (json format) within TUnit unit tests.
12+
</Description>
13+
<IsTestProject>false</IsTestProject>
14+
</PropertyGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\Snapshooter\Snapshooter.csproj" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<PackageReference Include="TUnit.Assertions" Version="0.3.20" />
22+
<PackageReference Include="TUnit.Core" Version="0.3.20" />
23+
</ItemGroup>
24+
25+
</Project>

src/Snapshooter.TUnit/Snapshot.cs

Lines changed: 397 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System;
2+
3+
namespace Snapshooter.TUnit
4+
{
5+
public static class SnapshotExtension
6+
{
7+
/// <summary>
8+
/// Creates a json snapshot of the given object and compares it with the
9+
/// already existing snapshot of the test.
10+
/// If no snapshot exists, a new snapshot will be created from the current result
11+
/// and saved under a certain file path, which will shown within the test message.
12+
/// </summary>
13+
/// <param name="currentResult">The object to match.</param>
14+
/// <param name="matchOptions">
15+
/// Additional compare actions, which can be applied during the snapshot comparison
16+
/// </param>
17+
public static void MatchSnapshot(
18+
this object currentResult,
19+
Func<MatchOptions, MatchOptions> matchOptions = null)
20+
{
21+
var cleanedObject = currentResult.RemoveUnwantedWrappers();
22+
Snapshot.Match(cleanedObject, matchOptions);
23+
}
24+
25+
/// <summary>
26+
/// Creates a json snapshot of the given object and compares it with the
27+
/// already existing snapshot of the test.
28+
/// If no snapshot exists, a new snapshot will be created from the current result
29+
/// and saved under a certain file path, which will shown within the test message.
30+
/// </summary>
31+
/// <param name="currentResult">The object to match.</param>
32+
/// <param name="snapshotNameExtension">
33+
/// The snapshot name extension will extend the generated snapshot name with
34+
/// this given extensions. It can be used to make a snapshot name even more
35+
/// specific.
36+
/// Example:
37+
/// Generated Snapshotname = 'NumberAdditionTest'
38+
/// Snapshot name extension = '5', '6', 'Result', '11'
39+
/// Result: 'NumberAdditionTest_5_6_Result_11'
40+
/// </param>
41+
/// <param name="matchOptions">
42+
/// Additional compare actions, which can be applied during the snapshot comparison
43+
/// </param>
44+
public static void MatchSnapshot(
45+
this object currentResult,
46+
SnapshotNameExtension snapshotNameExtension,
47+
Func<MatchOptions, MatchOptions> matchOptions = null)
48+
{
49+
var cleanedObject = currentResult.RemoveUnwantedWrappers();
50+
Snapshot.Match(cleanedObject, snapshotNameExtension, matchOptions);
51+
}
52+
53+
/// <summary>
54+
/// Creates a json snapshot of the given object and compares it with the
55+
/// already existing snapshot of the test.
56+
/// If no snapshot exists, a new snapshot will be created from the current result
57+
/// and saved under a certain file path, which will shown within the test message.
58+
/// </summary>
59+
/// <param name="currentResult">The object to match.</param>
60+
/// <param name="snapshotName">
61+
/// The name of the snapshot. If not set, then the snapshotname
62+
/// will be evaluated automatically from the TUnit test name.
63+
/// </param>
64+
/// <param name="matchOptions">
65+
/// Additional compare actions, which can be applied during the snapshot comparison
66+
/// </param>
67+
public static void MatchSnapshot(
68+
this object currentResult,
69+
string snapshotName,
70+
Func<MatchOptions, MatchOptions> matchOptions = null)
71+
{
72+
var cleanedObject = currentResult.RemoveUnwantedWrappers();
73+
Snapshot.Match(cleanedObject, snapshotName, matchOptions);
74+
}
75+
76+
/// <summary>
77+
/// Creates a json snapshot of the given object and compares it with the
78+
/// already existing snapshot of the test.
79+
/// If no snapshot exists, a new snapshot will be created from the current result
80+
/// and saved under a certain file path, which will shown within the test message.
81+
/// </summary>
82+
/// <param name="currentResult">The object to match.</param>
83+
/// <param name="snapshotName">
84+
/// The name of the snapshot. If not set, then the snapshotname
85+
/// will be evaluated automatically from the TUnit test name.
86+
/// </param>
87+
/// <param name="snapshotNameExtension">
88+
/// The snapshot name extension will extend the generated snapshot name with
89+
/// this given extensions. It can be used to make a snapshot name even more
90+
/// specific.
91+
/// Example:
92+
/// Generated Snapshotname = 'NumberAdditionTest'
93+
/// Snapshot name extension = '5', '6', 'Result', '11'
94+
/// Result: 'NumberAdditionTest_5_6_Result_11'
95+
/// </param>
96+
/// <param name="matchOptions">
97+
/// Additional compare actions, which can be applied during the snapshot comparison.
98+
/// </param>
99+
public static void MatchSnapshot(
100+
this object currentResult,
101+
string snapshotName,
102+
SnapshotNameExtension snapshotNameExtension,
103+
Func<MatchOptions, MatchOptions> matchOptions = null)
104+
{
105+
var cleanedObject = currentResult.RemoveUnwantedWrappers();
106+
Snapshot.Match(cleanedObject, snapshotName, snapshotNameExtension, matchOptions);
107+
}
108+
109+
/// <summary>
110+
/// Creates a json snapshot of the given object and compares it with the
111+
/// already existing snapshot of the test.
112+
/// If no snapshot exists, a new snapshot will be created from the current result
113+
/// and saved under a certain file path, which will shown within the test message.
114+
/// </summary>
115+
/// <param name="currentResult">The object to match.</param>
116+
/// <param name="snapshotFullName">
117+
/// The full name of a snapshot with folder and file name.
118+
/// To get a SnapshotFullName use Snapshot.FullName(). </param>
119+
/// <param name="matchOptions">
120+
/// Additional compare actions, which can be applied during the snapshot comparison.
121+
/// </param>
122+
public static void MatchSnapshot(
123+
this object currentResult,
124+
SnapshotFullName snapshotFullName,
125+
Func<MatchOptions, MatchOptions> matchOptions = null)
126+
{
127+
var cleanedObject = currentResult.RemoveUnwantedWrappers();
128+
Snapshot.Match(cleanedObject, snapshotFullName, matchOptions);
129+
}
130+
}
131+
}

src/Snapshooter.TUnit/TUnitAssert.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Snapshooter.Core;
2+
using TUnit.Assertions.Extensions;
3+
using TAssert = TUnit.Assertions.Assert;
4+
5+
namespace Snapshooter.TUnit
6+
{
7+
/// <summary>
8+
/// The NunitAssert instance compares two strings with the TUnit assert utility.
9+
/// </summary>
10+
public class TUnitAssert : IAssert
11+
{
12+
/// <summary>
13+
/// Asserts the expected snapshot and the actual snapshot
14+
/// with the TUnit assert utility.
15+
/// </summary>
16+
/// <param name="expectedSnapshot">The expected snapshot.</param>
17+
/// <param name="actualSnapshot">The actual snapshot.</param>
18+
public void Assert(string expectedSnapshot, string actualSnapshot)
19+
{
20+
// TUnit assertions use an async syntax but this interface is restricted to synchronous calls
21+
#pragma warning disable TUnitAssertions0002
22+
TAssert.That(actualSnapshot).IsEqualTo(expectedSnapshot).GetAwaiter().GetResult();
23+
#pragma warning restore TUnitAssertions0002
24+
}
25+
}
26+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Diagnostics;
5+
using System.Linq;
6+
using System.Reflection;
7+
using System.Runtime.CompilerServices;
8+
using Snapshooter.Core;
9+
using Snapshooter.Exceptions;
10+
using Snapshooter.Extensions;
11+
using TUnit.Core;
12+
13+
namespace Snapshooter.TUnit
14+
{
15+
/// <summary>
16+
/// A TUnit snapshot full name reader is responsible to get the information
17+
/// for the snapshot file from a TUnit test.
18+
/// </summary>
19+
public class TUnitSnapshotFullNameReader : ISnapshotFullNameReader
20+
{
21+
/// <summary>
22+
/// Evaluates the snapshot full name information.
23+
/// </summary>
24+
/// <returns>The full name of the snapshot.</returns>
25+
public SnapshotFullName ReadSnapshotFullName()
26+
{
27+
SnapshotFullName snapshotFullName = null;
28+
StackFrame[] stackFrames = new StackTrace(true).GetFrames();
29+
foreach (StackFrame stackFrame in stackFrames)
30+
{
31+
MethodBase method = stackFrame.GetMethod();
32+
if (IsTUnitTestMethod(method))
33+
{
34+
snapshotFullName = new SnapshotFullName(
35+
GetCurrentSnapshotName(),
36+
stackFrame.GetFileName().GetDirectoryName());
37+
38+
break;
39+
}
40+
41+
MethodBase asyncMethod = EvaluateAsynchronousMethodBase(method);
42+
if (IsTUnitTestMethod(asyncMethod))
43+
{
44+
snapshotFullName = new SnapshotFullName(
45+
GetCurrentSnapshotName(),
46+
stackFrame.GetFileName().GetDirectoryName());
47+
48+
break;
49+
}
50+
}
51+
52+
if (snapshotFullName == null)
53+
{
54+
throw new SnapshotTestException(
55+
"The snapshot full name could not be evaluated. " +
56+
"This error can occur, if you use the snapshot match " +
57+
"within a async test helper child method. To solve this issue, " +
58+
"use the Snapshot.FullName directly in the unit test to " +
59+
"get the snapshot name, then reach this name to your " +
60+
"Snapshot.Match method.");
61+
}
62+
63+
snapshotFullName = LiveUnitTestingDirectoryResolver
64+
.CheckForSession(snapshotFullName);
65+
66+
return snapshotFullName;
67+
}
68+
69+
private static bool IsTUnitTestMethod(MemberInfo method)
70+
{
71+
return method?.GetCustomAttributes(typeof(TestAttribute)).Any() ?? false;
72+
}
73+
74+
private static MethodBase EvaluateAsynchronousMethodBase(MemberInfo method)
75+
{
76+
Type methodDeclaringType = method?.DeclaringType;
77+
Type classDeclaringType = methodDeclaringType?.DeclaringType;
78+
79+
MethodInfo actualMethodInfo = null;
80+
if (classDeclaringType != null)
81+
{
82+
IEnumerable<MethodInfo> selectedMethodInfos =
83+
from methodInfo in classDeclaringType.GetMethods()
84+
let stateMachineAttribute = methodInfo
85+
.GetCustomAttribute<AsyncStateMachineAttribute>()
86+
where stateMachineAttribute != null &&
87+
stateMachineAttribute.StateMachineType == methodDeclaringType
88+
select methodInfo;
89+
90+
actualMethodInfo = selectedMethodInfos.SingleOrDefault();
91+
}
92+
93+
return actualMethodInfo;
94+
95+
}
96+
97+
private static string GetCurrentSnapshotName()
98+
{
99+
TestContext currentTestContext = TestContext.Current!;
100+
101+
var typeName = currentTestContext.TestDetails.ClassType.Name;
102+
var methodName = currentTestContext.TestDetails.TestName;
103+
var parameters = SnapshotNameExtension.Create(currentTestContext.TestDetails.TestMethodArguments).ToParamsString();
104+
105+
return $"{typeName}.{methodName}{parameters}";
106+
}
107+
}
108+
}

src/Snapshooter.sln

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snapshooter.Tests.Data", ".
3232
EndProject
3333
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snapshooter.Xunit.Tests", "..\test\Snapshooter.Xunit.Tests\Snapshooter.Xunit.Tests.csproj", "{3C7A875E-7B9C-45E6-93E1-E952F08758B4}"
3434
EndProject
35+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snapshooter.TUnit", "Snapshooter.TUnit\Snapshooter.TUnit.csproj", "{A17E038B-5283-4784-A302-BC3D64E5764A}"
36+
EndProject
37+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snapshooter.TUnit.Tests", "..\test\Snapshooter.TUnit.Tests\Snapshooter.TUnit.Tests.csproj", "{8B65FDBB-A430-406E-8992-1B4474D99358}"
38+
EndProject
3539
Global
3640
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3741
Debug|Any CPU = Debug|Any CPU
@@ -198,6 +202,30 @@ Global
198202
{3C7A875E-7B9C-45E6-93E1-E952F08758B4}.Release|x64.Build.0 = Release|Any CPU
199203
{3C7A875E-7B9C-45E6-93E1-E952F08758B4}.Release|x86.ActiveCfg = Release|Any CPU
200204
{3C7A875E-7B9C-45E6-93E1-E952F08758B4}.Release|x86.Build.0 = Release|Any CPU
205+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
206+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|Any CPU.Build.0 = Debug|Any CPU
207+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|x64.ActiveCfg = Debug|Any CPU
208+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|x64.Build.0 = Debug|Any CPU
209+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|x86.ActiveCfg = Debug|Any CPU
210+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Debug|x86.Build.0 = Debug|Any CPU
211+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|Any CPU.ActiveCfg = Release|Any CPU
212+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|Any CPU.Build.0 = Release|Any CPU
213+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|x64.ActiveCfg = Release|Any CPU
214+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|x64.Build.0 = Release|Any CPU
215+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|x86.ActiveCfg = Release|Any CPU
216+
{A17E038B-5283-4784-A302-BC3D64E5764A}.Release|x86.Build.0 = Release|Any CPU
217+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
218+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|Any CPU.Build.0 = Debug|Any CPU
219+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|x64.ActiveCfg = Debug|Any CPU
220+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|x64.Build.0 = Debug|Any CPU
221+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|x86.ActiveCfg = Debug|Any CPU
222+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Debug|x86.Build.0 = Debug|Any CPU
223+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|Any CPU.ActiveCfg = Release|Any CPU
224+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|Any CPU.Build.0 = Release|Any CPU
225+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|x64.ActiveCfg = Release|Any CPU
226+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|x64.Build.0 = Release|Any CPU
227+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|x86.ActiveCfg = Release|Any CPU
228+
{8B65FDBB-A430-406E-8992-1B4474D99358}.Release|x86.Build.0 = Release|Any CPU
201229
EndGlobalSection
202230
GlobalSection(SolutionProperties) = preSolution
203231
HideSolutionNode = FALSE
@@ -211,6 +239,7 @@ Global
211239
{616F100E-A562-4E17-805A-8755B9D4D1AA} = {F9DFF684-4ACF-45E4-B23E-E8928DE0C9FE}
212240
{A9A09C8D-E9D1-45CC-80F1-3C8DDF8F2600} = {F9DFF684-4ACF-45E4-B23E-E8928DE0C9FE}
213241
{3C7A875E-7B9C-45E6-93E1-E952F08758B4} = {F9DFF684-4ACF-45E4-B23E-E8928DE0C9FE}
242+
{8B65FDBB-A430-406E-8992-1B4474D99358} = {F9DFF684-4ACF-45E4-B23E-E8928DE0C9FE}
214243
EndGlobalSection
215244
GlobalSection(ExtensibilityGlobals) = postSolution
216245
SolutionGuid = {2F64A2AB-ACA2-4E2D-B7E2-B87E93C66A24}

test/Snapshooter.Json.Tests/Snapshooter.Json.Tests.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212
<ProjectReference Include="..\Snapshooter.Tests.Data\Snapshooter.Tests.Data.csproj" />
1313
</ItemGroup>
1414

15+
<ItemGroup>
16+
<PackageReference Include="xunit" Version="2.4.2" />
17+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
18+
<PrivateAssets>all</PrivateAssets>
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
</PackageReference>
21+
</ItemGroup>
22+
1523
<ItemGroup>
1624
<None Update="**\__snapshots__\*.snap">
1725
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="$(CCTestProjectProps)" Condition="Exists('$(CCTestProjectProps)')" />
3+
4+
<PropertyGroup>
5+
<TargetFrameworks>net8.0</TargetFrameworks>
6+
<AssemblyName>Snapshooter.TUnit.Tests</AssemblyName>
7+
<RootNamespace>Snapshooter.TUnit.Tests</RootNamespace>
8+
<IsTestProject>true</IsTestProject>
9+
<LangVersion>latest</LangVersion>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\..\src\Snapshooter.TUnit\Snapshooter.TUnit.csproj" />
14+
<ProjectReference Include="..\Snapshooter.Tests.Data\Snapshooter.Tests.Data.csproj" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="TUnit" Version="0.3.20" />
19+
</ItemGroup>
20+
21+
</Project>

0 commit comments

Comments
 (0)