
Using Regex to migrate from Fluent Assertions to XUnit Assertions
Author - Abdul Rahman (Bhai)
Regex
1 Articles
Table of Contents
What we gonna do?
January 2025 started with a big surprise from FluentAsserions, a popular free assertion library for .NET. The library maintainers decided to make the library paid one and started charging for it from v8.0. This decision made me realise that it have introduced a technical debt in all the projects in have worked so far over a decade by using this library.
I took This opportunity to pay off the debt I introduced and decided to use AI / LLM to give a prompt and convert to XUnit Assertions. I tried with ollama and it worked when I tried with prompt for single assertion statement in terminal but became very lazy and slow when I was running against multiple test files inside the repo.
So I thought I'm using wrong tool to solve the problem. It's then decided to use simple Regex to make this migration. when I tried and viola the entire repository with 1000+ test methods and all FluentAsserions statements got converted to XUnit Assertions within seconds. I was able to achieve 90% plus conversion in repository and left with small manual cleanup which took and hour to fix.
Why we gonna do?
Our client technical directors decided to move away from FluentAssertions and use XUnit assertions as we are a large enterprise with 1000+ developers and not ready to spend 130$ for each. Even though FluentAsserions team is ready to provide security patches for v7, we decided not to go forward and blocked FluentAsserions from our private Nuget Feed.
The trade off to pay and use FluentAsserions vs cost to migrate all repository to XUnit assertion kicked off this idea and now we are successful in that. This was possible because we have followed strict coding standards and kept our assertion statements simple to one line.
How we gonna do?
Here are the steps that I have followed.
Find all test files and group them by folder.
var testProjectPath = "/Users/ilovedotnet/Projects/FluentAssertionToXUnitAssertion"; var filesByFolder = Directory .EnumerateFiles(testProjectPath, "*.cs", SearchOption.AllDirectories) .Where(file => file.EndsWith("Tests.cs", StringComparison.OrdinalIgnoreCase)) .GroupBy(Path.GetDirectoryName) .ToList();Parallely Process each file.
foreach (var folderGroup in filesByFolder) { Console.WriteLine($"Processing folder group: {folderGroup.Key}"); var tasks = folderGroup.Select(ProcessWithRegex); await foreach (var result in Task.WhenEach(tasks)) { Console.WriteLine(result); // Log results if needed } }Read lines in each file. If the lines contains .Should(). then Process it and convert to XUnit Assertions and replace the line. Finally save the test file.
static async Task ProcessWithRegex(string filePath) { var lines = await File.ReadAllLinesAsync(filePath); var updatedLines = new StringBuilder(); var converted = false; foreach (var line in lines) { if (line.Contains(".Should().")) { var result = ConvertFluentAssertionsToXUnit(line); updatedLines.AppendLine(result); converted = true; } else { updatedLines.AppendLine(line); } } if (converted) await File.WriteAllTextAsync(filePath, updatedLines.ToString()); }The secret sauce Regex that I used for commonly used FluentAsserions statements.
static string ConvertFluentAssertionsToXUnit(string input) { // 1. Replace .Should().Be with Assert.Equal input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.Be\((.*)\);", @"Assert.Equal($2, $1);"); // 2. Replace .Should().Equal() with Assert.Equal() input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.Equal\((.*)\);", @"Assert.Equal($2, $1);"); // 3. Replace .Should().NotBe with Assert.NotEqual input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.NotBe\((.*)\);", @"Assert.NotEqual($2, $1);"); // 4. Replace .Should().NotBeNull with Assert.NotNull input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.NotBeNull\(\);", @"Assert.NotNull($1);"); // 5. Replace .Should().BeTrue with Assert.True input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.BeTrue\(\);", @"Assert.True($1);"); // 6. Replace .Should().BeFalse with Assert.False input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.BeFalse\(\);", @"Assert.False($1);"); // 7. Replace .Should().Contain with Assert.Contains input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.Contain\((.*)\);", @"Assert.Contains($2, $1);"); // 8. Replace .Should().NotContain with Assert.DoesNotContain input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.NotContain\((.*)\);", @"Assert.DoesNotContain($2, $1);"); // 9. Replace .Should().BeEquivalentTo with Assert.Equivalent input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.BeEquivalentTo\((.*)\);", @"Assert.Equivalent($2, $1);"); // 10. Handle .Should().BeGreaterThanOrEqualTo input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.BeGreaterThanOrEqualTo\((.*)\);", @"Assert.True($1 >= $2);"); // 11. Handle .Should().HaveCount with Assert.Equal input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.HaveCount\((\d+)\);", @"Assert.Equal($2, $1.Count);"); // 12. Convert .Should().BeEmpty() to Assert.Empty input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.BeEmpty\(\);", @"Assert.Empty($1);"); // 13. Convert .Should().BeNull() to Assert.Null() input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.BeNull\(\);", @"Assert.Null($1);"); // 14. Convert .Should().HaveCountGreaterThanOrEqualTo() to Assert.True input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.HaveCountGreaterThanOrEqualTo\((.*)\);", @"Assert.True($1.Count >= $2);"); // 15. Convert .Should().Throw<T>().WithMessage("...") to Assert.Throws<T>() input = Regex.Replace(input, @"(\w+)\.Should\(\)\.Throw<([^>]+)>\(\)\.WithMessage\((.*)\);", @"var ex = Assert.Throws<$2>($1); Assert.Equal($3, ex.Message);"); // 16. Replace .Should().BeOfType with Assert.IsType input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+)*))\.Should\(\)\.BeOfType<([^>]+)>\(\);", @"Assert.IsType<$2>($1);"); // 17. Handle BeCloseTo separately (converted to InRange) input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+)*))\.Should\(\)\.BeCloseTo\((.*), (.*)\);", @"Assert.InRange($1, $2 - $3, $2 + $3);"); // 18. Convert .Should().NotBeEmpty() to Assert.NotEmpty() input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.NotBeEmpty\(\);", @"Assert.NotEmpty($1);"); // 19. Convert .Should().HaveCountGreaterOrEqualTo() to Assert.True input = Regex.Replace(input, @"((?:[\w\[\]\.!]+(?:\.\w+|\(.*\))*))\.Should\(\)\.HaveCountGreaterOrEqualTo\((.*)\);", @"Assert.True($1.Count >= $2);"); return input; }
Here is the sample input and output.
using FluentAssertions;
namespace ILoveDotNet.FluentAssertionToXUnitAssertion;
public class Tests
{
public void FluentAssertionsToConvert()
{
var result = new FluentAssertionDto();
var results = new List<AnotherFluentAssertionDto>();
Action act = () =>
{
Error error = new();
};
act.Should().Throw<Exception>().WithMessage("Error");
result.Strings.Count.Should().BeGreaterThanOrEqualTo(1);
result.Strings[0].Should().Contain("XUnit");
result.Strings[0].Should().Be("XUnit");
results.Count.Should().Be(0);
results.Should().NotBeNull();
results.Should().BeNull();
result.Boolean.Should().BeTrue();
result.Boolean.Should().BeFalse();
result.Numbers.Should().NotContain(1);
result.Numbers.Should().HaveCount(0);
result.Numbers.Should().Equal(result.Numbers);
result.Numbers.Should().HaveCountGreaterThanOrEqualTo(0);
result.Now.Should().BeCloseTo(DateTimeOffset.Now, TimeSpan.FromMinutes(10));
results[0].Now.Should().BeCloseTo(DateTimeOffset.Now, TimeSpan.FromMinutes(10));
results[0].Number.Should().Be(0);
results[0].Numbers.Count.Should().BeGreaterThanOrEqualTo(0);
results[0].Numbers[0].Should().Be(0);
results[0].Persons[0].Name.Should().Be(string.Empty);
results[0].Persons![0].Name!.Should().BeEmpty();
result.Should().NotBe(null);
result.Should().BeEquivalentTo(result);
result.Should().BeOfType<FluentAssertionDto>();
result!.Should().Be(result);
result.Numbers.Sum().Should().Be(1);
results.Sum(x => x.Number).Should().Be(0);
results[0].Numbers.Sum().Should().Be(0);
results.Should().NotBeEmpty();
result.Numbers.Should().HaveCountGreaterOrEqualTo(0);
}
}
public class FluentAssertionDto
{
public List<string> Strings { get; set; } = ["XUnit"];
public List<int> Numbers { get; set; } = [1];
public bool Boolean { get; set; }
public DateTimeOffset Now { get; set; } = DateTimeOffset.UtcNow;
}
public class AnotherFluentAssertionDto
{
public int Number { get; set; }
public DateTimeOffset Now { get; set; } = DateTimeOffset.UtcNow;
public List<int> Numbers { get; set; } = [];
public List<Person> Persons { get; set; } = [];
}
public class Person
{
public string Name { get; set; }
}
public class Error
{
public Error()
{
throw new Exception("Error");
}
}
using Xunit;
namespace ILoveDotNet.FluentAssertionToXUnitAssertion;
public class Tests
{
public void FluentAssertionsToConvert()
{
var result = new FluentAssertionDto();
var results = new List<AnotherFluentAssertionDto>();
Action act = () =>
{
Error error = new();
};
var ex = Assert.Throws<Exception>(act); Assert.Equal("Error", ex.Message);
Assert.True(result.Strings.Count >= 1);
Assert.Contains("XUnit", result.Strings[0]);
Assert.Equal("XUnit", result.Strings[0]);
Assert.Equal(0, results.Count);
Assert.NotNull(results);
Assert.Null(results);
Assert.True(result.Boolean);
Assert.False(result.Boolean);
Assert.DoesNotContain(1, result.Numbers);
Assert.Equal(0, result.Numbers.Count);
Assert.Equal(result.Numbers, result.Numbers);
Assert.True(result.Numbers.Count >= 0);
Assert.InRange(result.Now, DateTimeOffset.Now - TimeSpan.FromMinutes(10), DateTimeOffset.Now + TimeSpan.FromMinutes(10));
Assert.InRange(results[0].Now, DateTimeOffset.Now - TimeSpan.FromMinutes(10), DateTimeOffset.Now + TimeSpan.FromMinutes(10));
Assert.Equal(0, results[0].Number);
Assert.True(results[0].Numbers.Count >= 0);
Assert.Equal(0, results[0].Numbers[0]);
Assert.Equal(string.Empty, results[0].Persons[0].Name);
Assert.Empty(results[0].Persons![0].Name!);
Assert.NotEqual(null, result);
Assert.Equivalent(result, result);
Assert.IsType<FluentAssertionDto>(result);
Assert.Equal(result, result!);
Assert.Equal(1, result.Numbers.Sum());
Assert.Equal(0, results.Sum(x => x.Number));
Assert.Equal(0, results[0].Numbers.Sum());
Assert.NotEmpty(results);
Assert.True(result.Numbers.Count >= 0);
}
}
Summary
In this article we learnt about how to use regular expression to migrate from FluentAsserions to XUnit Assertions. Regex are very powerful and I realised when I was able make this conversion happen within seconds. We save huge money and time and achieved 90% results using this approach. The idea is open now and you can extend and use it in your repos. I have never imagined that I'll use Regex for any task other than Email Validation.