Skip to content

Commit

Permalink
Merge a6b5b8c into e969354
Browse files Browse the repository at this point in the history
  • Loading branch information
siewers committed Mar 17, 2024
2 parents e969354 + a6b5b8c commit 3761571
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 1 deletion.
42 changes: 42 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,47 @@ public AndConstraint<TAssertions> NotBeAssignableTo(Type type, string because =
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Allows combining one or more assertions using the other assertion methods that this library offers on an instance of <typeparamref name="T"/>.
/// </summary>
/// <remarks>
/// If multiple assertions executed by the <paramref name="assertion"/> fail, they will be raised as a single failure.
/// </remarks>
/// <param name="assertion">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="assertion"/> is <see langword="null"/>.</exception>
public AndConstraint<TAssertions> Satisfy<T>(Action<T> assertion)
where T : TSubject
{
Guard.ThrowIfArgumentIsNull(assertion, nameof(assertion), "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())
{
assertion((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> assertion)
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> assertion)
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> assertion)
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> assertion)
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,288 @@
using System;
using FluentAssertions.Extensions;
using Xunit;
using Xunit.Sdk;

namespace FluentAssertions.Specs.Primitives;

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

// Act / Assert
someObject.Should().Satisfy<object>(x => x.Should().NotBeNull());
}

[Fact]
public void Object_not_satisfying_inspector_throws()
{
// 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 Object_satisfied_against_null_throws()
{
// 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.*");
}

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

// Act / Assert
personDto.Should().Satisfy<PersonDto>(o => o.Age.Should().BeGreaterThan(0));
}

[Fact]
public void Complex_typed_object_satisfying_inspector_does_not_throw()
{
// Arrange
var complexDto = new PersonAndAddressDto
{
Person = new PersonDto
{
Name = "Name Nameson",
Birthdate = new DateTime(2000, 1, 1),
},
Address = new AddressDto
{
Street = "Named St.",
Number = "42",
City = "Nowhere",
Country = "Neverland",
PostalCode = "12345",
}
};

// Act / Assert
complexDto.Should().Satisfy<PersonAndAddressDto>(dto =>
{
dto.Person.Should().Satisfy<PersonDto>(person =>
{
person.Name.Should().Be("Name Nameson");
person.Age.Should().BeGreaterThan(0);
person.Birthdate.Should().Be(1.January(2000));
});
dto.Address.Should().Satisfy<AddressDto>(address =>
{
address.Street.Should().Be("Named St.");
address.Number.Should().Be("42");
address.City.Should().Be("Nowhere");
address.Country.Should().Be("Neverland");
address.PostalCode.Should().Be("12345");
});
});
}

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

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

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

[Fact]
public void Complex_typed_object_not_satisfying_inspector_throws()
{
// Arrange
var complexDto = 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 = () => complexDto.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(26.March(1937));
});
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(complexDto)} 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 Typed_object_satisfied_against_incorrect_type_throws()
{
// 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 Sub_class_satisfied_against_base_class_does_not_throw()
{
// Arrange
var subClass = new SubClass
{
Number = 42,
Date = new DateTime(2021, 1, 1),
Text = "Some text"
};

// Act / Assert
subClass.Should().Satisfy<BaseClass>(x =>
{
x.Number.Should().Be(42);
x.Date.Should().Be(1.January(2021));
});
}

[Fact]
public void Base_class_satisfied_against_sub_class_throws()
{
// Arrange
var baseClass = new BaseClass
{
Number = 42,
Date = new DateTime(2021, 1, 1),
};

// Act
Action act = () => baseClass.Should().Satisfy<SubClass>(x =>
{
x.Number.Should().Be(42);
x.Date.Should().Be(1.January(2021));
x.Text.Should().Be("Some text");
});

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

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

public DateTime Birthdate { get; init; }

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

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

public AddressDto Address { get; init; }
}

private 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; }
}

private class BaseClass
{
public int Number { get; init; }

public DateTime Date { get; init; }
}

private sealed class SubClass : BaseClass
{
public string Text { get; init; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace FluentAssertions.Specs.Primitives;

public class ReferenceTypeAssertionsSpecs
public partial class ReferenceTypeAssertionsSpecs
{
[Fact]
public void When_the_same_objects_are_expected_to_be_the_same_it_should_not_fail()
Expand Down

0 comments on commit 3761571

Please sign in to comment.