question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Open up `OccurenceConstraint` for usage in custom assertion extensions

See original GitHub issue

The OccurrenceConstraint.Assert() method is internal - so people who write their own extensions cannot use this very handy constraint (and therefore create their own - creating inconsistency of how to deal with assertion occurences).

Understand why this is internal - so the actual usage hides this when defining an assertion for the test. However why not add a new overload in the Execute assertion fluent API to accept a OccurrenceConstraint. i.e. as an alternative to ForCondition(...):

Execute.Assertion
    .BecauseOf(because, becauseArgs)
    .ForConstraint(constraint)
    .FailWith("Expected {0}{reason} but found {1}",
              constraint, Subject.Count());

Note there would be a formatter for the occurrence constraint to render the expected amount / “mode” (since these are also internal - and should continue to be)

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:14 (9 by maintainers)

github_iconTop GitHub Comments

3reactions
rynkevichcommented, Jun 5, 2020

After some research I can offer a few solutions for this feature. I’ll list and describe them below. @brooknovak @dennisdoomen @jnyrup It would be great if you could look through and give some comments on this.


A) Literally what @brooknovak asked

Overview: ForConstraint method is added specifically for OccurrenceConstraint. A new formatter for OccurrenceConstraint is also registered.

Pros:

  • The simplest solution.

Cons:

  • Constraint, probably, is not something that should have a global formatter. It just needs a particular formatter in this particular case.
  • With this API user must specify some information twice (expected and actual values in both ForConstraint and FailWith).
  • It’s not the best solution from the extension interface perspective: a) If more “assertable” constraints will appear, either ForConstraint will have to be rewritten or more overloads added. b) The need to add a new formatter for new “assertable” constraint is not explicit.

Possible implementation

// AssertionScope.cs

public AssertionScope ForConstraint(OccurrenceConstraint constraint, int actual)
{
    succeeded = constraint.Assert(actual);

    return this;
}

...

// OccurrenceConstraintFormatter.cs

public class OccurrenceConstraintFormatter : IValueFormatter
{
    public bool CanHandle(object value) => value is OccurrenceConstraint;

    public string Format(object value, FormattingContext context, FormatChild formatChild)
    {
        var occurrenceConstraint = (OccurrenceConstraint)value;

        return $"{occurrenceConstraint.Mode} {occurrenceConstraint.ExpectedCount.Times()}";
    }
}

...

// Formatter.cs

private static readonly List<IValueFormatter> DefaultFormatters = new List<IValueFormatter>
{
    ...
    new OccurrenceConstraintFormatter()
}

Usage

Execute.Assertion
    .BecauseOf(because, becauseArgs)
    .ForConstraint(constraint, Subject.Count())
    .FailWith("Expected {0}{reason} but found {1}",
        constraint, Subject.Count());

B) Generalized solution

Overview: ForConstraint is added for AssertableConstraint, which is an abstract base class for constraints with Assert method. Probably, interface would be more suitable here, but it won’t allow keeping it internal. A new formatter for AssertableConstraint is registered, which invokes formatting logic, defined inside the constraint implementation.

Pros:

  • Agile, scalable solution.
  • The need for formatting logic implementation is obvious.

Cons:

  • The most complicated.
  • A little bit more reflection than it should be.
  • Global formatter for constraints is still required.
  • Duplicate specification of expected and actual values is still required.
  • Makes less sense if more “assertable” constraints will not ever appear.

Possible implementation

// AssertableConstraint.cs

public abstract class AssertableConstraint<T>
{
    internal abstract bool Assert(T actual);

    internal abstract string Format(); // or a better name
}

...

// OccurrenceConstraint.cs

public abstract class OccurrenceConstraint : AssertableConstraint<int>
{
    ...

    internal override string Format() => $"{Mode} {ExpectedCount.Times()}";
}

...

// AssertionScope.cs

public AssertionScope ForConstraint<T>(AssertableConstraint<T> constraint, T actual)
{
    succeeded = constraint.Assert(actual);

    return this;
}

...

// AssertableConstraintFormatter.cs

public class AssertableConstraintFormatter : IValueFormatter
{
    public bool CanHandle(object value) => IsInstanceOfGenericType(typeof(AssertableConstraint<>), value);

    public string Format(object value, FormattingContext context, FormatChild formatChild)
    {
        var constraint = (OccurrenceConstraint)value;

        return constraint.Format();
    }

    private static bool IsInstanceOfGenericType(Type genericType, object instance)
    {
        Type type = instance.GetType();

        while (type != null)
        {
            if (type.IsGenericType && type.GetGenericTypeDefinition() == genericType)
            {
                return true;
            }

            type = type.BaseType;
        }

        return false;
    }
}

...

// Formatter.cs

private static readonly List<IValueFormatter> DefaultFormatters = new List<IValueFormatter>
{
    ...
    new AssertableConstraintFormatter()
}

Usage

Execute.Assertion
    .BecauseOf(because, becauseArgs)
    .ForConstraint(constraint, Subject.Count())
    .FailWith("Expected {0}{reason} but found {1}",
        constraint, Subject.Count());

C) Generalized solution with placeholders for formatting

Overview: The same as B), but API is changed to use special placeholders FailWith message argument so there is no need for formatters.

Pros:

  • Scalable, the most agile solution.
  • The need for formatting logic implementation is obvious.
  • API does not have redundancies.
  • No global formatters for constraints.

Cons:

  • Not as simple as A).
  • Existence of special placeholders is not obvious for a library user.

Possible implementation

// AssertableConstraint.cs

public abstract class AssertableConstraint<T>
{
    internal abstract bool Assert(T actual);

    internal abstract void RegisterReportables(AssertionScope scope);
}

...

// OccurrenceConstraint.cs

public abstract class OccurrenceConstraint : AssertableConstraint<int>
{
    ...

    internal override void RegisterReportables(AssertionScope scope)
    {
        scope.AddReportable("occurrenceMode", Mode);
        scope.AddReportable("expectedOccurrenceCount", ExpectedCount.Times());
    }
}

...

// AssertionScope.cs

public AssertionScope ForConstraint<T>(AssertableConstraint<T> constraint, T actual)
{
    succeeded = constraint.Assert(actual);
    constraint.RegisterReportables(this);

    return this;
}

Usage

Execute.Assertion
    .BecauseOf(because, becauseArgs)
    .ForConstraint(constraint, Subject.Count())
    .FailWith("Expected {occurrenceMode} {expectedOccurrenceCount} {reason} but found {1}");

2reactions
brooknovakcommented, May 31, 2020

Go for it @dennisdoomen - use whatever you like. I love this library, it’s really helped shape the way I approach testing!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Extensibility - Fluent Assertions
A very extensive set of extension methods that allow you to more naturally specify the expected outcome of a TDD or BDD-style unit...
Read more >
Creating Custom Fluent Assertions | NimblePros Blog
The first step to creating a custom assertion is to define a class that inherits from ReferenceTypeAssertions<TSubject, TAssertions> abstract ...
Read more >
How do I write CustomAssertion using FluentAssertions?
I managed to write my own extension method via syntax BeWorking(this ObjectAssertions obj, string because="", params object[] becauseArgs) in ...
Read more >
Custom Assertions with AssertJ
In this tutorial, we'll walk through creating custom AssertJ assertions; the AssertJ's basics can be found here.
Read more >
The definitive guide to extending Fluent Assertions
So in this case, our nicely created ContainFile extension method will display the directory that it used to assert that file existed. You...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found