DEV Community

Mauro Petrini πŸ‘¨β€πŸ’»
Mauro Petrini πŸ‘¨β€πŸ’»

Posted on • Edited on

Combine xUnit with Fluent Assertions


Welcome! Spanish articles on LinkedIn. You can follow me on Twitter for news.


xUnit

One of the most popular frameworks to test code in the .NET ecosystem is xUnit.

xUnit.net is a free, open-source, community-focused unit testing tool for .NET.

A common situation using xUnit

xUnit uses the Assert class to verify conditions during the process of running tests. This class provides various extensions methods that commonly use two parameters:

  • Expected value
  • Actual value

Let's see an example.

    var variable = 1;

    Assert.Equal(10, variable);
    Assert.Equal(variable, 10);

So, which one of these Assert.Equal methods are correct? When we mix up the expected and the actual value parameters, if the test fails, the failure message may not make much sense.

In the first case, we get the correct message.

Assert.Equal() Failure
Expected: 10
Actual: 1

The second one is incorrect cause are expecting 10, not 1

Assert.Equal() Failure
Expected: 1
Actual: 10

Fluent Assertions

Fluent Assertions is a library that provides us:

  • Clearer explanations about why a test failed
  • Improve readability of test source code

Basically, with this library, we can read a test more like an English sentence.

If we perform the same test using Fluent Assertions library, the code will look something like this:

    var variable = 1;

    variable.Should().Be(10);

Let's take a look at the failure message.

Expected variable to be 10 but found 1.

This message is clearer than the Assert failure message. A more descriptive failure message may prevent the need for debugging through the test.

Let's dive into some examples

Inline asserts

We could write our asserts inline using the And constraint of fluent assertions.

    [Fact]
    public void xUnit_StringTest()
    {
        string code = "001SUMMERCODE";

        Assert.NotNull(code);
        Assert.NotEmpty(code);
        Assert.StartsWith("001", code);
        Assert.EndsWith("code".ToUpper(), code);
    }

    [Fact]
    public void xUnit_FluentAssertions_StringTest()
    {
        string code = "001SUMMERCODE";

        //code.Should().NotBeNullOrEmpty();
        //code.Should().StartWith("001");
        //code.Should().EndWithEquivalent("code");

        code.Should().NotBeNullOrEmpty().And.StartWith("001").And.EndWithEquivalent("code");
    }

Custom messages

The because parameter allows us to set a custom message when a test fails.

    [Fact]
    public void xUnit_FluentAssertions_CustomMessageTest()
    {
        string code = "002SUMMERCODE";

        code.Should().NotBeNullOrEmpty().And.
            StartWith("001", because: "the first batch of codes start with 001").And.
            EndWithEquivalent("code");
    }

Output

Expected code to start with
"001" because the first batch of codes start with 001, but
"002SUMMERCODE" differs near "2SU" (index 2).

Assertion scope

If we have multiple asserts and one fails, the next ones do not execute. This is the default behavior, but we can change it through the AssertionScope.

    [Fact]
    public void xUnit_FluentAssertions_StringTest()
    {
        string code = "001SUMMERCODE";

        code.Should().NotBeNullOrEmpty();
        code.Should().StartWith("002"); //fails
        code.Should().EndWithEquivalent("code");
        code.Should().ContainEquivalentOf("SUMMERS");//fails
    }

Output

Expected code to start with
"002", but
"001SUMMERCODE" differs near "1SU" (index 2).

Using AssertionScope

    [Fact]
    public void xUnit_FluentAssertions_StringTest()
    {
        string code = "001SUMMERCODE";

        using (new AssertionScope())
        {
            code.Should().NotBeNullOrEmpty();
            code.Should().StartWith("002"); //fails
            code.Should().EndWithEquivalent("code");
            code.Should().ContainEquivalentOf("SUMMERS"); //fails
        }
    }

Output

Expected code to start with
"002", but
"001SUMMERCODE" differs near "1SU" (index 2).
Expected code to contain equivalent of
"SUMMERS" but found
"001SUMMERCODE".

The difference is that with AssertionScope, we run all asserts.

Exceptions

The Throw and ThrowExactly methods help us to test if a method throws an exception. I recommend using ThrowExactly because Throw pass tests when check inheritance.

Let's define a Person class.

    public class Person
    {
        public string Name { get; }

        public Person(string name)
        {
            Name = name ?? throw new ArgumentNullException(nameof(name));
        }
    }

Then, test the constructor to throw the ArgumentNullException using Throw.

    [Fact]
    public void xUnit_FluentAssertions_ExceptionTest()
    {
        Action act = () => new Person(null);

        act.Should().Throw<Exception>()
            .WithMessage("Value cannot be null. (Parameter 'name')");
    }

That's a successful test!

Switch to ThrowExactly

    [Fact]
    public void xUnit_FluentAssertions_ExceptionTest()
    {
        Action act = () => new Person(null);

        act.Should().ThrowExactly<Exception>()
            .WithMessage("Value cannot be null. (Parameter 'name')");
    }

Output

Expected type to be System.Exception, but found System.ArgumentNullException.

End!

That was an introduction to this amazing library! I use a lot in the projects that I'm working on because of readability and easy use.

Top comments (1)

Collapse
 
srivatsahg profile image
Srivatsa Haridas

Thanks That was a short and clear introduction to Fluent Assertions using xUnit !