Skip to content

[API Proposal]: Advanced string comparison #2720

Open

Description

@POnakS

Background and motivation

I love most of string methods implemented in FluentAssertions, most of these are pretty obvious:

theString.Should().NotBeNull();
theString.Should().BeNull();
theString.Should().BeEmpty();
theString.Should().NotBeEmpty("because the string is not empty");
theString.Should().HaveLength(0);
theString.Should().BeNullOrWhiteSpace(); // either null, empty or whitespace only
theString.Should().NotBeNullOrWhiteSpace();
theString.Should().Be("This is a String");
theString.Should().NotBe("This is another String");

However what happens when you want to check if string is case invariant?

theString.Should().BeEquivalentTo("THIS IS A STRING");

At first glance, this method does anything. Does it trim whitespace? Maybe. Does it take casing into account? Maybe. Does it behave like object.Should().BeEquvalentTo()? Actually not.
This forces reader to reach for the documentation, and while this is quite easy in IDE, it can be improved.
This story could be greatly improved by making api similar to builtin comparison apis, which are provided by framework, i.e. string.Compare, string.Contains or string.StartsWith and similar methods.
Microsoft suggests specifying intent when comparing strings, and this perfectly applies to tests as well.

API Proposal

public class StringAssertions<TAssertions>
{
    public AndConstraint<TAssertions> Be(string expected, StringComparison comparisonType, string because = "", params object[] becauseArgs);
    public AndConstraint<TAssertions> Be(string expected, CultureInfo? cultureInfo, CompareOptions compareOptions,  string because = "", params object[] becauseArgs); // This could be potentially usefull to handle cultures like Turkish, however I do not know if this is really useful api
    public AndConstraint<TAssertions> BeOrdinal(string expected, string because = "", params object[] becauseArgs); // Or BeIgnoreCase
}

API Usage

var theString = "abc";

theString.Should().BeOrdinal("ABC");
theString.Should().Be("ABC", StringComparison.IgnoreCase);
theString.Should().Be("abc", CultureInfo.CurrentCulture);

Alternative Designs

No response

Risks

There will be multiple ways of asserting string equality:

"abc".Should().BeEquivalentTo("ABC");
"abc".Should().Be("ABC", StringComparison.IgnoreCase);
"abc".Should().BeOrdinal("ABC");

This is not huge problem. It may be a bit confusing in case multiple methods are used in single codebase.
Possible mitigations:

  1. Do nothing, it's not huge problem
  2. Mark StringAssertions.BeEquivalentTo as Obsolete (however this is huge change for existing costumers)
  3. Implement some kind of code analyzer which checks for presence of both methods in single codebase and proposes single

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?

Yes, please assign this issue to me.

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Aug 9, 2024
  2. dennisdoomen commented on Aug 10, 2024

    @dennisdoomen
    Member

    I'm not against it, but we should make this a complete example of all the methods and overloads you would like to add, before we approve the proposal.

    Notice that in #2413 we introduced extra options to BeEquivalentTo and extra overloads to some of the other string assertions that take an EquivalencyOptions parameter.

  3. Xceed-DelvaJB commented on May 19, 2026

    @Xceed-DelvaJB
    Contributor

    Function list

    In the StringAssertions class, we could add

    public class StringAssertions<TAssertions>
    {
      public AndConstraint<TAssertions> Be(string expected, StringComparison comparisonType, string because = "", params object[] becauseArgs);
    
      public AndConstraint<TAssertions> Be(string expected, CompareOptions? compareOptions, CultureInfo? cultureInfo, string because = "", params object[] becauseArgs);
    
      public AndConstraint<TAssertions> BeOrdinal(string expected, string because = "", params object[] becauseArgs);
    
      public AndConstraint<TAssertions> NotBe(string expected, StringComparison comparisonType, string because = "", params object[] becauseArgs);
    
      public AndConstraint<TAssertions> NotBe(string expected, CompareOptions? compareOptions, CultureInfo? cultureInfo, string because = "", params object[] becauseArgs);
    
      public AndConstraint<TAssertions> NotBeOrdinal(string expected, string because = "", params object[] becauseArgs);
    }

    Substring comparison

    The .NET framework offers substring comparison with

    public static int Compare(string strA, int indexA, string strB, int indexB, int length, StringComparison comparisonType);

    Users can already assert substrings by manually calling String.SubString() or ocnverting to a ReadOnlySpan (but not available in .NET 4.7).
    We could add assertions to StringAssertions to offer convenience or for when the actual and/or expected strings are very large.

    public class StringAssertions<TAssertions>
    {
      public AndConstraint<TAssertions> BeSubString( 
         int index,
         string expected,
         int expectedIndex,
         int length,
         string because = "",
         params object[] becauseArgs)
         
      public AndConstraint<TAssertions> BeSubString( 
         int index,
         string expected,
         int expectedIndex,
         int length,
         StringComparison comparisonType,
         string because = "",
         params object[] becauseArgs)
         
      public AndConstraint<TAssertions> BeSubStringOrdinal( 
         int index,
         string expected,
         int expectedIndex,
         int length,
         string because = "",
         params object[] becauseArgs)
         
      public AndConstraint<TAssertions> NotBeSubString( 
         int index,
         string expected,
         int expectedIndex,
         int length,
         string because = "",
         params object[] becauseArgs)
         
      public AndConstraint<TAssertions> NotBeSubString( 
         int index,
         string expected,
         int expectedIndex,
         int length,
         StringComparison comparisonType,
         string because = "",
         params object[] becauseArgs)
         
      public AndConstraint<TAssertions> NotBeSubStringOrdinal( 
         int index,
         string expected,
         int expectedIndex,
         int length,
         string because = "",
         params object[] becauseArgs)
    }	   

    The implementations would call BeEquivalentTo() with an appropriately built EquivalencyOptions<string> object.
    It needs APIs to store the new parameters.

    public abstract class SelfReferenceEquivalencyOptions<TSelf>
    or
    public interface IEquivalencyOptions
    {
      public bool CompareByOrdinal { get; }
      public TSelf ComparingByOrdinal()
      or
      public bool CompareStringsByOrdinal { get; }
      public TSelf ComparingStringsByOrdinal()
    
      public CompareOptions StringCompareOptions { get; } // This name feels off
      public TSelf Using(CompareOptions compareOptions)
    
      public CultureInfo CultureInfo { get; }
      public TSelf Using(CultureInfo cultureInfo)
    
      public int ActualIndex { get; }
      public int ExpectedIndex { get; }
      public TSelf ComparingByIndex(int actualIndex, int expectedIndex)
      or
      public TSelf WithIndex(int actualIndex, int expectedIndex)
    }

    Summary and questions

    I've listed a possible list of methods to add to StringAssertions that cover string comparison options available in the .NET Framework.
    They would be shortcuts to calling BeEquivalentTo() with existing and new options.

    Do we want to cover all these options? I would say why not? It would feel more natural to use these methods than to build
    EquivalencyOptions<string> objects.

    I see that IEquivalencyOptions/SelfReferenceEquivalencyOptions represents data that is not very closely related.
    But is adding the string comparison options, especially the indexes going too far?

    And if it's a viable design, I'm not sure what does in IEquivalencyOptions vs SelfReferenceEquivalencyOptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions