├── testsolution
├── .idea
│ └── .idea.TestSolution
│ │ └── .idea
│ │ ├── .name
│ │ └── .gitignore
├── TestSolution.sln
└── TestProject
│ ├── TestProject.csproj
│ └── TestAssertions.cs
├── src
├── .idea
│ └── .idea.FluentAssertionsMigrator
│ │ └── .idea
│ │ ├── .name
│ │ └── .gitignore
├── FluentAssertionsMigrator
│ ├── FluentAssertionsMigrator.csproj
│ └── Program.cs
├── FluentAssertionsMigrator.Tests
│ ├── FluentAssertionsMigrator.Tests.csproj
│ └── FluentAssertionsSolutionMigratorTests.cs
└── FluentAssertionsMigrator.sln
├── .idea
├── .idea.FluentAssertionsMigrator
│ └── .idea
│ │ └── .gitignore
└── .idea.fluentassertions-migrator.dir
│ └── .idea
│ └── .gitignore
├── .github
└── workflows
│ └── dotnet.yml
├── .gitignore
├── LICENSE
└── README.md
/testsolution/.idea/.idea.TestSolution/.idea/.name:
--------------------------------------------------------------------------------
1 | TestSolution
--------------------------------------------------------------------------------
/src/.idea/.idea.FluentAssertionsMigrator/.idea/.name:
--------------------------------------------------------------------------------
1 | FluentAssertionsMigrator
--------------------------------------------------------------------------------
/testsolution/.idea/.idea.TestSolution/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Rider ignored files
5 | /contentModel.xml
6 | /.idea.TestSolution.iml
7 | /modules.xml
8 | /projectSettingsUpdater.xml
9 | # Editor-based HTTP Client requests
10 | /httpRequests/
11 | # Datasource local storage ignored files
12 | /dataSources/
13 | /dataSources.local.xml
14 |
--------------------------------------------------------------------------------
/.idea/.idea.FluentAssertionsMigrator/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Rider ignored files
5 | /contentModel.xml
6 | /projectSettingsUpdater.xml
7 | /modules.xml
8 | /.idea.FluentAssertionsMigrator.iml
9 | # Editor-based HTTP Client requests
10 | /httpRequests/
11 | # Datasource local storage ignored files
12 | /dataSources/
13 | /dataSources.local.xml
14 |
--------------------------------------------------------------------------------
/.idea/.idea.fluentassertions-migrator.dir/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Rider ignored files
5 | /projectSettingsUpdater.xml
6 | /.idea.fluentassertions-migrator.iml
7 | /contentModel.xml
8 | /modules.xml
9 | # Editor-based HTTP Client requests
10 | /httpRequests/
11 | # Datasource local storage ignored files
12 | /dataSources/
13 | /dataSources.local.xml
14 |
--------------------------------------------------------------------------------
/src/.idea/.idea.FluentAssertionsMigrator/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Rider ignored files
5 | /modules.xml
6 | /projectSettingsUpdater.xml
7 | /contentModel.xml
8 | /.idea.FluentAssertionsMigrator.iml
9 | # Editor-based HTTP Client requests
10 | /httpRequests/
11 | # Datasource local storage ignored files
12 | /dataSources/
13 | /dataSources.local.xml
14 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: .NET
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | defaults:
14 | run:
15 | working-directory: ./src
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: 9.0.x
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.idea/**/workspace.xml
2 | **/.idea/**/tasks.xml
3 | **/.idea/shelf/*
4 | **/.idea/dictionaries
5 | **/.idea/**/dataSources/
6 | **/.idea/**/dataSources.ids
7 | **/.idea/**/dataSources.xml
8 | **/.idea/**/dataSources.local.xml
9 | **/.idea/**/sqlDataSources.xml
10 | **/.idea/**/dynamic.xml
11 | **/.idea/**/cody_history.xml
12 | **/.idea/**/*.iml
13 | **/.idea/**/contentModel.xml
14 | **/.idea/**/modules.xml
15 | **/.idea/**/encodings.xml
16 | **/.idea/**/indexLayout.xml
17 | **/.idea/**/misc.xml
18 | **/.idea/**/vcs.xml
19 |
20 | *.suo
21 | *.user
22 | .vs/
23 | [Bb]in/
24 | [Oo]bj/
25 | _UpgradeReport_Files/
26 | [Pp]ackages/
27 |
28 | Thumbs.db
29 | Desktop.ini
30 | .DS_Store
31 | *.nupkg
32 |
--------------------------------------------------------------------------------
/testsolution/TestSolution.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31903.59
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "TestProject\TestProject.csproj", "{B15C45BB-9BEF-4573-A349-EF7542277D3A}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(SolutionProperties) = preSolution
14 | HideSolutionNode = FALSE
15 | EndGlobalSection
16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
17 | {B15C45BB-9BEF-4573-A349-EF7542277D3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18 | {B15C45BB-9BEF-4573-A349-EF7542277D3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
19 | {B15C45BB-9BEF-4573-A349-EF7542277D3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
20 | {B15C45BB-9BEF-4573-A349-EF7542277D3A}.Release|Any CPU.Build.0 = Release|Any CPU
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/testsolution/TestProject/TestProject.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | all
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Alexander Moerman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/FluentAssertionsMigrator/FluentAssertionsMigrator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 | true
9 | migrate-fluentassertions
10 | ./nupkg
11 | 1.2.0
12 | FluentAssertionsMigrator
13 | Alexander Moerman
14 | Tool to migrate FluentAssertions to xUnit assertions
15 | Debug;Release
16 | bin\$(Configuration)\
17 | false
18 | false
19 | true
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/FluentAssertionsMigrator.Tests/FluentAssertionsMigrator.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | false
8 | true
9 | Debug;Release
10 | bin\$(Configuration)\
11 | false
12 | false
13 | true
14 |
15 |
16 |
17 |
18 |
19 | all
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 |
22 |
23 |
24 |
25 |
26 | all
27 | runtime; build; native; contentfiles; analyzers; buildtransitive
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/FluentAssertionsMigrator.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31903.59
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentAssertionsMigrator", "FluentAssertionsMigrator\FluentAssertionsMigrator.csproj", "{21428281-80A6-40E8-90CE-CBDBFF9325CC}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentAssertionsMigrator.Tests", "FluentAssertionsMigrator.Tests\FluentAssertionsMigrator.Tests.csproj", "{5B423BBC-70CE-4078-86B5-C43B0EF97A15}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "..\testsolution\TestProject\TestProject.csproj", "{E80FECA0-4CFB-47FF-9A69-ED6CCC0C5C86}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C10DC7B0-F395-4B5A-B02E-4B931ED0D27B}"
13 | ProjectSection(SolutionItems) = preProject
14 | ..\.github\workflows\dotnet.yml = ..\.github\workflows\dotnet.yml
15 | EndProjectSection
16 | EndProject
17 | Global
18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(SolutionProperties) = preSolution
23 | HideSolutionNode = FALSE
24 | EndGlobalSection
25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
26 | {21428281-80A6-40E8-90CE-CBDBFF9325CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {21428281-80A6-40E8-90CE-CBDBFF9325CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {21428281-80A6-40E8-90CE-CBDBFF9325CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {21428281-80A6-40E8-90CE-CBDBFF9325CC}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {5B423BBC-70CE-4078-86B5-C43B0EF97A15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {5B423BBC-70CE-4078-86B5-C43B0EF97A15}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {5B423BBC-70CE-4078-86B5-C43B0EF97A15}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {5B423BBC-70CE-4078-86B5-C43B0EF97A15}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {E80FECA0-4CFB-47FF-9A69-ED6CCC0C5C86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {E80FECA0-4CFB-47FF-9A69-ED6CCC0C5C86}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {E80FECA0-4CFB-47FF-9A69-ED6CCC0C5C86}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {E80FECA0-4CFB-47FF-9A69-ED6CCC0C5C86}.Release|Any CPU.Build.0 = Release|Any CPU
38 | EndGlobalSection
39 | EndGlobal
40 |
--------------------------------------------------------------------------------
/src/FluentAssertionsMigrator.Tests/FluentAssertionsSolutionMigratorTests.cs:
--------------------------------------------------------------------------------
1 | using CliWrap;
2 | using Microsoft.Extensions.Logging;
3 | using Xunit.Abstractions;
4 |
5 | namespace FluentAssertionsMigrator.Tests;
6 |
7 | public sealed class FluentAssertionsSolutionMigratorTests
8 | {
9 | private readonly FluentAssertionsSolutionMigrator _solutionMigrator;
10 | private readonly FileInfo _solutionFile;
11 | private readonly ILogger _testLogger;
12 |
13 | public FluentAssertionsSolutionMigratorTests(ITestOutputHelper output)
14 | {
15 | _testLogger = output.ToLogger();
16 | _solutionMigrator = new FluentAssertionsSolutionMigrator(
17 | output.ToLogger(),
18 | new FluentAssertionsDocumentMigrator(
19 | output.ToLogger(),
20 | new FluentAssertionsSyntaxRewriterFactory(output.ToLoggerFactory())
21 | )
22 | );
23 |
24 | var originalTestSolutionFile = new FileInfo(Path.GetFullPath("../../../../testsolution/TestSolution.sln"));
25 | if (!originalTestSolutionFile.Exists)
26 | {
27 | throw new InvalidOperationException($"Solution file {originalTestSolutionFile} does not exist");
28 | }
29 |
30 | // Copy test solution to a temporary folder to support repeated test runs
31 | var newTestSolutionFileDirectory = new DirectoryInfo(Path.GetFullPath("./testsolution"));
32 | if (newTestSolutionFileDirectory.Exists)
33 | {
34 | newTestSolutionFileDirectory.Delete(true);
35 | }
36 | newTestSolutionFileDirectory.Create();
37 | CopyDirectory(originalTestSolutionFile.Directory!, newTestSolutionFileDirectory);
38 |
39 | _solutionFile = new FileInfo(Path.Combine(newTestSolutionFileDirectory.FullName, "TestSolution.sln"));
40 | }
41 |
42 | [Fact]
43 | public async Task MigrateTestSolution()
44 | {
45 | // Act
46 | await _solutionMigrator.MigrateAsync(_solutionFile);
47 |
48 | // Assert
49 | await Cli.Wrap("dotnet")
50 | .WithArguments("test")
51 | .WithWorkingDirectory(_solutionFile.Directory!.FullName)
52 | .WithStandardOutputPipe(PipeTarget.ToDelegate(message => _testLogger.LogInformation("[dotnet test]: {Message}", message)))
53 | .WithStandardErrorPipe(PipeTarget.ToDelegate(message => _testLogger.LogError("[dotnet test]: {Message}", message)))
54 | .ExecuteAsync();
55 | }
56 |
57 | // Helper method for recursive directory copying
58 | private static void CopyDirectory(DirectoryInfo source, DirectoryInfo destination)
59 | {
60 | destination.Create();
61 |
62 | foreach (var file in source.GetFiles())
63 | {
64 | file.CopyTo(Path.Combine(destination.FullName, file.Name), true);
65 | }
66 |
67 | foreach (var dir in source.GetDirectories())
68 | {
69 | CopyDirectory(dir, new DirectoryInfo(Path.Combine(destination.FullName, dir.Name)));
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FluentAssertions to xUnit Migration Tool
2 |
3 | A .NET tool that automatically converts FluentAssertions test assertions to their xUnit equivalents.
4 | Regardless of how we feel about the upcoming license changes in the next major version of FluentAssertions, I imagine many of us will find ourselves in the position of having to strip out FluentAssertions of our codebases.
5 | I know I will. This tool tries to automate that process as much as possible.
6 |
7 | ## Based on the following sources
8 |
9 | - https://github.com/xunit/xunit/issues/3133
10 | - https://www.meziantou.net/using-roslyn-to-analyze-and-rewrite-code-in-a-solution.htm
11 | - https://stackoverflow.com/questions/31481251/applying-multiple-changes-to-a-solution-in-rosly
12 | - ...
13 |
14 |
15 | ## Installation
16 |
17 | ```bash
18 | dotnet tool install -g FluentAssertionsMigrator
19 | ```
20 |
21 | ### Building the Tool yourself
22 |
23 | ```bash
24 | cd C:\...\fluentassertions-migrator\src\
25 | dotnet pack
26 | dotnet tool install -g FluentAssertionsMigrator
27 | ```
28 |
29 | ## Usage
30 |
31 | **Make a backup of your solution (or use version control like a sane person) before you run this tool**
32 |
33 | **Before you run this tool, be sure to restore the nuget packages of your solution. This tool uses semantic analysis in some places that will not work without these packages.**
34 |
35 | ```bash
36 | migrate-fluentassertions
37 | ```
38 |
39 | Example:
40 | ```bash
41 | migrate-fluentassertions C:\Projects\MySolution.sln
42 | ```
43 |
44 | ## What it does
45 |
46 | This tool analyzes your test files and converts FluentAssertions API calls to their xUnit equivalents. It handles many common assertion patterns including:
47 |
48 | ### Equality
49 | - `.Should().Be()` → `Assert.Equal()`
50 | - `.Should().NotBe()` → `Assert.NotEqual()`
51 | - `.Should().BeSameAs()` → `Assert.Same()`
52 | - `.Should().NotBeSameAs()` → `Assert.NotSame()`
53 | - `.Should().BeEquivalentTo()` → `Assert.Equivalent()`
54 |
55 | ### Boolean checks
56 | - `.Should().BeTrue()` → `Assert.True()`
57 | - `.Should().BeFalse()` → `Assert.False()`
58 |
59 | ### Null checks
60 | - `.Should().BeNull()` → `Assert.Null()`
61 | - `.Should().NotBeNull()` → `Assert.NotNull()`
62 |
63 | ### Collections
64 | - `.Should().BeEmpty()` → `Assert.Empty()`
65 | - `.Should().NotBeEmpty()` → `Assert.NotEmpty()`
66 | - `.Should().HaveCount()` → `Assert.Equal(count)` or `Assert.Single()` or `Assert.Empty()`
67 | - `.Should().Contain()` → `Assert.Contains()`
68 | - `.Should().NotContain()` → `Assert.DoesNotContain()`
69 | - `.Should().ContainSingle()` → `Assert.Single()`
70 | - `.Should().BeNullOrEmpty()` → `Assert.False(?.Any() ?? false)`
71 | - `.Should().NotBeNullOrEmpty()` → `Assert.True(?.Any())`
72 | - `.Should().BeOneOf()` → `Assert.Contains()`
73 | - `.Should().ContainEquivalentOf()` → `Assert.Contains()`
74 | - `.Should().NotContainEquivalentOf()` → `Assert.DoesNotContain()`
75 |
76 | ### Strings
77 | - `.Should().StartWith()` → `Assert.StartsWith()`
78 | - `.Should().EndWith()` → `Assert.EndsWith()`
79 | - `.Should().ContainAll()` → `Assert.All(strings, s => Assert.Contains(s, x))`
80 |
81 | ### Type checks
82 | - `.Should().BeOfType()` → `Assert.True(x is Type)`
83 | - `.Should().NotBeOfType()` → `Assert.False(x is Type)`
84 |
85 | ### Exceptions
86 | - `.Should().Throw()` → `Assert.Throws()`
87 | - `.Should().NotThrow()` → `Assert.Nulll(Record.Exception())`
88 | - `.Should().Throw()` → `Assert.Throws()`
89 | - `.Should().NotThrow()` → `Assert.IsType(Record.Exception())`
90 | - `.Should().ThrowAsync()` → `Assert.ThrowsAsync()`
91 | - `.Should().NotThrowAsync()` → `Assert.Null(Record.ExceptionAsync())`
92 | - `.Should().ThrowAsync()` → `Assert.ThrowsAsync()`
93 | - `.Should().NotThrowAsync()` → `Assert.IsNotType(Record.ExceptionAsync())`
94 |
95 | ### Numeric comparisons
96 | - `.Should().BeGreaterThan()` → `Assert.True(x > y)`
97 | - `.Should().BeLessThan()` → `Assert.True(x < y)`
98 | - `.Should().BeCloseTo()` → `Assert.True(x > (expected - precision) && x < (expected + precision))`
99 | - `.Should().NotBeCloseTo()` → `Assert.False(x > (expected - precision) && x < (expected + precision))`
100 |
101 | ### Date comparisons
102 | - `.Should().BeBefore()` → `Assert.True(x < y)`
103 | - `.Should().NotBeBefore()` → `Assert.False(x < y)`
104 | - `.Should().BeAfter()` → `Assert.True(x > y)`
105 | - `.Should().NotBeAfter()` → `Assert.False(x > y)`
106 | - `.Should().BeOnOrBefore()` → `Assert.True(x <= y)`
107 | - `.Should().NotBeOnOrBefore()` → `Assert.False(x <= y)`
108 | - `.Should().BeOnOrAfter()` → `Assert.True(x >= y)`
109 | - `.Should().NotBeOnOrAfter()` → `Assert.False(x >= y)`
110 |
111 | ## Important Note
112 |
113 | While this tool automates a significant portion of the migration, manual review and adjustments will be needed:
114 |
115 | 1. Some complex FluentAssertions may not have direct xUnit equivalents
116 | 2. Custom assertion messages will need to be reformatted
117 | 3. Chain assertions (using .And) will need to be split into multiple Assert statements
118 | 4. Some assertions might require additional null checks or type conversions
119 |
120 | Always review the generated code and test thoroughly after migration.
121 |
122 | ## Contributing
123 |
124 | Feel free to submit issues and pull requests but I'm not making any promises. :-)
125 |
126 | ## Is this free?
127 |
128 | Yes, it is!
129 | If you really like this and want to give something in return, donations can be made here: https://github.com/sponsors/amoerie?frequency=one-time. Thanks!
130 |
--------------------------------------------------------------------------------
/testsolution/TestProject/TestAssertions.cs:
--------------------------------------------------------------------------------
1 | using FluentAssertions;
2 |
3 | namespace TestProject;
4 |
5 | public class TestAssertions
6 | {
7 | /* Equality */
8 | [Fact]
9 | public void Be()
10 | {
11 | var actual = 42;
12 | actual.Should().Be(42);
13 | }
14 |
15 | [Fact]
16 | public void NotBe()
17 | {
18 | var actual = 42;
19 | actual.Should().NotBe(43);
20 | }
21 |
22 | [Fact]
23 | public void BeSameAs()
24 | {
25 | var obj = new object();
26 | var same = obj;
27 | same.Should().BeSameAs(obj);
28 | }
29 |
30 | [Fact]
31 | public void NotBeSameAs()
32 | {
33 | var obj1 = new object();
34 | var obj2 = new object();
35 | obj1.Should().NotBeSameAs(obj2);
36 | }
37 |
38 | [Fact]
39 | public void BeEquivalentTo()
40 | {
41 | var list1 = new List { 1, 2, 3 };
42 | var list2 = new List { 1, 2, 3 };
43 | list1.Should().BeEquivalentTo(list2);
44 | }
45 |
46 | /* Boolean checks */
47 | [Fact]
48 | public void BeTrue()
49 | {
50 | var result = true;
51 | result.Should().BeTrue();
52 | }
53 |
54 | [Fact]
55 | public void BeFalse()
56 | {
57 | var result = false;
58 | result.Should().BeFalse();
59 | }
60 |
61 | /* Null checks */
62 | [Fact]
63 | public void BeNull()
64 | {
65 | string? value = null;
66 | value.Should().BeNull();
67 | }
68 |
69 | [Fact]
70 | public void NotBeNull()
71 | {
72 | string value = "test";
73 | value.Should().NotBeNull();
74 | }
75 |
76 | /* Collections */
77 | [Fact]
78 | public void BeEmpty()
79 | {
80 | var list = new List();
81 | list.Should().BeEmpty();
82 | }
83 |
84 | [Fact]
85 | public void NotBeEmpty()
86 | {
87 | var list = new List { 1 };
88 | list.Should().NotBeEmpty();
89 | }
90 |
91 | [Fact]
92 | public void HaveCount()
93 | {
94 | var list = new List { 1, 2, 3 };
95 | list.Should().HaveCount(3);
96 | }
97 |
98 | [Fact]
99 | public void Contain()
100 | {
101 | var list = new List { 1, 2, 3 };
102 | list.Should().Contain(2);
103 | }
104 |
105 | [Fact]
106 | public void ContainSingle()
107 | {
108 | var list = new List { 1};
109 | list.Should().ContainSingle();
110 | }
111 |
112 | [Fact]
113 | public void NotContain()
114 | {
115 | var list = new List { 1, 2, 3 };
116 | list.Should().NotContain(4);
117 | }
118 |
119 | [Fact]
120 | public void BeNullOrEmpty()
121 | {
122 | string? value = null;
123 | value.Should().BeNullOrEmpty();
124 | }
125 |
126 | [Fact]
127 | public void NotBeNullOrEmpty()
128 | {
129 | string value = "test";
130 | value.Should().NotBeNullOrEmpty();
131 | }
132 |
133 | [Fact]
134 | public void BeOneOf()
135 | {
136 | var value = 2;
137 | var validValues = new[] { 1, 2, 3 };
138 | value.Should().BeOneOf(validValues);
139 | }
140 |
141 | [Fact]
142 | public void ContainEquivalentOf()
143 | {
144 | var value = "TestExample";
145 | value.Should().ContainEquivalentOf("TEST");
146 | }
147 |
148 | [Fact]
149 | public void NotContainEquivalentOf()
150 | {
151 | var value = "TestExample";
152 | value.Should().NotContainEquivalentOf("OTHER");
153 | }
154 |
155 | /* Strings */
156 | [Fact]
157 | public void StartWith()
158 | {
159 | var text = "Hello World";
160 | text.Should().StartWith("Hello");
161 | }
162 |
163 | [Fact]
164 | public void EndWith()
165 | {
166 | var text = "Hello World";
167 | text.Should().EndWith("World");
168 | }
169 |
170 | /* Type checks */
171 | [Fact]
172 | public void BeOfType()
173 | {
174 | var obj = "test";
175 | obj.Should().BeOfType();
176 | }
177 |
178 | [Fact]
179 | public void NotBeOfType()
180 | {
181 | var obj = "test";
182 | obj.Should().NotBeOfType();
183 | }
184 |
185 | /* Exceptions */
186 | [Fact]
187 | public void Throw()
188 | {
189 | Action action = () => throw new InvalidOperationException();
190 | action.Should().Throw();
191 | }
192 |
193 | [Fact]
194 | public void NotThrow()
195 | {
196 | Action action = () => { };
197 | action.Should().NotThrow();
198 | }
199 |
200 | [Fact]
201 | public async Task ThrowAsync()
202 | {
203 | Func action = () => Task.FromException(new InvalidOperationException());
204 | await action.Should().ThrowAsync();
205 | }
206 |
207 | [Fact]
208 | public async Task NotThrowAsync()
209 | {
210 | Func action = async () => await Task.CompletedTask;
211 | await action.Should().NotThrowAsync();
212 | }
213 |
214 | /* Numeric comparison */
215 | [Fact]
216 | public void BeGreaterThan()
217 | {
218 | var number = 10;
219 | number.Should().BeGreaterThan(5);
220 | }
221 |
222 | [Fact]
223 | public void BeLessThan()
224 | {
225 | var number = 5;
226 | number.Should().BeLessThan(10);
227 | }
228 |
229 | [Fact]
230 | public void BeCloseTo()
231 | {
232 | var value = 10;
233 | value.Should().BeCloseTo(11, 1);
234 | }
235 |
236 | [Fact]
237 | public void NotBeCloseTo()
238 | {
239 | var value = 10;
240 | value.Should().NotBeCloseTo(12, 1);
241 | }
242 |
243 | /* Date comparisons */
244 | [Fact]
245 | public void BeBefore()
246 | {
247 | var earlier = DateTime.Now.AddDays(-1);
248 | var later = DateTime.Now;
249 | earlier.Should().BeBefore(later);
250 | }
251 |
252 | [Fact]
253 | public void BeAfter()
254 | {
255 | var earlier = DateTime.Now.AddDays(-1);
256 | var later = DateTime.Now;
257 | later.Should().BeAfter(earlier);
258 | }
259 |
260 | [Fact]
261 | public void NotBeBefore()
262 | {
263 | var date1 = DateTime.Now;
264 | var date2 = date1.AddDays(-1);
265 | date1.Should().NotBeBefore(date2);
266 | }
267 |
268 | [Fact]
269 | public void NotBeAfter()
270 | {
271 | var date1 = DateTime.Now;
272 | var date2 = date1.AddDays(1);
273 | date1.Should().NotBeAfter(date2);
274 | }
275 |
276 | [Fact]
277 | public void BeOnOrBefore()
278 | {
279 | var date1 = DateTime.Now;
280 | var date2 = date1;
281 | date1.Should().BeOnOrBefore(date2);
282 | }
283 |
284 | [Fact]
285 | public void BeOnOrAfter()
286 | {
287 | var date1 = DateTime.Now;
288 | var date2 = date1;
289 | date1.Should().BeOnOrAfter(date2);
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/src/FluentAssertionsMigrator/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Text.RegularExpressions;
4 | using Microsoft.CodeAnalysis;
5 | using Microsoft.CodeAnalysis.CSharp;
6 | using Microsoft.CodeAnalysis.CSharp.Syntax;
7 | using Microsoft.CodeAnalysis.MSBuild;
8 | using Microsoft.Extensions.Logging;
9 |
10 | // Sources
11 | // https://github.com/xunit/xunit/issues/3133
12 | // https://www.meziantou.net/using-roslyn-to-analyze-and-rewrite-code-in-a-solution.htm
13 | // https://stackoverflow.com/questions/31481251/applying-multiple-changes-to-a-solution-in-roslyn
14 |
15 | if (args.Length == 0)
16 | {
17 | Console.WriteLine("FluentAssertions to xUnit Migration Tool");
18 | Console.WriteLine("Usage: FluentAssertionsMigrator.exe ");
19 | Console.WriteLine("Example: FluentAssertionsMigrator.exe C:\\Projects\\MySolution.sln");
20 | return 1;
21 | }
22 |
23 | var solutionFile = new FileInfo(args[0]);
24 |
25 | if (!solutionFile.Exists)
26 | {
27 | Console.WriteLine($"Error: Solution file not found at path: {solutionFile.FullName}");
28 | return 1;
29 | }
30 |
31 | var loggerFactory = LoggerFactory.Create(builder =>
32 | {
33 | builder.SetMinimumLevel(LogLevel.Debug);
34 | builder.AddSimpleConsole(options =>
35 | {
36 | options.SingleLine = true;
37 | options.TimestampFormat = "HH:mm:ss";
38 | });
39 | });
40 |
41 | var solutionMigrator = new FluentAssertionsSolutionMigrator(
42 | loggerFactory.CreateLogger(),
43 | new FluentAssertionsDocumentMigrator(
44 | loggerFactory.CreateLogger(),
45 | new FluentAssertionsSyntaxRewriterFactory(loggerFactory)
46 | )
47 | );
48 |
49 | await solutionMigrator.MigrateAsync(solutionFile);
50 |
51 | return 0;
52 |
53 | public sealed class FluentAssertionsSolutionMigrator(
54 | ILogger logger,
55 | FluentAssertionsDocumentMigrator documentMigrator)
56 | {
57 | public async Task MigrateAsync(FileInfo solutionFile)
58 | {
59 | if (!solutionFile.Exists)
60 | {
61 | throw new ArgumentException($"The specified solution file {solutionFile} does not exist.",
62 | nameof(solutionFile));
63 | }
64 |
65 | logger.LogInformation("Migrating solution: {SolutionFile}", solutionFile);
66 |
67 | // Find where the MSBuild assemblies are located on your system.
68 | // If you need a specific version, you can use MSBuildLocator.RegisterMSBuildPath.
69 | logger.LogDebug("Locating MSBuild");
70 | var instance = Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults();
71 | logger.LogInformation("Located MSBuild at {MSBuildPath}", instance.MSBuildPath);
72 |
73 | // Note that you may need to restore the NuGet packages for the solution before opeing it with Roslyn.
74 | // Depending on what you want to do, dependencies may be required for a correct analysis.
75 |
76 | // Create a Roslyn workspace and load the solution
77 | logger.LogDebug("Creating Roslyn workspace and opening solution (this may take a few moments)");
78 | var start = Stopwatch.GetTimestamp();
79 | var workspace = MSBuildWorkspace.Create();
80 | var solution = await workspace.OpenSolutionAsync(solutionFile.FullName);
81 | var elapsed = Stopwatch.GetElapsedTime(start);
82 | logger.LogInformation("Opened solution {Solution} in {Elapsed}s, found {NumberOfProjects} projects",
83 | solution.FilePath, elapsed.TotalSeconds, solution.ProjectIds.Count);
84 |
85 | var solutionProjectIds = solution.ProjectIds;
86 | foreach (var projectId in solutionProjectIds)
87 | {
88 | var project = solution.GetProject(projectId);
89 | if (project is null)
90 | {
91 | continue;
92 | }
93 |
94 | var projectDocumentIds = project.DocumentIds;
95 |
96 | foreach (var documentId in projectDocumentIds)
97 | {
98 | var document = project.GetDocument(documentId);
99 | if (document is null)
100 | {
101 | continue;
102 | }
103 |
104 | var migratedDocument = await documentMigrator.MigrateAsync(document);
105 |
106 | //Persist your changes to the current project
107 | project = migratedDocument.Project;
108 | }
109 |
110 | //Persist the project changes to the current solution
111 | solution = project.Solution;
112 | }
113 |
114 | //Finally, apply all your changes to the workspace at once.
115 | if (workspace.TryApplyChanges(solution))
116 | {
117 | logger.LogInformation("Successfully migrated solution: {SolutionFile}", solutionFile);
118 | }
119 | else
120 | {
121 | logger.LogError("Failed to apply changes to solution: {SolutionFile}", solutionFile);
122 | }
123 | }
124 | }
125 |
126 | public sealed class FluentAssertionsDocumentMigrator(
127 | ILogger logger,
128 | FluentAssertionsSyntaxRewriterFactory syntaxRewriterFactory)
129 | {
130 | public async Task MigrateAsync(Document document)
131 | {
132 | logger.LogTrace(">> Migrating: {Document}", document.FilePath);
133 |
134 | var originalRoot = await document.GetSyntaxRootAsync();
135 |
136 | // Remove FluentAssertions using directive
137 | var rootWithoutUsingFluentAssertions = originalRoot?.RemoveNodes(
138 | originalRoot.DescendantNodes()
139 | .OfType()
140 | .Where(u => u.Name?.ToString().StartsWith("FluentAssertions") == true),
141 | SyntaxRemoveOptions.KeepNoTrivia);
142 |
143 | var lazySemanticModel = new Lazy>(async () => await document.GetSemanticModelAsync());
144 | var syntaxRewriter = syntaxRewriterFactory.Create(lazySemanticModel);
145 | var migratedRoot = syntaxRewriter.Visit(rootWithoutUsingFluentAssertions);
146 |
147 | if (migratedRoot is null || migratedRoot.IsEquivalentTo(originalRoot))
148 | {
149 | logger.LogTrace(">> No changes: {Document}", document.FilePath);
150 | return document;
151 | }
152 |
153 | logger.LogDebug(">> Migrated: {Document}", document.FilePath);
154 | return document.WithSyntaxRoot(migratedRoot);
155 | }
156 | }
157 |
158 | public sealed class FluentAssertionsSyntaxRewriterFactory(ILoggerFactory loggerFactory)
159 | {
160 | public FluentAssertionsSyntaxRewriter Create(Lazy> semanticModel)
161 | {
162 | return new FluentAssertionsSyntaxRewriter(
163 | loggerFactory.CreateLogger(),
164 | semanticModel);
165 | }
166 | }
167 |
168 | public sealed partial class FluentAssertionsSyntaxRewriter(
169 | ILogger logger,
170 | Lazy> lazySemanticModel)
171 | : CSharpSyntaxRewriter
172 | {
173 | public override SyntaxNode? VisitAwaitExpression(AwaitExpressionSyntax node)
174 | {
175 | if (TryResolveActualValueFromAwaitExpression(node, out var invocation, out var actualValueExpression))
176 | {
177 | return HandleAssertionExpression(node, invocation, actualValueExpression);
178 | }
179 |
180 | return base.VisitAwaitExpression(node);
181 | }
182 |
183 | public override SyntaxNode? VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node)
184 | {
185 | if (TryResolveActualValueFromConditionalAccessExpression(node, out var invocation,
186 | out var actualValueExpression))
187 | {
188 | return HandleAssertionExpression(node, invocation, actualValueExpression);
189 | }
190 |
191 | return base.VisitConditionalAccessExpression(node);
192 | }
193 |
194 | public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node)
195 | {
196 | if (TryResolveActualValueFromShouldInvocationExpression(node, out var actualValueExpression))
197 | {
198 | return HandleAssertionExpression(node, node, actualValueExpression);
199 | }
200 |
201 | return base.VisitInvocationExpression(node);
202 | }
203 |
204 | private SyntaxNode? HandleAssertionExpression(ExpressionSyntax node,
205 | InvocationExpressionSyntax shouldInvocationExpression, ExpressionSyntax actualValueExpression)
206 | {
207 | var shouldInvocationExpressionAsString = shouldInvocationExpression.Expression.ToString();
208 |
209 | if (shouldInvocationExpressionAsString.EndsWith(".Should().Be"))
210 | {
211 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
212 | logger.LogTrace("Rewriting .Should().Be() in {Node}", node);
213 | // TODO equality between something nullable and something not-nullable
214 | return CreateAssertExpression($"Assert.Equal({expectedValue}, {actualValueExpression})", node);
215 | }
216 |
217 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBe"))
218 | {
219 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
220 | logger.LogTrace("Rewriting .Should().NotBe() in {Node}", node);
221 | return CreateAssertExpression($"Assert.NotEqual({expectedValue}, {actualValueExpression})", node);
222 | }
223 |
224 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeSameAs"))
225 | {
226 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
227 | logger.LogTrace("Rewriting .Should().BeSameAs() in {Node}", node);
228 | return CreateAssertExpression($"Assert.Same({expectedValue}, {actualValueExpression})", node);
229 | }
230 |
231 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBeSameAs"))
232 | {
233 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
234 | logger.LogTrace("Rewriting .Should().NotBeSameAs() in {Node}", node);
235 | return CreateAssertExpression($"Assert.NotSame({expectedValue}, {actualValueExpression})", node);
236 | }
237 |
238 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeTrue"))
239 | {
240 | logger.LogTrace("Rewriting .Should().BeTrue() in {Node}", node);
241 | return CreateAssertExpression($"Assert.True({actualValueExpression})", node);
242 | }
243 |
244 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeFalse"))
245 | {
246 | logger.LogTrace("Rewriting .Should().BeFalse() in {Node}", node);
247 | return CreateAssertExpression($"Assert.False({actualValueExpression})", node);
248 | }
249 |
250 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeNull"))
251 | {
252 | logger.LogTrace("Rewriting .Should().BeNull() in {Node}", node);
253 | return CreateAssertExpression($"Assert.Null({actualValueExpression})", node);
254 | }
255 |
256 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBeNull"))
257 | {
258 | logger.LogTrace("Rewriting .Should().NotBeNull() in {Node}", node);
259 | return CreateAssertExpression($"Assert.NotNull({actualValueExpression})", node);
260 | }
261 |
262 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeEmpty"))
263 | {
264 | logger.LogTrace("Rewriting .Should().BeEmpty() in {Node}", node);
265 | if (IsNullable(actualValueExpression) == false)
266 | {
267 | return CreateAssertExpression($"Assert.Empty({actualValueExpression})", node);
268 | }
269 | // xUnit's Assert.Empty does not handle null well, so add a fallback for that
270 | return CreateAssertExpression($"Assert.Empty({actualValueExpression} ?? [])", node);
271 | }
272 |
273 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBeEmpty"))
274 | {
275 | logger.LogTrace("Rewriting .Should().NotBeEmpty() in {Node}", node);
276 | var assertCode = $"Assert.NotEmpty({actualValueExpression})";
277 | return CreateAssertExpression(assertCode, node);
278 | }
279 |
280 | if (ThrowRegex().IsMatch(shouldInvocationExpressionAsString))
281 | {
282 | var exceptionType = GetGenericTypeArgument(shouldInvocationExpression) ?? GetTypeOfArgument(shouldInvocationExpression);
283 | logger.LogTrace("Rewriting .Should().Throw() in {Node}", node);
284 | if (exceptionType is not null)
285 | {
286 | return CreateAssertExpression($"Assert.Throws<{exceptionType}>({actualValueExpression})", node);
287 | }
288 | return CreateAssertExpression($"Assert.Throws({actualValueExpression})", node);
289 | }
290 |
291 | if (NotThrowRegex().IsMatch(shouldInvocationExpressionAsString))
292 | {
293 | var exceptionType = GetGenericTypeArgument(shouldInvocationExpression) ?? GetTypeOfArgument(shouldInvocationExpression);
294 | logger.LogTrace("Rewriting .Should().NotThrow() in {Node}", node);
295 | if (exceptionType is not null)
296 | {
297 | return CreateAssertExpression($"Assert.IsNotType<{exceptionType}>(Record.Exception({actualValueExpression}))", node);
298 | }
299 |
300 | return CreateAssertExpression($"Assert.Null(Record.Exception({actualValueExpression}))", node);
301 | }
302 |
303 | if (ThrowAsyncRegex().IsMatch(shouldInvocationExpressionAsString))
304 | {
305 | logger.LogTrace("Rewriting .Should().ThrowAsync() in {Node}", node);
306 |
307 | if (GetGenericTypeArgument(shouldInvocationExpression) is { } genericType)
308 | {
309 | return CreateAssertExpression($"await Assert.ThrowsAsync<{genericType}>({actualValueExpression})", node);
310 | }
311 | if (GetTypeOfArgument(shouldInvocationExpression) is { } typeofArgument)
312 | {
313 | return CreateAssertExpression($"await Assert.ThrowsAsync({typeofArgument}, {actualValueExpression})", node);
314 | }
315 |
316 | return CreateAssertExpression($"await Assert.ThrowsAsync({actualValueExpression})", node);
317 | }
318 |
319 | if (NotThrowAsyncRegex().IsMatch(shouldInvocationExpressionAsString))
320 | {
321 | logger.LogTrace("Rewriting .Should().NotThrowAsync() in {Node}", node);
322 |
323 | if (GetGenericTypeArgument(shouldInvocationExpression) is { } genericType)
324 | {
325 | return CreateAssertExpression($"Assert.IsNotType<{genericType}>(await Record.ExceptionAsync({actualValueExpression}))", node);
326 | }
327 |
328 | if (GetTypeOfArgument(shouldInvocationExpression) is { } typeofArgument)
329 | {
330 | return CreateAssertExpression($"Assert.IsNotType({typeofArgument}, await Record.ExceptionAsync({actualValueExpression}))", node);
331 | }
332 |
333 | return CreateAssertExpression($"Assert.Null(await Record.ExceptionAsync({actualValueExpression}))", node);
334 | }
335 |
336 | if (BeOfTypeRegex().IsMatch(shouldInvocationExpressionAsString))
337 | {
338 | logger.LogTrace("Rewriting .Should().BeOfType() in {Node}", node);
339 | if (GetGenericTypeArgument(shouldInvocationExpression) is { } genericType)
340 | {
341 | return CreateAssertExpression($"Assert.True(({actualValueExpression}) is {genericType})", node);
342 | }
343 | if (GetTypeOfArgument(shouldInvocationExpression) is { } typeofArgument)
344 | {
345 | return CreateAssertExpression($"Assert.True(({actualValueExpression}) is {typeofArgument})", node);
346 | }
347 | }
348 |
349 | if (NotBeOfTypeRegex().IsMatch(shouldInvocationExpressionAsString))
350 | {
351 | logger.LogTrace("Rewriting .Should().NotBeOfType() in {Node}", node);
352 | if (GetGenericTypeArgument(shouldInvocationExpression) is { } genericType)
353 | {
354 | return CreateAssertExpression($"Assert.False(({actualValueExpression}) is {genericType})", node);
355 | }
356 | if (GetTypeOfArgument(shouldInvocationExpression) is { } typeofArgument)
357 | {
358 | return CreateAssertExpression($"Assert.False(({actualValueExpression}) is {typeofArgument})", node);
359 | }
360 | }
361 |
362 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeEquivalentTo"))
363 | {
364 | ExpressionSyntax expectedValue =
365 | shouldInvocationExpression.ArgumentList.Arguments.FirstOrDefault()?.Expression
366 | ?? SyntaxFactory.IdentifierName("");
367 | logger.LogTrace("Rewriting .Should().BeEquivalentTo() in {Node}", node);
368 | return CreateAssertExpression($"Assert.Equivalent({expectedValue}, {actualValueExpression})", node);
369 | }
370 |
371 | if (shouldInvocationExpressionAsString.EndsWith(".Should().Contain"))
372 | {
373 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
374 | logger.LogTrace("Rewriting .Should().Contain() in {Node}", node);
375 |
376 | // xUnit API:
377 | // Assert.Contains(someElement, someCollection)
378 | // OR
379 | // Assert.Contains(someCollection, x => somePredicate)
380 | // In FluentAssertions this uses the same Should().Contain(..) method
381 | // so we need to resolve the type of the expected value to know which xUnit overload we need
382 | if (IsLambdaExpression(expectedValue) == true)
383 | {
384 | return CreateAssertExpression($"Assert.Contains({actualValueExpression}, {expectedValue})", node);
385 | }
386 |
387 | return CreateAssertExpression($"Assert.Contains({expectedValue}, {actualValueExpression})", node);
388 | }
389 |
390 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotContain"))
391 | {
392 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
393 | logger.LogTrace("Rewriting .Should().NotContain() in {Node}", node);
394 |
395 | // xUnit API:
396 | // Assert.DoesNotContain(someElement, someCollection)
397 | // OR
398 | // Assert.DoesNotContain(someCollection, x => somePredicate)
399 | // In FluentAssertions this uses the same Should().NotContain(..) method
400 | // so we need to resolve the type of the expected value to know which xUnit overload we need
401 | if (IsLambdaExpression(expectedValue) == true)
402 | {
403 | return CreateAssertExpression($"Assert.DoesNotContain({actualValueExpression}, {expectedValue})", node);
404 | }
405 |
406 | return CreateAssertExpression($"Assert.DoesNotContain({expectedValue}, {actualValueExpression})", node);
407 | }
408 |
409 | if (shouldInvocationExpressionAsString.EndsWith(".Should().HaveCount"))
410 | {
411 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
412 | logger.LogTrace("Rewriting .Should().HaveCount() in {Node}", node);
413 |
414 | // Special case, checking if count is 0
415 | if (expectedValue.ToString() == "0")
416 | {
417 | if (IsNullable(actualValueExpression) == false)
418 | {
419 | return CreateAssertExpression($"Assert.Empty({actualValueExpression})", node);
420 | }
421 | // xUnit's Assert.Empty does not handle null well, so add a fallback for that
422 | return CreateAssertExpression($"Assert.Empty({actualValueExpression} ?? [])", node);
423 | }
424 |
425 | // Special case, checking if count is 1
426 | if (expectedValue.ToString() == "1")
427 | {
428 | if (IsNullable(actualValueExpression) == false)
429 | {
430 | return CreateAssertExpression($"Assert.Single({actualValueExpression})", node);
431 | }
432 | // xUnit's Assert.Single does not handle null well, so add a fallback for that
433 | return CreateAssertExpression($"Assert.Single({actualValueExpression} ?? [])", node);
434 | }
435 |
436 | if (IsArray(actualValueExpression) == true)
437 | {
438 | if (IsNullable(actualValueExpression) == false)
439 | {
440 | return CreateAssertExpression($"Assert.Equal({expectedValue}, ({actualValueExpression}).Length)", node);
441 | }
442 | return CreateAssertExpression($"Assert.Equal({expectedValue}, ({actualValueExpression})?.Length)", node);
443 | }
444 | if (IsCollection(actualValueExpression) == true)
445 | {
446 | if (IsNullable(actualValueExpression) == false)
447 | {
448 | return CreateAssertExpression($"Assert.Equal({expectedValue}, ({actualValueExpression}).Count)", node);
449 | }
450 | return CreateAssertExpression($"Assert.Equal({expectedValue}, ({actualValueExpression})?.Count)", node);
451 | }
452 | if (IsNullable(actualValueExpression) == false)
453 | {
454 | return CreateAssertExpression($"Assert.Equal({expectedValue}, ({actualValueExpression}).Count())", node);
455 | }
456 | return CreateAssertExpression($"Assert.Equal({expectedValue}, ({actualValueExpression})?.Count())", node);
457 | }
458 |
459 | if (shouldInvocationExpressionAsString.EndsWith(".Should().StartWith"))
460 | {
461 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
462 | logger.LogTrace("Rewriting .Should().StartWith() in {Node}", node);
463 | return CreateAssertExpression($"Assert.StartsWith({expectedValue}, {actualValueExpression})", node);
464 | }
465 |
466 | if (shouldInvocationExpressionAsString.EndsWith(".Should().EndWith"))
467 | {
468 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
469 | logger.LogTrace("Rewriting .Should().EndWith() in {Node}", node);
470 | return CreateAssertExpression($"Assert.EndsWith({expectedValue}, {actualValueExpression})", node);
471 | }
472 |
473 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeGreaterThan"))
474 | {
475 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
476 | logger.LogTrace("Rewriting .Should().BeGreaterThan() in {Node}", node);
477 | return CreateAssertExpression($"Assert.True(({actualValueExpression}) > ({expectedValue}))", node);
478 | }
479 |
480 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeLessThan"))
481 | {
482 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
483 | logger.LogTrace("Rewriting .Should().BeLessThan() in {Node}", node);
484 | return CreateAssertExpression($"Assert.True(({actualValueExpression}) < ({expectedValue}))", node);
485 | }
486 |
487 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeBefore"))
488 | {
489 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
490 | logger.LogTrace("Rewriting .Should().BeBefore() in {Node}", node);
491 | return CreateAssertExpression($"Assert.True(({actualValueExpression}) < ({expectedValue}))", node);
492 | }
493 |
494 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBeBefore"))
495 | {
496 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
497 | logger.LogTrace("Rewriting .Should().NotBeBefore() in {Node}", node);
498 | return CreateAssertExpression($"Assert.False(({actualValueExpression}) < ({expectedValue}))", node);
499 | }
500 |
501 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeOnOrBefore"))
502 | {
503 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
504 | logger.LogTrace("Rewriting .Should().BeOnOrBefore() in {Node}", node);
505 | return CreateAssertExpression($"Assert.True(({actualValueExpression}) <= ({expectedValue}))", node);
506 | }
507 |
508 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBeOnOrBefore"))
509 | {
510 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
511 | logger.LogTrace("Rewriting .Should().NotBeOnOrBefore() in {Node}", node);
512 | return CreateAssertExpression($"Assert.False(({actualValueExpression}) <= ({expectedValue}))", node);
513 | }
514 |
515 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeAfter"))
516 | {
517 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
518 | logger.LogTrace("Rewriting .Should().BeAfter() in {Node}", node);
519 | return CreateAssertExpression($"Assert.True(({actualValueExpression}) > ({expectedValue}))", node);
520 | }
521 |
522 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBeAfter"))
523 | {
524 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
525 | logger.LogTrace("Rewriting .Should().NotBeAfter() in {Node}", node);
526 | return CreateAssertExpression($"Assert.False(({actualValueExpression}) > ({expectedValue}))", node);
527 | }
528 |
529 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeOnOrAfter"))
530 | {
531 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
532 | logger.LogTrace("Rewriting .Should().BeOnOrAfter() in {Node}", node);
533 | return CreateAssertExpression($"Assert.True(({actualValueExpression}) >= ({expectedValue}))", node);
534 | }
535 |
536 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBeOnOrAfter"))
537 | {
538 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
539 | logger.LogTrace("Rewriting .Should().NotBeOnOrAfter() in {Node}", node);
540 | return CreateAssertExpression($"Assert.False(({actualValueExpression}) >= ({expectedValue}))", node);
541 | }
542 |
543 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeNullOrEmpty"))
544 | {
545 | logger.LogTrace("Rewriting .Should().BeNullOrEmpty() in {Node}", node);
546 | return CreateAssertExpression($"Assert.False(({actualValueExpression})?.Any() ?? false)", node);
547 | }
548 |
549 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBeNullOrEmpty"))
550 | {
551 | logger.LogTrace("Rewriting .Should().BeNullOrEmpty() in {Node}", node);
552 | if (IsNullable(actualValueExpression) == false)
553 | {
554 | return CreateAssertExpression($"Assert.True(({actualValueExpression}).Any())", node);
555 | }
556 | return CreateAssertExpression($"Assert.True(({actualValueExpression})?.Any())", node);
557 | }
558 |
559 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeCloseTo"))
560 | {
561 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
562 | var precision = shouldInvocationExpression.ArgumentList.Arguments[1].Expression;
563 | logger.LogTrace("Rewriting .Should().BeCloseTo() in {Node}", node);
564 | return CreateAssertExpression(
565 | $"Assert.True(({actualValueExpression}) >= (({expectedValue}) - ({precision})) && ({actualValueExpression}) <= (({expectedValue}) + ({precision})))", node);
566 | }
567 |
568 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotBeCloseTo"))
569 | {
570 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
571 | var precision = shouldInvocationExpression.ArgumentList.Arguments[1].Expression;
572 | logger.LogTrace("Rewriting .Should().NotBeCloseTo() in {Node}", node);
573 | return CreateAssertExpression(
574 | $"Assert.False(({actualValueExpression}) >= (({expectedValue}) - ({precision})) && ({actualValueExpression}) <= (({expectedValue}) + ({precision})))", node);
575 | }
576 |
577 | if (shouldInvocationExpressionAsString.EndsWith(".Should().BeOneOf"))
578 | {
579 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
580 | logger.LogTrace("Rewriting .Should().BeOneOf() in {Node}", node);
581 | return CreateAssertExpression($"Assert.Contains({actualValueExpression}, {expectedValue})", node);
582 | }
583 |
584 | if (shouldInvocationExpressionAsString.EndsWith(".Should().ContainAll"))
585 | {
586 | // Get all the arguments passed to ContainAll
587 | var expectedValues = shouldInvocationExpression.ArgumentList.Arguments
588 | .Select(arg => arg.Expression)
589 | .ToList();
590 |
591 | // Get the target string being tested (e.g. "abc")
592 | var targetString = actualValueExpression;
593 |
594 | // Create a collection expression with all the arguments
595 | var collectionExpression = SyntaxFactory.CollectionExpression(
596 | SyntaxFactory.SeparatedList(
597 | expectedValues.SelectMany(value => new SyntaxNodeOrToken[] {
598 | SyntaxFactory.ExpressionElement(value),
599 | SyntaxFactory.Token(SyntaxKind.CommaToken)
600 | })
601 | .Take(expectedValues.Count * 2 - 1) // Remove the last comma
602 | .ToArray()
603 | )
604 | );
605 |
606 | // Create the lambda for Assert.All
607 | var lambdaParameter = SyntaxFactory.Parameter(
608 | SyntaxFactory.Identifier("substring"));
609 |
610 | var containsExpression = SyntaxFactory.InvocationExpression(
611 | SyntaxFactory.MemberAccessExpression(
612 | SyntaxKind.SimpleMemberAccessExpression,
613 | SyntaxFactory.IdentifierName("Assert"),
614 | SyntaxFactory.IdentifierName("Contains")))
615 | .WithArgumentList(
616 | SyntaxFactory.ArgumentList(
617 | SyntaxFactory.SeparatedList(new[] {
618 | SyntaxFactory.Argument(SyntaxFactory.IdentifierName("substring")),
619 | SyntaxFactory.Argument(targetString)
620 | })));
621 |
622 | var lambda = SyntaxFactory.SimpleLambdaExpression(
623 | lambdaParameter,
624 | containsExpression);
625 |
626 | // Create the full Assert.All expression
627 | var assertAllExpression = SyntaxFactory.InvocationExpression(
628 | SyntaxFactory.MemberAccessExpression(
629 | SyntaxKind.SimpleMemberAccessExpression,
630 | SyntaxFactory.IdentifierName("Assert"),
631 | SyntaxFactory.IdentifierName("All")))
632 | .WithArgumentList(
633 | SyntaxFactory.ArgumentList(
634 | SyntaxFactory.SeparatedList([
635 | SyntaxFactory.Argument(collectionExpression),
636 | SyntaxFactory.Argument(lambda)
637 | ])));
638 |
639 | logger.LogTrace("Rewriting .Should().ContainAll() in {Node}", node);
640 | return CreateAssertExpression(assertAllExpression, node);
641 | }
642 |
643 | if (shouldInvocationExpressionAsString.EndsWith(".Should().ContainSingle"))
644 | {
645 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments.Count > 0 ? shouldInvocationExpression.ArgumentList.Arguments[0].Expression : null;
646 | logger.LogTrace("Rewriting .Should().ContainSingle() in {Node}", node);
647 |
648 | if (expectedValue != null && IsLambdaExpression(expectedValue) == true)
649 | {
650 | return CreateAssertExpression($"Assert.Single({actualValueExpression}, {expectedValue})", node);
651 | }
652 |
653 | return CreateAssertExpression($"Assert.Single({actualValueExpression})", node);
654 | }
655 |
656 | if (shouldInvocationExpressionAsString.EndsWith(".Should().ContainEquivalentOf"))
657 | {
658 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
659 | logger.LogTrace("Rewriting .Should().ContainEquivalentOf() in {Node}", node);
660 | return CreateAssertExpression($"Assert.Contains({expectedValue}, {actualValueExpression}, StringComparison.OrdinalIgnoreCase)", node);
661 | }
662 |
663 | if (shouldInvocationExpressionAsString.EndsWith(".Should().NotContainEquivalentOf"))
664 | {
665 | var expectedValue = shouldInvocationExpression.ArgumentList.Arguments[0].Expression;
666 | logger.LogTrace("Rewriting .Should().NotContainEquivalentOf() in {Node}", node);
667 | return CreateAssertExpression($"Assert.DoesNotContain({expectedValue}, {actualValueExpression}, StringComparison.OrdinalIgnoreCase)", node);
668 | }
669 |
670 | return base.VisitInvocationExpression(shouldInvocationExpression);
671 | }
672 |
673 | // Tries to resolve the part before .Should() when conditional access is used
674 | // e.g. myObject?.MyProperty.Should().Be(...)
675 | private static bool TryResolveActualValueFromConditionalAccessExpression(
676 | ConditionalAccessExpressionSyntax conditionalAccess,
677 | [NotNullWhen(true)] out InvocationExpressionSyntax? shouldInvocation,
678 | [NotNullWhen(true)] out ExpressionSyntax? actualValueExpression)
679 | {
680 | // Dig all the way down to the deepest WhenNotNull conditional access
681 | List conditionalAccesses = [conditionalAccess];
682 | ConditionalAccessExpressionSyntax current = conditionalAccess;
683 | while (current.WhenNotNull is ConditionalAccessExpressionSyntax nestedConditionalAccess)
684 | {
685 | conditionalAccesses.Add(nestedConditionalAccess);
686 | current = nestedConditionalAccess;
687 | }
688 |
689 | // Take the deepest WhenNotNull and try to resolve the actual value expression from it
690 | var deepestConditionalAccess = conditionalAccesses[^1];
691 | if (deepestConditionalAccess.WhenNotNull is InvocationExpressionSyntax possibleShouldInvocationExpression
692 | && TryResolveActualValueFromShouldInvocationExpression(possibleShouldInvocationExpression,
693 | out var finalActualValueExpression))
694 | {
695 | var newConditionalAccess = deepestConditionalAccess.WithWhenNotNull(finalActualValueExpression);
696 |
697 | if (conditionalAccesses.Count > 1)
698 | {
699 | for (var i = conditionalAccesses.Count - 2; i >= 0; i--)
700 | {
701 | newConditionalAccess = conditionalAccesses[i].WithWhenNotNull(newConditionalAccess);
702 | }
703 | }
704 |
705 | shouldInvocation = possibleShouldInvocationExpression;
706 | actualValueExpression = newConditionalAccess;
707 | return true;
708 | }
709 |
710 | shouldInvocation = null;
711 | actualValueExpression = null;
712 | return false;
713 | }
714 |
715 | // Tries to resolve the part before .Should() when await is used
716 | // e.g. await someAction.Should().ThrowAsync(...)
717 | private static bool TryResolveActualValueFromAwaitExpression(
718 | AwaitExpressionSyntax awaitExpression,
719 | [NotNullWhen(true)] out InvocationExpressionSyntax? shouldInvocation,
720 | [NotNullWhen(true)] out ExpressionSyntax? actualValueExpression)
721 | {
722 | if (awaitExpression.Expression is InvocationExpressionSyntax invocationExpression
723 | && TryResolveActualValueFromShouldInvocationExpression(invocationExpression, out var innerActualValueExpression))
724 | {
725 | actualValueExpression = innerActualValueExpression;
726 | shouldInvocation = invocationExpression;
727 | return true;
728 | }
729 |
730 | actualValueExpression = null;
731 | shouldInvocation = null;
732 | return false;
733 | }
734 |
735 | // Tries to resolve the part before .Should()
736 | // e.g. myVariable.Should().Be(...)
737 | private static bool TryResolveActualValueFromShouldInvocationExpression(
738 | InvocationExpressionSyntax possibleShouldInvocationExpression,
739 | [NotNullWhen(true)] out ExpressionSyntax? actualValueExpression)
740 | {
741 | if (possibleShouldInvocationExpression.Expression is MemberAccessExpressionSyntax
742 | {
743 | Expression: InvocationExpressionSyntax innerInvocation
744 | }
745 | && innerInvocation.Expression.ToString().EndsWith(".Should"))
746 | {
747 | if (innerInvocation.Expression is MemberAccessExpressionSyntax shouldMemberAccess)
748 | {
749 | actualValueExpression = shouldMemberAccess.Expression;
750 | return true;
751 | }
752 | }
753 |
754 | actualValueExpression = null;
755 | return false;
756 | }
757 |
758 | private TypeSyntax? GetGenericTypeArgument(InvocationExpressionSyntax shouldInvocationExpression, int index = 0)
759 | {
760 | var typeArguments = shouldInvocationExpression
761 | .DescendantNodes()
762 | .OfType()
763 | .FirstOrDefault()
764 | ?.TypeArgumentList
765 | .Arguments;
766 |
767 | if (typeArguments is null || typeArguments.Value.Count <= index)
768 | {
769 | return null;
770 | }
771 |
772 | return typeArguments.Value[index];
773 | }
774 |
775 | private TypeSyntax? GetTypeOfArgument(InvocationExpressionSyntax shouldInvocationExpression, int index = 0)
776 | {
777 | var arguments = shouldInvocationExpression.ArgumentList.Arguments;
778 | if (arguments.Count <= index)
779 | {
780 | return null;
781 | }
782 |
783 | return arguments[index].Expression is TypeOfExpressionSyntax typeOfExpression
784 | ? typeOfExpression.Type
785 | : null;
786 | }
787 |
788 | private static ExpressionSyntax CreateAssertExpression(string assertCode, ExpressionSyntax originalNode)
789 | {
790 | return SyntaxFactory.ParseExpression(assertCode)
791 | .WithLeadingTrivia(originalNode.GetLeadingTrivia())
792 | .WithTrailingTrivia(originalNode.GetTrailingTrivia());
793 | }
794 |
795 | private static ExpressionSyntax CreateAssertExpression(ExpressionSyntax assertCode, ExpressionSyntax originalNode)
796 | {
797 | return assertCode
798 | .WithLeadingTrivia(originalNode.GetLeadingTrivia())
799 | .WithTrailingTrivia(originalNode.GetTrailingTrivia());
800 | }
801 |
802 | private bool? IsLambdaExpression(ExpressionSyntax expression)
803 | {
804 | try
805 | {
806 | var semanticModel = lazySemanticModel.Value.GetAwaiter().GetResult();
807 | var typeInfo = semanticModel.GetTypeInfo(expression);
808 | var type = typeInfo.Type ?? typeInfo.ConvertedType;
809 | var typeFullName = type?.ToString();
810 | if (typeFullName is null)
811 | {
812 | return null;
813 | }
814 |
815 | return typeFullName.StartsWith("System.Predicate")
816 | || typeFullName.StartsWith("System.Func")
817 | || typeFullName.StartsWith("System.Action")
818 | || typeFullName.StartsWith("System.Linq.Expressions");
819 | }
820 | catch
821 | {
822 | return null;
823 | }
824 | }
825 |
826 | private bool? IsCollection(ExpressionSyntax expression)
827 | {
828 | try
829 | {
830 | var semanticModel = lazySemanticModel.Value.GetAwaiter().GetResult();
831 | var typeInfo = semanticModel.GetTypeInfo(expression);
832 | var type = typeInfo.Type ?? typeInfo.ConvertedType;
833 | if (type is null)
834 | {
835 | return null;
836 | }
837 |
838 | // Check if type implements ICollection
839 | return type.ToString()!.StartsWith("System.Collections.Generic.ICollection<")
840 | || type.AllInterfaces.Any(i =>
841 | i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_ICollection_T ||
842 | i.ToString()?.StartsWith("System.Collections.Generic.ICollection<") == true);
843 | }
844 | catch
845 | {
846 | return null;
847 | }
848 | }
849 |
850 | private bool? IsArray(ExpressionSyntax expression)
851 | {
852 | try
853 | {
854 | var semanticModel = lazySemanticModel.Value.GetAwaiter().GetResult();
855 | var typeInfo = semanticModel.GetTypeInfo(expression);
856 | var type = typeInfo.Type ?? typeInfo.ConvertedType;
857 | if (type == null)
858 | {
859 | return null;
860 | }
861 |
862 | return type.TypeKind == TypeKind.Array;
863 | }
864 | catch
865 | {
866 | return null;
867 | }
868 | }
869 |
870 | private bool? IsNullable(ExpressionSyntax expression)
871 | {
872 | try
873 | {
874 | if (expression is ConditionalAccessExpressionSyntax)
875 | {
876 | return true;
877 | }
878 |
879 | var semanticModel = lazySemanticModel.Value.GetAwaiter().GetResult();
880 | var typeInfo = semanticModel.GetTypeInfo(expression);
881 | var type = typeInfo.Type;
882 |
883 | if (type == null)
884 | {
885 | return null;
886 | }
887 |
888 | // Check for nullable value types (Nullable)
889 | if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
890 | {
891 | return true;
892 | }
893 |
894 | // Check for nullable reference types
895 | return type.NullableAnnotation == NullableAnnotation.Annotated;
896 | }
897 | catch
898 | {
899 | return null;
900 | }
901 | }
902 |
903 | [GeneratedRegex(@"\.Should\(\)\.Throw(?:<[^>]+>)?$")]
904 | private static partial Regex ThrowRegex();
905 |
906 | [GeneratedRegex(@"\.Should\(\)\.NotThrow(?:<[^>]+>)?$")]
907 | private static partial Regex NotThrowRegex();
908 |
909 | [GeneratedRegex(@"\.Should\(\)\.ThrowAsync(?:<[^>]+>)?$")]
910 | private static partial Regex ThrowAsyncRegex();
911 |
912 | [GeneratedRegex(@"\.Should\(\)\.NotThrowAsync(?:<[^>]+>)?$")]
913 | private static partial Regex NotThrowAsyncRegex();
914 |
915 | [GeneratedRegex(@"\.Should\(\)\.BeOfType(?:<[^>]+>)?$")]
916 | private static partial Regex BeOfTypeRegex();
917 |
918 | [GeneratedRegex(@"\.Should\(\)\.NotBeOfType(?:<[^>]+>)?$")]
919 | private static partial Regex NotBeOfTypeRegex();
920 | }
921 |
--------------------------------------------------------------------------------