Skip to content

Commit 3e00296

Browse files
authored
Add support for xUnit.net v3 (fluentassertions#2718)
1 parent 7a4cc2a commit 3e00296

24 files changed

Lines changed: 192 additions & 89 deletions

Build/Build.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,9 @@ class Build : NukeBuild
244244
Solution.TestFrameworks.MSTestV2_Specs,
245245
Solution.TestFrameworks.NUnit3_Specs,
246246
Solution.TestFrameworks.NUnit4_Specs,
247-
Solution.TestFrameworks.XUnit2_Specs
247+
Solution.TestFrameworks.XUnit2_Specs,
248+
Solution.TestFrameworks.XUnit3_Specs,
249+
Solution.TestFrameworks.XUnit3Core_Specs,
248250
];
249251

250252
var testCombinations =

FluentAssertions.sln

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Extensibil
5959
EndProject
6060
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TUnit.Specs", "Tests\TestFrameworks\TUnit.Specs\TUnit.Specs.csproj", "{6540564E-9B6E-4E1E-8881-6F0DD0F35576}"
6161
EndProject
62+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnit3.Specs", "Tests\TestFrameworks\XUnit3.Specs\XUnit3.Specs.csproj", "{AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}"
63+
EndProject
64+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnit3Core.Specs", "Tests\TestFrameworks\XUnit3Core.Specs\XUnit3Core.Specs.csproj", "{77BB9496-169D-43DA-BCED-7DB3ACD9179A}"
65+
EndProject
6266
Global
6367
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6468
CI|Any CPU = CI|Any CPU
@@ -173,6 +177,18 @@ Global
173177
{6540564E-9B6E-4E1E-8881-6F0DD0F35576}.Debug|Any CPU.Build.0 = Debug|Any CPU
174178
{6540564E-9B6E-4E1E-8881-6F0DD0F35576}.Release|Any CPU.ActiveCfg = Release|Any CPU
175179
{6540564E-9B6E-4E1E-8881-6F0DD0F35576}.Release|Any CPU.Build.0 = Release|Any CPU
180+
{AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.CI|Any CPU.ActiveCfg = Debug|Any CPU
181+
{AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.CI|Any CPU.Build.0 = Debug|Any CPU
182+
{AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
183+
{AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.Debug|Any CPU.Build.0 = Debug|Any CPU
184+
{AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.Release|Any CPU.ActiveCfg = Release|Any CPU
185+
{AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.Release|Any CPU.Build.0 = Release|Any CPU
186+
{77BB9496-169D-43DA-BCED-7DB3ACD9179A}.CI|Any CPU.ActiveCfg = Debug|Any CPU
187+
{77BB9496-169D-43DA-BCED-7DB3ACD9179A}.CI|Any CPU.Build.0 = Debug|Any CPU
188+
{77BB9496-169D-43DA-BCED-7DB3ACD9179A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
189+
{77BB9496-169D-43DA-BCED-7DB3ACD9179A}.Debug|Any CPU.Build.0 = Debug|Any CPU
190+
{77BB9496-169D-43DA-BCED-7DB3ACD9179A}.Release|Any CPU.ActiveCfg = Release|Any CPU
191+
{77BB9496-169D-43DA-BCED-7DB3ACD9179A}.Release|Any CPU.Build.0 = Release|Any CPU
176192
EndGlobalSection
177193
GlobalSection(SolutionProperties) = preSolution
178194
HideSolutionNode = FALSE
@@ -196,6 +212,8 @@ Global
196212
{8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
197213
{450FC408-A4E2-4483-B064-2007024D6CF1} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
198214
{6540564E-9B6E-4E1E-8881-6F0DD0F35576} = {4D8FA213-8724-4C43-B68A-F018148D238C}
215+
{AF1479D8-519F-4C6B-9E8C-5A84A6D5549A} = {4D8FA213-8724-4C43-B68A-F018148D238C}
216+
{77BB9496-169D-43DA-BCED-7DB3ACD9179A} = {4D8FA213-8724-4C43-B68A-F018148D238C}
199217
EndGlobalSection
200218
GlobalSection(ExtensibilityGlobals) = postSolution
201219
SolutionGuid = {75DDA3D8-9D6F-4865-93F4-DDE11DEE8290}

Src/FluentAssertions/Execution/AssertionFailedException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace FluentAssertions.Execution;
77
/// </summary>
88
/// <param name="message">The <em>mandatory</em> exception message</param>
99
#pragma warning disable CA1032, RCS1194 // AssertionFailedException should never be constructed with an empty message
10-
public class AssertionFailedException(string message) : Exception(message)
10+
public class AssertionFailedException(string message) : Exception(message), IAssertionException
1111
#pragma warning restore CA1032, RCS1194
1212
{
1313
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace FluentAssertions.Execution;
2+
3+
/// <summary>
4+
/// This is a marker interface for xUnit.net v3 to set the test failure cause as an assertion failure.
5+
/// See <a href="https://xunit.net/docs/getting-started/v3/whats-new#third-party-assertion-library-extension-points">What’s New in xUnit.net v3 - Third party assertion library extension points</a>.
6+
/// </summary>
7+
internal interface IAssertionException;

Src/FluentAssertions/Execution/LateBoundTestFramework.cs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,58 @@
11
using System;
22
using System.Diagnostics.CodeAnalysis;
3+
using System.IO;
34
using System.Reflection;
45

56
namespace FluentAssertions.Execution;
67

78
internal abstract class LateBoundTestFramework : ITestFramework
89
{
9-
private Assembly assembly;
10+
private readonly bool loadAssembly;
11+
private Func<string, Exception> exceptionFactory;
1012

11-
[DoesNotReturn]
12-
public void Throw(string message)
13+
protected LateBoundTestFramework(bool loadAssembly = false)
1314
{
14-
Type exceptionType = assembly.GetType(ExceptionFullName);
15-
16-
if (exceptionType is null)
17-
{
18-
throw new NotSupportedException(
19-
$"Failed to create the assertion exception for the current test framework: \"{ExceptionFullName}, {assembly.FullName}\"");
20-
}
21-
22-
throw (Exception)Activator.CreateInstance(exceptionType, message);
15+
this.loadAssembly = loadAssembly;
16+
exceptionFactory = _ => throw new InvalidOperationException($"{nameof(IsAvailable)} must be called first.");
2317
}
2418

19+
[DoesNotReturn]
20+
public void Throw(string message) => throw exceptionFactory(message);
21+
2522
public bool IsAvailable
2623
{
2724
get
2825
{
29-
string prefix = AssemblyName + ",";
26+
var assembly = FindExceptionAssembly();
27+
var exceptionType = assembly?.GetType(ExceptionFullName);
3028

31-
assembly = Array.Find(AppDomain.CurrentDomain
32-
.GetAssemblies(), a => a.FullName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
29+
exceptionFactory = exceptionType != null
30+
? message => (Exception)Activator.CreateInstance(exceptionType, message)
31+
: _ => throw new InvalidOperationException($"{GetType().Name} is not available");
3332

34-
return assembly is not null;
33+
return exceptionType is not null;
3534
}
3635
}
3736

37+
private Assembly FindExceptionAssembly()
38+
{
39+
var assembly = Array.Find(AppDomain.CurrentDomain.GetAssemblies(), a => a.GetName().Name == AssemblyName);
40+
41+
if (assembly is null && loadAssembly)
42+
{
43+
try
44+
{
45+
return Assembly.Load(new AssemblyName(AssemblyName));
46+
}
47+
catch (FileNotFoundException)
48+
{
49+
return null;
50+
}
51+
}
52+
53+
return assembly;
54+
}
55+
3856
protected internal abstract string AssemblyName { get; }
3957

4058
protected abstract string ExceptionFullName { get; }

Src/FluentAssertions/Execution/LoadableTestFramework.cs

Lines changed: 0 additions & 45 deletions
This file was deleted.

Src/FluentAssertions/Execution/TUnitFramework.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace FluentAssertions.Execution;
22

3-
internal class TUnitFramework : LoadableTestFramework
3+
internal class TUnitFramework() : LateBoundTestFramework(loadAssembly: true)
44
{
55
protected override string ExceptionFullName => "TUnit.Assertions.Exceptions.AssertionException";
66

Src/FluentAssertions/Execution/TestFrameworkProvider.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ internal class TestFrameworkProvider
1818
["mspec"] = new MSpecFramework(),
1919
["nunit"] = new NUnitTestFramework(),
2020
["mstestv2"] = new MSTestFrameworkV2(),
21+
22+
// Keep TUnitFramework and XUnitTestFramework last as they use a try/catch approach
2123
["tunit"] = new TUnitFramework(),
22-
["xunit2"] = new XUnit2TestFramework() // Keep this the last one as it uses a try/catch approach
24+
["xunit2"] = new XUnitTestFramework("xunit.assert"),
25+
["xunit3"] = new XUnitTestFramework("xunit.v3.assert"),
2326
};
2427

2528
private readonly Configuration configuration;

Src/FluentAssertions/Execution/XUnit2TestFramework.cs

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace FluentAssertions.Execution;
2+
3+
/// <summary>
4+
/// Implements the xUnit (version 2 and 3) test framework adapter.
5+
/// </summary>
6+
internal class XUnitTestFramework(string assemblyName) : LateBoundTestFramework(loadAssembly: true)
7+
{
8+
protected internal override string AssemblyName => assemblyName;
9+
10+
protected override string ExceptionFullName => "Xunit.Sdk.XunitException";
11+
}

0 commit comments

Comments
 (0)