fluentassertions / fluentassertions

A very extensive set of extension methods that allow you to more naturally specify the expected outcome of a TDD or BDD-style unit tests. Targets .NET Framework 4.7, as well as .NET Core 2.1, .NET Core 3.0, .NET 6, .NET Standard 2.0 and 2.1. Supports the unit test frameworks MSTest2, NUnit3, XUnit2, MSpec, and NSpec3.
https://www.fluentassertions.com
Apache License 2.0
3.77k stars 548 forks source link

[API Proposal]: Allow to assert RegEx matched groups #2625

Open rklec opened 7 months ago

rklec commented 7 months ago

Background and motivation

Suppose I have the following rgeular expression (regex): (abc)(\d{3})([X-Z]*) (https://regex101.com/r/Aszkcm/1)

I can write it like this, and this is fine:

[Test]
public void SomethingReturnsValidCode()
{
    var resultOfSystemUnderTest = "abc123XY";

    resultOfSystemUnderTest.Should().MatchRegex(new Regex(@"(abc?)(\d{3})([X-Z]*)"));
}        

However, I have, AFAIK, no cool way to match the actual groups of the RegEx.

My motivation behind this here is validating the syntax of a code generator. It should, under some circumstances, e.g. return "123" as the second group in the RegEx. Note, as this assertion needs to target the middle of the string, it is very hard to match and assert without using RegEx (and arguably maybe as ugly, e.g. if as in my case the start and end could be different lengths...). More complex use cases are possible, of course.

API Proposal

public class StringAssertions<TEnum, TAssertions>
{
    public AndConstraint<TAssertions> MatchRegex(Regex regularExpression,
        string because = "", params object[] becauseArgs)
}

Maybe MatchRegex just needs to be expanded to return an extended TAssertions that can handle the regex then?

API Usage

[Test]
public void SomethingReturnsValidCode()
{
    var resultOfSystemUnderTest = "abc123XY";

    resultOfSystemUnderTest.Should().MatchRegex(new Regex(@"(abc?)(\d{3})([X-Z]*)"))
        .And.Group[2].Should().Be("123")
        .And.Group[3].Should().Be("XY");
}

...or similar. (Indexers probably need to be used, as potentially many matches are possible).

Alternative Designs

Current solution would be (Chaining is also not really possible here):

resultOfSystemUnderTest.Match(result).Groups[2].Value.Should().Be("123"); // etc.

Risks

IMHO/AFAIK no breaking changes, performance should also be the same as the existing matches, because AFAIK the groups are anyway evaluated by C# if you match it, but I am not sure.

Are you willing to help with a proof-of-concept (as PR in that or a separate repo) first and as pull-request later on?

No

IT-VBFK commented 6 months ago

What about this (to avoid this indexers):

[Test]
public void SomethingReturnsValidCode()
{
    var resultOfSystemUnderTest = "abc123XY";

    resultOfSystemUnderTest.Should().MatchRegex(new Regex(@"(abc?)(\d{3})([X-Z]*)"))
        .Which.Groups.Should().SatisfyRespectively(group0 => {}, groupX => { groupX.Should().Be("123") }, ...);
}

The only drawback is, that you have to add all inspectors (like group0), which are empty if you want to ignore them...

jnyrup commented 6 months ago

If the method signature MatchRegex is changed to AndWhichConstraint<TAssertions, Match> and #2597 gets merged, it could even be written as

[Test]
public void SomethingReturnsValidCode()
{
    var resultOfSystemUnderTest = "abc123XY";

    resultOfSystemUnderTest.Should().MatchRegex(new Regex(@"(abc?)(\d{3})([X-Z]*)"))
        .Which.Should().Satisfy<Match>(match =>
        {
            match.Groups[2].Should().Be("123");
            match.Groups[3].Should().Be("XY");
        });
}

One microscopic downside is that MatchRegex will need to switch from the lighter Regex.IsMatch to Regex.Match, but I don't think that will be noticeable from test.

dennisdoomen commented 6 months ago

Or even more idiomatic FA like

var resultOfSystemUnderTest = "abc123XY";

resultOfSystemUnderTest.Should().MatchRegex(@"(abc?)(\d{3})([X-Z]*)")
    .WithGroup(2).Being("123")
    .And.WithGroup(3).Being("XY");