String
Describes the possible expectations for strings.
| Expectation | Negated | Summary |
|---|---|---|
IsEqualTo | IsNotEqualTo | equal to the expected string, or matching a pattern |
IsOneOf | IsNotOneOf | equal to one of the expected strings |
IsNull | IsNotNull | null |
IsEmpty | IsNotEmpty | the empty string |
IsNullOrEmpty | IsNotNullOrEmpty | null or the empty string |
IsNullOrWhiteSpace | IsNotNullOrWhiteSpace | null, empty or only whitespace |
HasLength | negated comparison | has the expected number of characters |
HasLineCount | negated comparison | has the expected number of lines |
HasLines | its lines meet collection expectations | |
StartsWith | DoesNotStartWith | starts with the expected string |
EndsWith | DoesNotEndWith | ends with the expected string |
Contains | DoesNotContain | contains the expected substring, optionally a number of times |
IsUpperCased | IsNotUpperCased | every letter with an upper-case form is upper-case |
IsLowerCased | IsNotLowerCased | every letter with a lower-case form is lower-case |
IsParsableInto<T> | IsNotParsableInto<T> | can be parsed into T |
Equality
You can verify that the string is equal to another one or not:
string title = "Abbey Road";
await Expect.That(title).IsEqualTo("Abbey Road");
await Expect.That(title).IsNotEqualTo("Let It Be");
The comparison is ordinal and can be configured with the string options or compare against a pattern.
String options
The expectations that compare the string with an expected string (IsEqualTo, IsOneOf, StartsWith, EndsWith,
Contains and their negations) take the following options:
| Option | Effect |
|---|---|
IgnoringCase() | compares with StringComparison.OrdinalIgnoreCase |
IgnoringNewlineStyle() | treats \r\n, \n and \r as equal |
IgnoringIndentation() | removes the leading whitespace from every line (see below) |
IgnoringLeadingWhiteSpace() | ignores whitespace at the start of the strings |
IgnoringTrailingWhiteSpace() | ignores whitespace at the end of the strings |
Using(comparer) | compares with a custom IEqualityComparer<string> |
string title = "Abbey Road";
await Expect.That(title).IsEqualTo("ABBEY ROAD").IgnoringCase();
await Expect.That("Abbey\r\nRoad").IsEqualTo("Abbey\nRoad").IgnoringNewlineStyle();
await Expect.That(" Abbey\n Road").IsEqualTo("Abbey\nRoad").IgnoringIndentation();
await Expect.That(title).IsEqualTo(" Abbey Road").IgnoringLeadingWhiteSpace();
await Expect.That(title).IsEqualTo("Abbey Road \t").IgnoringTrailingWhiteSpace();
await Expect.That(title).StartsWith("ABBEY").Using(StringComparer.OrdinalIgnoreCase);
The same options apply wherever strings are compared, e.g. to the items of a collection of strings or to the message of an exception.
IgnoringCase() and Using(…) can't be combined, because only one of them could decide how the casing is compared:
the second one throws an InvalidOperationException, whichever order they are specified in. Use a case-insensitive
comparer such as StringComparer.OrdinalIgnoreCase instead.
The negations take the same options, so an option can make them fail:
string title = "Abbey Road";
await Expect.That(title).DoesNotEndWith("ROAD")
.Because("the casing differs, which would not count with `IgnoringCase()`");
Indentation
While IgnoringLeadingWhiteSpace only trims the start of the complete string, IgnoringIndentation removes the
leading whitespace from every line. This allows comparing against a raw string literal that is indented differently
than the subject:
string code = """
public class Beatles
{
public string Album => "Abbey Road";
}
""";
await Expect.That(code).Contains("""
public string Album => "Abbey Road";
""").IgnoringIndentation();
As the lines are split on \r\n, \n and \r, this also normalizes the newline style, which makes
IgnoringNewlineStyle redundant. Trailing whitespace within a line is kept, but a line that consists only of
whitespace becomes empty. To keep the relative indentation within the snippet, use AsBlock instead.
Match types
Instead of comparing for equality, IsEqualTo can match the subject against a pattern, a prefix or a suffix. The
same match types are available for IsNotEqualTo, IsOneOf, IsNotOneOf, Contains and DoesNotContain.
null subject has no content to matchEvery match type except the plain comparison asks about the content of the subject, so it fails for a null subject in
both directions, exactly like StartsWith and DoesNotStartWith do.
Wildcards
string title = "Let It Be";
await Expect.That(title).IsEqualTo("Let*B?").AsWildcard();
| Wildcard specifier | Matches |
|---|---|
| * (asterisk) | Zero or more characters |
| ? (question mark) | Exactly one character |
The pattern has to cover the complete subject, including all its lines and a trailing newline. An empty pattern
therefore matches only an empty subject. A null pattern is rejected with an ArgumentNullException, because it
matches no subject at all.
Regular expressions
string title = "Let It Be";
await Expect.That(title).IsEqualTo("(.*)Be").AsRegex();
The pattern is matched like Regex.IsMatch(subject, pattern), so ^ and $ bind to the start and the end of the
complete subject and not to every line. IgnoreCase and CultureInvariant are added when the IgnoringCase method is
also used, so that the casing is ignored the same way as for every other expectation and never depends on the current
culture. Every other
option
is opt-in:
using System.Text.RegularExpressions;
string lyrics = "Come together\nRight now";
await Expect.That(lyrics).IsEqualTo("^Right now$").AsRegex(RegexOptions.Multiline);
An empty pattern is rejected with an ArgumentException and a null pattern with an ArgumentNullException, because
an empty pattern matches every subject and a null pattern matches no subject, so one of the two expectations could
never fail. A pattern that is not a valid regex is rejected with an ArgumentException that carries the parse error as
its inner exception, even for a null subject.
A wildcard or regex pattern is matched by the regex engine, which can't use a custom comparer, so Using(…) after
AsWildcard() or AsRegex() throws an InvalidOperationException. Matching a pattern is limited to one second. A
pattern that takes longer, e.g. because of catastrophic backtracking, throws an ArgumentException that asks you to
simplify the pattern.
Prefix / Suffix
string title = "Abbey Road";
await Expect.That(title).IsEqualTo("Abbey").AsPrefix();
await Expect.That(title).IsEqualTo("Road").AsSuffix();
An empty prefix or suffix is rejected with an ArgumentException, because every subject starts and ends with the empty
string, so such an expectation says nothing about the subject. A null prefix or suffix is rejected with an
ArgumentNullException, just like a null pattern.
One of
You can verify that the string is one of many alternatives, with the same options as for
equality:
string title = "Abbey Road";
await Expect.That(title).IsOneOf("Help!", "Abbey Road", "Revolver");
await Expect.That(title).IsOneOf("HELP!", "ABBEY ROAD", "REVOLVER").IgnoringCase();
await Expect.That(title).IsNotOneOf("Help!", "Revolver");
Null, empty or whitespace
You can verify that the string is null, empty or contains only whitespace:
string? title = null;
await Expect.That(title).IsNull();
await Expect.That("Abbey Road").IsNotNull();
await Expect.That("").IsEmpty();
await Expect.That("Abbey Road").IsNotEmpty();
await Expect.That(title).IsNullOrEmpty();
await Expect.That("Abbey Road").IsNotNullOrEmpty();
await Expect.That(title).IsNullOrWhiteSpace();
await Expect.That("Abbey Road").IsNotNullOrWhiteSpace();
Length
You can verify that the string has the expected length:
string title = "Abbey Road";
await Expect.That(title).HasLength(10);
await Expect.That(title).HasLength().Between(8).And(12);
await Expect.That(title).HasLength().NotGreaterThan(12);
The Has… expectations for a number or a TimeSpan, e.g. HasLength(), HasCount(), HasYear() or HasMajor(),
continue with one of the following comparisons, each with a negated counterpart:
| Comparison | Negated | Succeeds when the value is |
|---|---|---|
EqualTo(x) | NotEqualTo(x) | equal to x |
GreaterThan(x) | NotGreaterThan(x) | greater than x |
GreaterThanOrEqualTo(x) | NotGreaterThanOrEqualTo(x) | greater than or equal to x |
LessThan(x) | NotLessThan(x) | less than x |
LessThanOrEqualTo(x) | NotLessThanOrEqualTo(x) | less than or equal to x |
Between(min).And(max) | NotBetween(min).And(max) | between min and max, both bounds included |
Passing the value directly, e.g. HasLength(10), is a shorthand for EqualTo.
The expected value is nullable, e.g. to pass a value mapped from a property. As the actual value is never null,
EqualTo(null) fails and NotEqualTo(null) succeeds, while a comparison of order against null, such as
GreaterThan(null) or Between(null).And(3), fails even when negated.
Lines
You can verify how many lines the string has:
string lyrics = """
Come together
Right now
Over me
""";
await Expect.That(lyrics).HasLineCount(3);
await Expect.That(lyrics).HasLineCount().NotEqualTo(4);
await Expect.That(lyrics).HasLineCount().GreaterThan(2);
HasLineCount() takes the same comparisons as HasLength().
You can also verify the lines themselves. HasLines applies the expectations on the lines as a collection, so all
collection expectations are available:
await Expect.That(lyrics).HasLines(lines => lines.Contains("Right now"));
await Expect.That(lyrics).HasLines(lines => lines.StartsWith("Come together"));
await Expect.That(lyrics).HasLines(lines => lines.All().Satisfy(line => line?.Length < 20));
Lines are separated by \r\n, \n or \r, which are all treated equivalently. A single trailing line terminator
does not start a new line, so "Come together\n" has one line and "" has none, matching how File.ReadLines counts
the lines of a file:
await Expect.That("").HasLineCount(0);
await Expect.That("Come together").HasLineCount(1);
await Expect.That("Come together\n").HasLineCount(1);
await Expect.That("Come together\n\n").HasLineCount(2);
Start / end
You can verify that the string starts or ends with a given string, or that it does not, with the same
options as for equality:
string title = "Abbey Road";
await Expect.That(title).StartsWith("Abbey");
await Expect.That(title).EndsWith("ROAD").IgnoringCase();
await Expect.That(title).DoesNotStartWith("Road");
await Expect.That(title).DoesNotEndWith("Abbey");
Contains
You can verify that the string contains a given substring, or that it does not, with the same
options as for equality:
string title = "Strawberry Fields Forever";
await Expect.That(title).Contains("Fields");
await Expect.That(title).Contains("FIELDS").IgnoringCase();
await Expect.That(title).DoesNotContain("Penny Lane");
You can also specify how often the substring should be found:
string lyrics = "get back, get back, get back to where you once belonged.";
// 'get' can be found 3 times
await Expect.That(lyrics).Contains("get").MoreThan(1)
.Because("count should be '> 1'");
await Expect.That(lyrics).Contains("get").AtLeast(2)
.Because("count should be '>= 2'");
await Expect.That(lyrics).Contains("get").Exactly(3)
.Because("count should be '== 3'");
await Expect.That(lyrics).Contains("get").AtMost(4)
.Because("count should be '<= 4'");
await Expect.That(lyrics).Contains("get").LessThan(5)
.Because("count should be '< 5'");
await Expect.That(lyrics).Contains("get").Between(1).And(6)
.Because("count should be '>= 1 AND <= 6'");
Blocks
While IgnoringIndentation removes the leading whitespace from every line, AsBlock keeps the relative
indentation within the expected block and only allows the block as a whole to be indented in the subject.
This is stricter, as a line that is indented differently from the rest of the block does not match:
string code = """
public class Beatles
{
public string Album
{
get;
}
}
""";
await Expect.That(code).Contains("""
public string Album
{
get;
}
""").AsBlock();
The block must start and end at line boundaries, and all its lines must share the same whitespace prefix in the
subject. A line that consists only of whitespace matches any line that consists only of whitespace. The newline style
is always ignored, and a single trailing line terminator does not start a new line, so "a\nb\n" has the same two
lines as "a\nb" (as for lines). AsBlock can be combined with IgnoringCase, Using and the count
quantifiers.
Character casing
You can verify that the characters in a string are all upper or lower cased:
await Expect.That("1ST PLACE").IsUpperCased()
.Because("it contains no lowercase characters");
await Expect.That("1st PLACE").IsNotUpperCased()
.Because("it contains at least one lowercase character");
await Expect.That("1st place").IsLowerCased()
.Because("it contains no uppercase characters");
await Expect.That("1st PLACE").IsNotLowerCased()
.Because("it contains at least one uppercase character");
Letters without an upper-case (lower-case) form, like ß, count as upper-cased (lower-cased). Use
IncludingUncasedLetters() to also reject them and titlecase letters:
await Expect.That("STRAßE").IsNotUpperCased().IncludingUncasedLetters()
.Because("ß is a lowercase letter without an uppercase form");
Parsing
The parsing expectations are only available on .NET 8 or later.
You can verify that the string can be parsed into a type that implements IParsable<T>, and continue with
expectations on the parsed value with Which:
using System.Globalization;
await Expect.That("42").IsParsableInto<int>();
await Expect.That("42").IsParsableInto<int>().Which.IsGreaterThan(40);
await Expect.That("1,5").IsParsableInto<double>(new CultureInfo("de-DE"))
.Because("the format provider is passed to `Parse`");
await Expect.That("Abbey Road").IsNotParsableInto<int>();
A failure shows the exception thrown by Parse and keeps it as inner exception. A null subject fails both
expectations.
The same expectations are available for a ReadOnlySpan<char> of a type that implements ISpanParsable<T> and for a
UTF-8 ReadOnlySpan<byte> of a type that implements IUtf8SpanParsable<T>:
await Expect.That("42".AsSpan()).IsParsableInto<int>();
await Expect.That("42"u8).IsParsableInto<int>();