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 16 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions Src/FluentAssertions/Common/StringExtensions.cs
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;
}
}
}
70 changes: 68 additions & 2 deletions Src/FluentAssertions/Primitives/StringAssertions.cs
Expand Up @@ -631,7 +631,40 @@ public AndConstraint<StringAssertions> Contain(string expected, string because =
Execute.Assertion
.ForCondition(Contains(Subject, expected, StringComparison.Ordinal))
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:string} {0} to contain {1}{reason}.", Subject, expected);
.FailWith("Expected {context:string} {0} to contain {1}{reason}, but not found.", Subject, expected);

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="timesConstraint">
/// 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, TimesConstraint timesConstraint, 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));
}

timesConstraint.AssertContain(Subject, expected, because, becauseArgs);

return new AndConstraint<StringAssertions>(this);
}
Expand Down Expand Up @@ -660,7 +693,40 @@ 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 equivalent of {1}{reason}, but not found.", Subject, expected);
danielmpetrov marked this conversation as resolved.
Show resolved Hide resolved

return new AndConstraint<StringAssertions>(this);
}

/// <summary>
/// Asserts that a string contains another (fragment of a) string a set amount of times.
danielmpetrov marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
/// <param name="expected">
/// The (fragment of a) string that the current string should contain.
/// </param>
/// <param name="timesConstraint">
/// 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, TimesConstraint timesConstraint, 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));
}

timesConstraint.AssertContainEquivalentOf(Subject, expected, because, becauseArgs);

return new AndConstraint<StringAssertions>(this);
}
Expand Down
169 changes: 169 additions & 0 deletions Src/FluentAssertions/Primitives/TimesConstraint.cs
@@ -0,0 +1,169 @@
using System;
using FluentAssertions.Common;
using FluentAssertions.Execution;

namespace FluentAssertions.Primitives
danielmpetrov marked this conversation as resolved.
Show resolved Hide resolved
{
public abstract class TimesConstraint
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
{
private int? actualCount;

protected readonly int expectedCount;

public TimesConstraint(int expectedCount)
danielmpetrov marked this conversation as resolved.
Show resolved Hide resolved
{
this.expectedCount = expectedCount;
}

protected int ActualCount
{
get
{
if (!actualCount.HasValue)
{
actualCount = Subject.CountSubstring(Expected, StringComparison);
}

return actualCount.Value;
}
}

protected abstract string Mode { get; }

protected abstract bool IsMatch { get; }

private string Expected { get; set; }

private string Subject { get; set; }

private StringComparison StringComparison { get; set; }

private static string Times(int count) => count == 1 ? "1 time" : $"{count} times";
danielmpetrov marked this conversation as resolved.
Show resolved Hide resolved

internal void AssertContain(string subject, string expected, string because, params object[] becauseArgs)
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
{
Subject = subject;
Expected = expected;
StringComparison = StringComparison.Ordinal;

Execute.Assertion
.ForCondition(IsMatch)
.BecauseOf(because, becauseArgs)
.FailWith(
$"Expected {{context:string}} {{0}} to contain {{1}} {Mode} {Times(expectedCount)}{{reason}}, but found {Times(ActualCount)}.",
Subject, expected);
}

internal void AssertContainEquivalentOf(string subject, string expected, string because, params object[] becauseArgs)
{
Subject = subject;
Expected = expected;
StringComparison = StringComparison.CurrentCultureIgnoreCase;

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

internal sealed class AtLeastTimesConstraint : TimesConstraint
danielmpetrov marked this conversation as resolved.
Show resolved Hide resolved
{
internal AtLeastTimesConstraint(int expectedCount) : base(expectedCount) { }

protected override string Mode => "at least";

protected override bool IsMatch => ActualCount >= expectedCount;
}

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

protected override string Mode => "at most";

protected override bool IsMatch => ActualCount <= expectedCount;
}

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

protected override string Mode => "more than";

protected override bool IsMatch => ActualCount > expectedCount;
}

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

protected override string Mode => "less than";

protected override bool IsMatch => ActualCount < expectedCount;
}

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

protected override string Mode => "exactly";

protected override bool IsMatch => ActualCount == expectedCount;
}

public static class AtLeast
{
public static TimesConstraint Once() => new AtLeastTimesConstraint(1);

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

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

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

public static class AtMost
{
public static TimesConstraint Once() => new AtMostTimesConstraint(1);

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

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

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

public static class MoreThan
{
public static TimesConstraint Once() => new MoreThanTimesConstraint(1);

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

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

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

public static class LessThan
{
public static TimesConstraint Twice() => new LessThanTimesConstraint(2);

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

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

public static class Exactly
{
public static TimesConstraint Once() => new ExactlyTimesConstraint(1);

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

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

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