├── 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 | --------------------------------------------------------------------------------