Skip to content

Commit

Permalink
Add support for Satisfy on reference type assertions using element in…
Browse files Browse the repository at this point in the history
…spectors
  • Loading branch information
siewers committed Feb 29, 2024
1 parent 8ffa298 commit c330735
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 73 deletions.
39 changes: 39 additions & 0 deletions Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using FluentAssertions.Common;
using FluentAssertions.Execution;
Expand Down Expand Up @@ -416,6 +417,44 @@ public AndConstraint<TAssertions> NotBeAssignableTo(Type type, string because =
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that the criteria provided by the element inspector is satisfied.
/// </summary>
/// <param name="expected">The element inspector which must be satisfied by the <typeparamref name="TSubject" />.</param>
/// <returns>An <see cref="AndConstraint{T}" /> which can be used to chain assertions.</returns>
/// <exception cref="ArgumentNullException"><paramref name="expected"/> is <see langword="null"/>.</exception>
public AndConstraint<TAssertions> Satisfy<T>(Action<T> expected)
where T : TSubject
{
Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot verify an object against a <null> inspector.");

Execute.Assertion
.ForCondition(Subject is T)
.WithDefaultIdentifier(Identifier)
.FailWith("Expected {context:object} to be assignable to {0}{reason}, but {1} is not.", typeof(T), Subject.GetType());

string[] failuresFromInspector;

using (var assertionScope = new AssertionScope())
{
expected((T)Subject);
failuresFromInspector = assertionScope.Discard();
}

if (failuresFromInspector.Length > 0)
{
string failureMessage = Environment.NewLine
+ string.Join(Environment.NewLine, failuresFromInspector.Select(x => x.IndentLines()));

Execute.Assertion
.WithDefaultIdentifier(Identifier)
.WithExpectation("Expected {context:object} to match inspector, but the inspector was not satisfied:", Subject)
.FailWithPreFormatted(failureMessage);
}

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Returns the type of the subject the assertion applies on.
/// It should be a user-friendly name as it is included in the failure message.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2001,6 +2001,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> NotBeOfType(System.Type unexpectedType, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeOfType<T>(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeSameAs(TSubject unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Satisfy<T>(System.Action<T> expected)
where T : TSubject { }
}
public class SimpleTimeSpanAssertions : FluentAssertions.Primitives.SimpleTimeSpanAssertions<FluentAssertions.Primitives.SimpleTimeSpanAssertions>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2085,6 +2085,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> NotBeOfType(System.Type unexpectedType, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeOfType<T>(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeSameAs(TSubject unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Satisfy<T>(System.Action<T> expected)
where T : TSubject { }
}
public class SimpleTimeSpanAssertions : FluentAssertions.Primitives.SimpleTimeSpanAssertions<FluentAssertions.Primitives.SimpleTimeSpanAssertions>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1952,6 +1952,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> NotBeOfType(System.Type unexpectedType, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeOfType<T>(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeSameAs(TSubject unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Satisfy<T>(System.Action<T> expected)
where T : TSubject { }
}
public class SimpleTimeSpanAssertions : FluentAssertions.Primitives.SimpleTimeSpanAssertions<FluentAssertions.Primitives.SimpleTimeSpanAssertions>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2001,6 +2001,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> NotBeOfType(System.Type unexpectedType, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeOfType<T>(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeSameAs(TSubject unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Satisfy<T>(System.Action<T> expected)
where T : TSubject { }
}
public class SimpleTimeSpanAssertions : FluentAssertions.Primitives.SimpleTimeSpanAssertions<FluentAssertions.Primitives.SimpleTimeSpanAssertions>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using Xunit;
using Xunit.Sdk;

namespace FluentAssertions.Specs.Primitives;

public partial class ReferenceTypeAssertionsSpecs
{
[Fact]
public void When_object_matches_predicate_it_should_not_throw()
{
// Arrange
var someObject = new object();

// Act / Assert
someObject.Should().Match(o => o != null);
}

[Fact]
public void When_typed_object_matches_predicate_it_should_not_throw()
{
// Arrange
var someObject = new SomeDto
{
Name = "Dennis Doomen",
Age = 36,
Birthdate = new DateTime(1973, 9, 20)
};

// Act / Assert
someObject.Should().Match<SomeDto>(o => o.Age > 0);
}

[Fact]
public void When_object_does_not_match_the_predicate_it_should_throw()
{
// Arrange
var someObject = new object();

// Act
Action act = () => someObject.Should().Match(o => o == null, "it is not initialized yet");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected someObject to match (o == null) because it is not initialized yet*");
}

[Fact]
public void When_a_typed_object_does_not_match_the_predicate_it_should_throw()
{
// Arrange
var someObject = new SomeDto
{
Name = "Dennis Doomen",
Age = 36,
Birthdate = new DateTime(1973, 9, 20)
};

// Act
Action act = () => someObject.Should().Match((SomeDto d) => d.Name.Length == 0, "it is not initialized yet");

// Assert
act.Should().Throw<XunitException>().WithMessage(
"Expected someObject to match (d.Name.Length == 0) because it is not initialized yet*");
}

[Fact]
public void When_object_is_matched_against_a_null_predicate_it_should_throw()
{
// Arrange
var someObject = new object();

// Act
Action act = () => someObject.Should().Match(null);

// Assert
act.Should().Throw<ArgumentNullException>().WithMessage(
"Cannot match an object against a <null> predicate.*");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
using System;
using Xunit;
using Xunit.Sdk;

namespace FluentAssertions.Specs.Primitives;

public partial class ReferenceTypeAssertionsSpecs
{
[Fact]
public void When_object_satisfies_inspector_it_should_not_throw()
{
// Arrange
var someObject = new object();

// Act
Action act = () => someObject.Should().Satisfy<object>(x => x.Should().NotBeNull());

// Assert
act.Should().NotThrow();
}

[Fact]
public void When_typed_object_satisfies_inspector_it_should_not_throw()
{
// Arrange
var someObject = new PersonDto
{
Name = "Name Nameson",
Birthdate = new DateTime(2000, 1, 1),
};

// Act
Action act = () => someObject.Should().Satisfy<PersonDto>(o => o.Age.Should().BeGreaterThan(0));

// Assert
act.Should().NotThrow();
}

[Fact]
public void When_object_does_not_satisfy_the_inspector_it_should_throw()
{
// Arrange
var someObject = new object();

// Act
Action act = () => someObject.Should().Satisfy<object>(o => o.Should().BeNull("it is not initialized yet"));

// Assert
act.Should().Throw<XunitException>().WithMessage(
$"""
Expected {nameof(someObject)} to match inspector, but the inspector was not satisfied:
*Expected o to be <null> because it is not initialized yet, but found System.Object*
""");
}

[Fact]
public void When_a_typed_object_does_not_satisfy_the_inspector_it_should_throw()
{
// Arrange
const string personName = "Name Nameson";
var somePerson = new PersonDto
{
Name = personName,
Birthdate = new DateTime(1973, 9, 20),
};

// Act
Action act = () => somePerson.Should().Satisfy<PersonDto>(d => d.Name.Should().HaveLength(0, "it is not initialized yet"));

// Assert
act.Should().Throw<XunitException>().WithMessage(
$"""
Expected {nameof(somePerson)} to match inspector, but the inspector was not satisfied:
*Expected d.Name with length 0 because it is not initialized yet, but found string "{personName}" with length {personName.Length}.
""");
}

[Fact]
public void When_a_complex_typed_object_does_not_satisfy_inspector_it_should_throw()
{
// Arrange
var someComplexDto = new PersonAndAddressDto
{
Person = new PersonDto
{
Name = "Buford Howard Tannen",
Birthdate = new DateTime(1937, 3, 26),
},
Address = new AddressDto
{
Street = "Mason Street",
Number = "1809",
City = "Hill Valley",
Country = "United States",
PostalCode = "CA 91905",
},
};

// Act
Action act = () => someComplexDto.Should().Satisfy<PersonAndAddressDto>(dto =>
{
dto.Person.Should().Satisfy<PersonDto>(person =>
{
person.Name.Should().Be("Biff Tannen");
person.Age.Should().Be(48);
person.Birthdate.Should().Be(new DateTime(1937, 3, 26));
});
dto.Address.Should().Satisfy<AddressDto>(address =>
{
address.Street.Should().Be("Mason Street");
address.Number.Should().Be("1809");
address.City.Should().Be("Hill Valley, San Diego County, California");
address.Country.Should().Be("United States");
address.PostalCode.Should().Be("CA 91905");
});
});

// Assert
act.Should().Throw<XunitException>().WithMessage(
$"""
Expected {nameof(someComplexDto)} to match inspector, but the inspector was not satisfied:
*Expected dto.Person to match inspector*
*Expected person.Name*
*Expected dto.Address to match inspector*
*Expected address.City*
""");
}

[Fact]
public void When_a_typed_object_is_satisfied_against_an_incorrect_type_it_should_throw()
{
// Arrange
var personDto = new PersonDto();

// Act
Action act = () => personDto.Should().Satisfy<AddressDto>(dto => dto.Should().NotBeNull());

// Assert
act.Should().Throw<XunitException>()
.WithMessage($"Expected {nameof(personDto)} to be assignable to {typeof(AddressDto)}, but {typeof(PersonDto)} is not.");
}

[Fact]
public void When_a_typed_object_does_not_match_multiple_inspectors_it_should_throw()
{
// Arrange
var somePerson = new PersonDto
{
Name = "Name Nameson",
Birthdate = new DateTime(2000, 1, 1),
};

// Act
Action act = () => somePerson.Should().Satisfy<PersonDto>(d =>
{
d.Name.Should().Be("Someone Else");
d.Age.Should().BeLessThan(20);
d.Birthdate.Should().BeAfter(new DateTime(2001, 1, 1));
});

// Assert
act.Should().Throw<XunitException>().WithMessage(
$"""
Expected {nameof(somePerson)} to match inspector, but the inspector was not satisfied:
*Expected d.Name*
*Expected d.Age*
*Expected d.Birthdate*
""");
}

[Fact]
public void When_object_is_satisfied_against_a_null_inspector_it_should_throw()
{
// Arrange
var someObject = new object();

// Act
Action act = () => someObject.Should().Satisfy<object>(null);

// Assert
act.Should().Throw<ArgumentNullException>()
.WithMessage("Cannot verify an object against a <null> inspector.*");
}
}

file class PersonDto
{
public string Name { get; init; }

public DateTime Birthdate { get; init; }

public int Age => DateTime.UtcNow.Subtract(Birthdate).Days / 365;
}

file class PersonAndAddressDto
{
public PersonDto Person { get; init; }

public AddressDto Address { get; init; }
}

file class AddressDto
{
public string Street { get; init; }

public string Number { get; init; }

public string City { get; init; }

public string PostalCode { get; init; }

public string Country { get; init; }
}

0 comments on commit c330735

Please sign in to comment.