FluentAssertions Asserting multiple properties of a single object
Asked Answered
A

4

42

Is there a way to do something like this using FluentAssertions

response.Satisfy(r =>
    r.Property1== "something" &&
    r.Property2== "anotherthing"));

I am trying to avoid writing multiple Assert statements. This was possible with https://sharptestex.codeplex.com/ which I was using for the longest time. But SharpTestEx does not support .Net Core.

Approver answered 21/4, 2017 at 13:36 Comment(2)
What should this do?Linzer
At the time of writing the accepted answer may have been the best, not anymore...Groveman
H
26

You should be able to use general purpose Match assertion to verify multiple properties of the subject via a predicate

response.Should()
        .Match<MyResponseObject>((x) => 
            x.Property1 == "something" && 
            x.Property2 == "anotherthing"
        );
Herlindaherm answered 29/5, 2017 at 20:15 Comment(2)
While this code works, error message when assertion fails is very awkward. Too far from what FluentAssertions usually produces. I'd suggest to use multiple assertions instead :)Garey
@Garey or use ShouldBeEquivalentToBrittney
B
50

The .Match() solution does not return a good error message. So if you want to have a good error and only one assert then use:

result.Should().BeEquivalentTo(new MyResponseObject()
            {
                Property1 = "something",
                Property2 = "anotherthing"
            });

Anonymous objects (use with care!)

If you want to only check certain members then use:

    result.Should().BeEquivalentTo(new
            {
                Property1 = "something",
                Property2 = "anotherthing"
            }, options => options.ExcludingMissingMembers());

Note: You will miss (new) members when testing like this. So only use if you really want to check only certain members now and in the future. Not using the exclude option will force you to edit your test when a new property is added and that can be a good thing

Multiple asserts

Note: All given solutions gives you one line asserts. In my opinion there is nothing wrong with multiple lines of asserts as long as it is one assert functionally.

If you want this because you want multiple errors at once, consider wrapping your multi line assertions in an AssertionScope.

using (new AssertionScope())
{
    result.Property1.Should().Be("something");
    result.Property2.Should().Be("anotherthing");
}

Above statement will now give both errors at once, if they both fail.

https://fluentassertions.com/introduction#assertion-scopes

Brittney answered 4/10, 2018 at 9:49 Comment(2)
Can you ellaborate on this one "Anonymous objects (use with care!)"?Thurnau
@Menyus Yes, you will miss (new) members when testing like this. So only use if you really want to check only certain members now and in the future. Not using the exclude option will force you to edit your test when a new property is added and that can be a good thing.Brittney
H
26

You should be able to use general purpose Match assertion to verify multiple properties of the subject via a predicate

response.Should()
        .Match<MyResponseObject>((x) => 
            x.Property1 == "something" && 
            x.Property2 == "anotherthing"
        );
Herlindaherm answered 29/5, 2017 at 20:15 Comment(2)
While this code works, error message when assertion fails is very awkward. Too far from what FluentAssertions usually produces. I'd suggest to use multiple assertions instead :)Garey
@Garey or use ShouldBeEquivalentToBrittney
E
2

Assuming you use xUnit, you can just solve it by inheriting from the right base class. There is no need for an implementation change in your tests. Here is how this works:

public class UnitTest1 : TestBase
{
    [Fact]
    public void Test1()
    {
        string x = "A";
        string y = "B";
        string expectedX = "a";
        string expectedY = "b";
        x.Should().Be(expectedX);
        y.Should().Be(expectedY);
    }
}

public class TestBase : IDisposable
{
    private AssertionScope scope;
    public TestBase()
    {
        scope = new AssertionScope();
    }

    public void Dispose()
    {
        scope.Dispose();
    }
}

Alternatively, you can just wrap your expectations into a ValueTuple. Here is how:

[Fact]
public void Test2()
{
    string x = "A";
    string y = "B";
    string expectedX = "a";
    string expectedY = "b";
    (x, y).Should().Be((expectedX, expectedY));
}
Exportation answered 4/11, 2020 at 17:35 Comment(0)
E
1

I use an extension function for this that works similarly to SatisfyRespectively():

public static class FluentAssertionsExt {
    public static AndConstraint<ObjectAssertions> Satisfy(
        this ObjectAssertions parent,
        Action<MyClass> inspector) {
        inspector((MyClass)parent.Subject);
        return new AndConstraint<ObjectAssertions>(parent);
    }
}

Here is how I use it:

[TestMethod] public void FindsMethodGeneratedForLambda() =>
    Method(x => x.Lambda())
    .CollectGeneratedMethods(visited: empty)
    .Should().ContainSingle().Which
        .Should().Satisfy(m => m.Name.Should().Match("<Lambda>*"))
        .And.Satisfy(m => m.DeclaringType.Name.Should().Be("<>c"));

[TestMethod] public void FindsMethodGeneratedForClosure() =>
    Method(x => x.Closure(0))
    .CollectGeneratedMethods(visited: empty)
    .Should().HaveCount(2).And.SatisfyRespectively(
        fst => fst.Should()
            .Satisfy(m => m.Name.Should().Be(".ctor"))
            .And.Satisfy(m => m.DeclaringType.Name.Should().Match("<>c__DisplayClass*")),
        snd => snd.Should()
            .Satisfy(m => m.Name.Should().Match("<Closure>*"))
            .And.Satisfy(m => m.DeclaringType.Name.Should().Match("<>c__DisplayClass*")));

Unfortunately this doesn't generalize very well due to FluentAssertions' design, so you might have to provide multiple overloads of this method with different types in place of MyClass.

I think the truly correct way however is to implement an *Assertions type for the type you want to run such assertions against. The documentation provides an example:

public static class DirectoryInfoExtensions 
{
    public static DirectoryInfoAssertions Should(this DirectoryInfo instance)
    {
      return new DirectoryInfoAssertions(instance); 
    } 
}

public class DirectoryInfoAssertions : 
    ReferenceTypeAssertions<DirectoryInfo, DirectoryInfoAssertions>
{
    public DirectoryInfoAssertions(DirectoryInfo instance)
    {
        Subject = instance;
    }

    protected override string Identifier => "directory";

    public AndConstraint<DirectoryInfoAssertions> ContainFile(
        string filename, string because = "", params object[] becauseArgs)
    {
        Execute.Assertion
            .BecauseOf(because, becauseArgs)
            .ForCondition(!string.IsNullOrEmpty(filename))
            .FailWith("You can't assert a file exist if you don't pass a proper name")
            .Then
            .Given(() => Subject.GetFiles())
            .ForCondition(files => files.Any(fileInfo => fileInfo.Name.Equals(filename)))
            .FailWith("Expected {context:directory} to contain {0}{reason}, but found {1}.", 
                _ => filename, files => files.Select(file => file.Name));

        return new AndConstraint<DirectoryInfoAssertions>(this);
    }
}
Eckart answered 30/10, 2020 at 15:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.