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

Add more specific variants of contain (continued) #1145

Merged
merged 39 commits into from
Nov 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9c0a064
Add more specific variants of contain (#818)
mkolumb Nov 10, 2018
107aa8f
Pull request suggestion (#818)
mkolumb Nov 13, 2018
000fc96
Merge work of @mkolumb
danielmpetrov Sep 21, 2019
572822d
Fix build
danielmpetrov Sep 21, 2019
be381a9
Make API backwards compatible
danielmpetrov Sep 22, 2019
c26fe31
WIP - Generate proper messages
danielmpetrov Sep 23, 2019
6126db2
Add fallback message
danielmpetrov Sep 23, 2019
60fbde2
Delete extra classes
danielmpetrov Sep 23, 2019
bf8acee
Refactoring
danielmpetrov Sep 23, 2019
2ecd5d1
WIP
danielmpetrov Sep 23, 2019
d82f396
Add Times
danielmpetrov Sep 23, 2019
424c837
Refactoring
danielmpetrov Sep 23, 2019
280e5dd
Refactoring
danielmpetrov Sep 23, 2019
4605c10
Refactoring
danielmpetrov Sep 23, 2019
7f96239
Update docs
danielmpetrov Sep 23, 2019
0998c52
Update code docs and refactor
danielmpetrov Sep 23, 2019
2a15a4b
Merge branch 'master' into gh_818
danielmpetrov Oct 29, 2019
9f2dcd6
Rename to OccurrenceConstraint
danielmpetrov Oct 29, 2019
f6dace1
Move OccuranceConstraint to root namespace
danielmpetrov Oct 29, 2019
8d4792f
Mark constructor protected
danielmpetrov Oct 29, 2019
989f37e
Move Times(int count) below callers
danielmpetrov Oct 29, 2019
b5cc5dc
Reorganize types
danielmpetrov Oct 29, 2019
c1d50cf
Leave single example per constraint type
danielmpetrov Oct 29, 2019
8174816
Fix weird message
danielmpetrov Oct 29, 2019
d49572f
Use light AAA syntax
danielmpetrov Oct 29, 2019
980b2b7
Move assert out of OccurrenceConstraint
danielmpetrov Oct 30, 2019
e34a561
Formatting
danielmpetrov Oct 30, 2019
ed5e354
Naming
danielmpetrov Oct 30, 2019
acafaa7
Add negative expected count test
danielmpetrov Oct 30, 2019
0f84409
Merge branch 'master' into gh_818
danielmpetrov Nov 9, 2019
9c18a03
Fix typos and doc strings
danielmpetrov Nov 9, 2019
8eebae2
Remove duplicate tests when expected is null
danielmpetrov Nov 9, 2019
f9a507a
Remove duplicate tests when expected is empty
danielmpetrov Nov 9, 2019
ad7dd80
Improve diff
danielmpetrov Nov 9, 2019
1184159
Detangle new from existing tests
danielmpetrov Nov 9, 2019
c7fefce
Remove duplicate 'Exactly' tests
danielmpetrov Nov 9, 2019
daf4a32
Clean up reasons
danielmpetrov Nov 9, 2019
983b3d8
Merge branch 'master' of github.com:fluentassertions/fluentassertions…
danielmpetrov Nov 12, 2019
441e414
OrdinalIgnoreCase, improve error messages, private classes
danielmpetrov Nov 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Src/FluentAssertions/AtLeast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace FluentAssertions
{
public static class AtLeast
{
public static OccurrenceConstraint Once() => new AtLeastTimesConstraint(1);

public static OccurrenceConstraint Twice() => new AtLeastTimesConstraint(2);

public static OccurrenceConstraint Thrice() => new AtLeastTimesConstraint(3);

public static OccurrenceConstraint Times(int expected) => new AtLeastTimesConstraint(expected);

private sealed class AtLeastTimesConstraint : OccurrenceConstraint
{
internal AtLeastTimesConstraint(int expectedCount) : base(expectedCount) { }

internal override string Mode => "at least";

internal override bool Assert(int actual) => actual >= ExpectedCount;
}
}
}
22 changes: 22 additions & 0 deletions Src/FluentAssertions/AtMost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace FluentAssertions
{
public static class AtMost
{
public static OccurrenceConstraint Once() => new AtMostTimesConstraint(1);

public static OccurrenceConstraint Twice() => new AtMostTimesConstraint(2);

public static OccurrenceConstraint Thrice() => new AtMostTimesConstraint(3);

public static OccurrenceConstraint Times(int expected) => new AtMostTimesConstraint(expected);

private sealed class AtMostTimesConstraint : OccurrenceConstraint
{
internal AtMostTimesConstraint(int expectedCount) : base(expectedCount) { }

internal override string Mode => "at most";

internal override bool Assert(int actual) => actual <= ExpectedCount;
}
}
}
7 changes: 7 additions & 0 deletions Src/FluentAssertions/Common/IntegerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace FluentAssertions.Common
{
internal static class IntegerExtensions
{
public static string Times(this int count) => count == 1 ? "1 time" : $"{count} times";
}
}
23 changes: 23 additions & 0 deletions Src/FluentAssertions/Common/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,28 @@ public static string RemoveNewLines(this string @this)
{
return @this.Replace("\n", "").Replace("\r", "").Replace("\\r\\n", "");
}

/// <summary>
/// Counts the number of times a substring appears within a string by using the specified <see cref="StringComparison"/>.
/// </summary>
/// <param name="substring">The substring to search for.</param>
/// <param name="comparisonType">The <see cref="StringComparison"/> option to use for comparison.</param>
/// <returns></returns>
public static int CountSubstring(this string @this, string substring, StringComparison comparisonType)
{
string actual = @this ?? "";
string search = substring ?? "";

int count = 0;
int index = 0;

while ((index = actual.IndexOf(search, index, comparisonType)) >= 0)
{
index += search.Length;
count++;
}

return count;
}
}
}
22 changes: 22 additions & 0 deletions Src/FluentAssertions/Exactly.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace FluentAssertions
{
public static class Exactly
{
public static OccurrenceConstraint Once() => new ExactlyTimesConstraint(1);

public static OccurrenceConstraint Twice() => new ExactlyTimesConstraint(2);

public static OccurrenceConstraint Thrice() => new ExactlyTimesConstraint(3);

public static OccurrenceConstraint Times(int expected) => new ExactlyTimesConstraint(expected);

private sealed class ExactlyTimesConstraint : OccurrenceConstraint
{
internal ExactlyTimesConstraint(int expectedCount) : base(expectedCount) { }

internal override string Mode => "exactly";

internal override bool Assert(int actual) => actual == ExpectedCount;
}
}
}
20 changes: 20 additions & 0 deletions Src/FluentAssertions/LessThan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace FluentAssertions
{
public static class LessThan
{
public static OccurrenceConstraint Twice() => new LessThanTimesConstraint(2);

public static OccurrenceConstraint Thrice() => new LessThanTimesConstraint(3);

public static OccurrenceConstraint Times(int expected) => new LessThanTimesConstraint(expected);

private sealed class LessThanTimesConstraint : OccurrenceConstraint
{
internal LessThanTimesConstraint(int expectedCount) : base(expectedCount) { }

internal override string Mode => "less than";

internal override bool Assert(int actual) => actual < ExpectedCount;
}
}
}
22 changes: 22 additions & 0 deletions Src/FluentAssertions/MoreThan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace FluentAssertions
{
public static class MoreThan
{
public static OccurrenceConstraint Once() => new MoreThanTimesConstraint(1);

public static OccurrenceConstraint Twice() => new MoreThanTimesConstraint(2);

public static OccurrenceConstraint Thrice() => new MoreThanTimesConstraint(3);

public static OccurrenceConstraint Times(int expected) => new MoreThanTimesConstraint(expected);

private sealed class MoreThanTimesConstraint : OccurrenceConstraint
{
internal MoreThanTimesConstraint(int expectedCount) : base(expectedCount) { }

internal override string Mode => "more than";

internal override bool Assert(int actual) => actual > ExpectedCount;
}
}
}
23 changes: 23 additions & 0 deletions Src/FluentAssertions/OccurrenceConstraint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;

namespace FluentAssertions
{
public abstract class OccurrenceConstraint
{
protected OccurrenceConstraint(int expectedCount)
{
if (expectedCount < 0)
{
throw new ArgumentOutOfRangeException(nameof(expectedCount), "Expected count cannot be negative.");
}

ExpectedCount = expectedCount;
}

internal int ExpectedCount { get; private set; }

internal abstract string Mode { get; }

internal abstract bool Assert(int actual);
}
}
83 changes: 82 additions & 1 deletion Src/FluentAssertions/Primitives/StringAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,46 @@ public AndConstraint<StringAssertions> Contain(string expected, string because =
return new AndConstraint<StringAssertions>(this);
}

/// <summary>
/// Asserts that a string contains another (fragment of a) string a set amount of times.
/// </summary>
/// <param name="expected">
/// The (fragment of a) string that the current string should contain.
/// </param>
/// <param name="occurrenceConstraint">
/// A constraint specifying the amount of times a substring should be present within the test subject.
/// It can be created by invoking static methods Once, Twice, Thrice, or Times(int)
/// on the classes <see cref="Exactly"/>, <see cref="AtLeast"/>, <see cref="MoreThan"/>, <see cref="AtMost"/>, and <see cref="LessThan"/>.
/// For example, <see cref="Exactly.Times(int)"/> or <see cref="LessThan.Twice()"/>.
/// </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 <see cref="because" />.
/// </param>
public AndConstraint<StringAssertions> Contain(string expected, OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot assert string containment against <null>.");

if (expected.Length == 0)
{
throw new ArgumentException("Cannot assert string containment against an empty string.", nameof(expected));
}

int actual = Subject.CountSubstring(expected, StringComparison.Ordinal);

Execute.Assertion
.ForCondition(occurrenceConstraint.Assert(actual))
.BecauseOf(because, becauseArgs)
.FailWith(
$"Expected {{context:string}} {{0}} to contain {{1}} {occurrenceConstraint.Mode} {occurrenceConstraint.ExpectedCount.Times()}{{reason}}, but found it {actual.Times()}.",
Subject, expected);

return new AndConstraint<StringAssertions>(this);
}

/// <summary>
/// Asserts that a string contains the specified <paramref name="expected"/>,
/// including any leading or trailing whitespace, with the exception of the casing.
Expand All @@ -660,7 +700,48 @@ public AndConstraint<StringAssertions> ContainEquivalentOf(string expected, stri
Execute.Assertion
.ForCondition(Contains(Subject, expected, StringComparison.CurrentCultureIgnoreCase))
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:string} to contain equivalent of {0}{reason} but found {1}.", expected, Subject);
.FailWith("Expected {context:string} {0} to contain the equivalent of {1}{reason}.", Subject, expected);
jnyrup marked this conversation as resolved.
Show resolved Hide resolved

return new AndConstraint<StringAssertions>(this);
}

/// <summary>
/// Asserts that a string contains the specified <paramref name="expected"/> a set amount of times,
/// including any leading or trailing whitespace, with the exception of the casing.
/// </summary>
/// <param name="expected">
/// The (fragment of a) string that the current string should contain.
/// </param>
/// <param name="occurrenceConstraint">
/// A constraint specifying the amount of times a substring should be present within the test subject.
/// It can be created by invoking static methods Once, Twice, Thrice, or Times(int)
/// on the classes <see cref="Exactly"/>, <see cref="AtLeast"/>, <see cref="MoreThan"/>, <see cref="AtMost"/>, and <see cref="LessThan"/>.
/// For example, <see cref="Exactly.Times(int)"/> or <see cref="LessThan.Twice()"/>.
/// </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 <see cref="because" />.
/// </param>
public AndConstraint<StringAssertions> ContainEquivalentOf(string expected, OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot assert string containment against <null>.");

if (expected.Length == 0)
{
throw new ArgumentException("Cannot assert string containment against an empty string.", nameof(expected));
}

int actual = Subject.CountSubstring(expected, StringComparison.OrdinalIgnoreCase);

Execute.Assertion
.ForCondition(occurrenceConstraint.Assert(actual))
.BecauseOf(because, becauseArgs)
.FailWith(
$"Expected {{context:string}} {{0}} to contain equivalent of {{1}} {occurrenceConstraint.Mode} {occurrenceConstraint.ExpectedCount.Times()}{{reason}}, but found it {actual.Times()}.",
Subject, expected);

return new AndConstraint<StringAssertions>(this);
}
Expand Down
Loading