Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a new overload to MatchRegex() to assert on the number of regex matches #1869

Merged
merged 15 commits into from Apr 1, 2022
Merged
90 changes: 89 additions & 1 deletion Src/FluentAssertions/Primitives/StringAssertions.cs
Expand Up @@ -387,6 +387,42 @@ public AndConstraint<TAssertions> NotMatch(string wildcardPattern, string becaus
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that a string matches a regular expression with expected occurrence
/// </summary>
/// <param name="regularExpression">
/// The regular expression with which the subject is matched.
/// </param>
/// <param name="occurrenceConstraint">
/// The excpected occurences of regex matches
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> MatchRegex([RegexPattern][StringSyntax("Regex")] string regularExpression,
OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs)
{
IT-VBFK marked this conversation as resolved.
Show resolved Hide resolved
Guard.ThrowIfArgumentIsNull(regularExpression, nameof(regularExpression), "Cannot match string against <null>. Provide a regex pattern or use the BeNull method.");

Regex regex;
try
{
regex = new Regex(regularExpression);
}
catch (ArgumentException)
{
Execute.Assertion.FailWith("Cannot match {context:string} against {0} because it is not a valid regular expression.",
regularExpression);
return new AndConstraint<TAssertions>((TAssertions)this);
}

return MatchRegex(regex, occurrenceConstraint, because, becauseArgs);
}

/// <summary>
/// Asserts that a string matches a regular expression.
/// </summary>
Expand Down Expand Up @@ -420,6 +456,57 @@ public AndConstraint<TAssertions> NotMatch(string wildcardPattern, string becaus
return MatchRegex(regex, because, becauseArgs);
}

/// <summary>
/// Asserts that a string matches a regular expression with expected occurrence
/// </summary>
/// <param name="regularExpression">
/// The regular expression with which the subject is matched.
/// </param>
/// <param name="occurrenceConstraint">
/// The excpected occurences of regex matches
IT-VBFK marked this conversation as resolved.
Show resolved Hide resolved
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> MatchRegex(Regex regularExpression,
OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(regularExpression, nameof(regularExpression),
"Cannot match string against <null>. Provide a regex pattern or use the BeNull method.");

var regexStr = regularExpression.ToString();
if (regexStr.Length == 0)
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
{
throw new ArgumentException(
"Cannot match string against an empty string. Provide a regex pattern or use the BeEmpty method.",
nameof(regularExpression));
}

bool success = Execute.Assertion
.ForCondition(Subject is not null)
.UsingLineBreaks
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:string} to match regex {0}{reason}, but it was <null>.", regexStr);

if (success)
{
int actual = regularExpression.Matches(Subject).Count;

Execute.Assertion
.ForConstraint(occurrenceConstraint, actual)
.UsingLineBreaks
.BecauseOf(because, becauseArgs)
.FailWith($"Expected {{context:string}} to match regex {{0}} {{expectedOccurrence}}{{reason}}, but found it {actual.Times()}.",
regularExpression.ToString());
IT-VBFK marked this conversation as resolved.
Show resolved Hide resolved
}

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that a string matches a regular expression.
/// </summary>
Expand All @@ -433,7 +520,8 @@ public AndConstraint<TAssertions> NotMatch(string wildcardPattern, string becaus
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> MatchRegex(Regex regularExpression, string because = "", params object[] becauseArgs)
public AndConstraint<TAssertions> MatchRegex(Regex regularExpression,
string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(regularExpression, nameof(regularExpression),
"Cannot match string against <null>. Provide a regex pattern or use the BeNull method.");
Expand Down
Expand Up @@ -2121,6 +2121,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> MatchEquivalentOf(string wildcardPattern, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBe(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -2204,6 +2204,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> MatchEquivalentOf(string wildcardPattern, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBe(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -2121,6 +2121,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> MatchEquivalentOf(string wildcardPattern, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBe(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -2121,6 +2121,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> MatchEquivalentOf(string wildcardPattern, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBe(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -2073,6 +2073,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> MatchEquivalentOf(string wildcardPattern, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBe(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -2121,6 +2121,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> MatchEquivalentOf(string wildcardPattern, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(string regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> MatchRegex(System.Text.RegularExpressions.Regex regularExpression, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBe(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -199,6 +199,62 @@ public void When_a_string_is_matched_against_an_empty_regex_it_should_throw_with
.WithParameterName("regularExpression");
}

[Fact]
public void When_a_string_is_matched_against_a_regex_and_the_count_of_matches_are_exactly_the_expected_it_passes()
{
// Arrange
string subject = "hello world";

// Act
Action act = () => subject.Should().MatchRegex(new Regex("hello.*"), Exactly.Once());

// Assert
act.Should().NotThrow();
}

[Fact]
public void When_a_string_is_matched_against_a_regex_and_the_count_of_matches_are_not_the_expected_it_fails()
{
// Arrange
string subject = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt " +
"ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et " +
"ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.";

// Act
Action act = () => subject.Should().MatchRegex("Lorem.*", Exactly.Twice());

// Assert
act.Should().Throw<XunitException>()
.WithMessage($"Expected subject to match regex*\"Lorem.*\" exactly 2 times, but found it 1 time.");
}

[Fact]
public void When_a_string_is_matched_against_a_regex_and_the_expected_count_is_zero_and_string_not_matches_it_passes()
{
// Arrange
string subject = "a";

// Act
Action act = () => subject.Should().MatchRegex("b", Exactly.Times(0));

// Assert
act.Should().NotThrow();
}

[Fact]
public void When_a_string_is_matched_against_a_regex_and_the_expected_count_is_zero_and_string_matches_it_fails()
{
// Arrange
string subject = "a";

// Act
Action act = () => subject.Should().MatchRegex("a", Exactly.Times(0));

// Assert
act.Should().Throw<XunitException>()
.WithMessage($"Expected subject to match regex*\"a\" exactly 0 times, but found it 1 time.");
}

#endregion

#region Not Match Regex
Expand Down
2 changes: 2 additions & 0 deletions docs/_pages/releases.md
Expand Up @@ -14,6 +14,8 @@ sidebar:
* Added support for .NET6 `DateOnly` struct - [#1844](https://github.com/fluentassertions/fluentassertions/pull/1844)
* Added support for .NET6 `TimeOnly` struct - [#1848](https://github.com/fluentassertions/fluentassertions/pull/1848)
* Added `NotBe` for nullable boolean values - [#1865](https://github.com/fluentassertions/fluentassertions/pull/1865)
* Added a new overload to `MatchRegex()` to be able to assert on count of regex matches - [#1869](https://github.com/fluentassertions/fluentassertions/pull/1869)

IT-VBFK marked this conversation as resolved.
Show resolved Hide resolved

### Fixes
* `EnumAssertions.Be` did not determine the caller name - [#1835](https://github.com/fluentassertions/fluentassertions/pull/1835)
Expand Down
7 changes: 7 additions & 0 deletions docs/_pages/strings.md
Expand Up @@ -108,3 +108,10 @@ someString.Should().MatchRegex(new System.Text.RegularExpressions.Regex("h.*\\sw
subject.Should().NotMatchRegex(new System.Text.RegularExpressions.Regex(".*earth.*"));
subject.Should().NotMatchRegex(".*earth.*");
```

And if that's not enough, you can assert on the number of matches of a regular expression:

```csharp
someString.Should().MatchRegex("h.*\\sworld.$", Exactly.Once());
someString.Should().MatchRegex(new Regex("h.*\\sworld.$"), AtLeast.Twice());
```