Fluent Assertions

Was mir bei NUnit besser gefällt als bei MSTest ist die Aussagekraft von den Assertions. “Assert.That(something, Is.EqualTo(anotherthing);” finde ich viel aussagekräftiger als “Assert.AreEqual(anotherthing, something). Besonders die generischen Variante “Assert.AreEqual<string>(expectedstring, actualstring)” finde ich auf den ersten Blick nicht leicht verständlich.

Deshalb kam ich auf die Idee eine eigene Lösung mit einem Fluent-Interface für MSTest zu entwickeln. Allerdings hat eine kurze Suche bei Google ergeben, das es hier schon etliches auf dem Markt gibt. Näher angesehen habe ich mir bisher Fluent Assertions.

Die Lösung steht auf GitHub zur Verfügung – kann aber bequem über NuGet installiert werden.

FluentAssertions

Die Lösung funktioniert sowohl mit MSTest, NUnit, XUnit, MSpec, MBUnit unddem  Gallio Framework! Eine Dokumentation steht hier zur Verfügung.

Jetzt wird es aber Zeit für ein paar Beispiele. Ein paar Zeilen Code sagt mehr als tausend Worte.

object myobject = null;
myobject = "the object is a string...";
myobject.Should().NotBeNull().And.BeOfTyp<string>();

Wie man sieht sind die Assertions als Extension-Methods für die eigentlichen Objekte realisiert. Das Wort “Assert” kommt also erst mal nicht vor – das ist etwas gewöhnungsbedürftig. Das Ergebnis liest sich aber sehr flüssig. Besonders die Verknüpfung mit .And erspart separate Zeilen Code. Schade, dass es (noch) kein .Or gibt!

string otherObject = "the object is a string...";
myobject.Should().Be(otherObject);
myobject.Should().BeSameAs(otherObject);

“Be” ist dabei Equality (Assert.AreEqual) und BeSameAs Referenzgleichheit (Assert.AreSame). Wei Wertetypen wie hier verhalten sich beide gleich.

Für unterschiedliche Typen gibt es jetzt ganz unterschiedliche Funktionen. Hier ein paar Beispiele für Integers, DateTime und Strings.

[TestMethod]
public void Integers()
{
    int a = 5;
    int b = 5;
    a.Should().Be(b);
    a.Should().BePositive();
    a.ShouldBeEquivalentTo(b);

    a = 6;
    a.Should().BeGreaterThan(b);
}

[TestMethod]
public void DateTimes()
{
    DateTime d = DateTime.Now.AddMilliseconds(90);
    d.Should().BeCloseTo(DateTime.Now, 90);

    DateTime appointment = DateTime.Now.AddDays(5);
    DateTime alert = appointment.AddDays(-1);
    alert.Should().BeExactly(24.Hours()).Before(appointment);
}

[TestMethod]
public void Strings()
{
    string mystring = "Cities in Germany: Stuttgart, München, Berlin";
    mystring.Should().StartWith("Cities");
    mystring.Should().Match("*Germany:");
    mystring.Should().Contain("Stuttgart");
}

Das meiste hier ist selbsterklärend und mit etwas Übung findet man sich schnell damit zurecht. Erwähnenswert sind noch Collections. Hier gibt es ein reiches Set an Assertions.

[TestMethod]
public void Collections()
{
    IEnumerable collection = new[] { "Item 1", "Item 2", "Item 3", "Item 4" };
    collection.Should().NotBeEmpty()
        .And.HaveCount(4)
        .And.ContainInOrder(new[] {"Item 2", "Item 3" })
        .And.ContainItemsAssignableTo<string>();

    collection.Should().BeEquivalentTo(new[] { "Item 1", "Item 2", "Item 3", "Item 4" });

    collection = new[] { 2, 3, 4, 5 };
    collection.Should().NotBeEmpty();
    collection.Should().BeInAscendingOrder();
    collection.Should().BeEquivalentTo(2, 3, 4, 5);
    collection.Should().BeSubsetOf(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
}

Auch das Fehlerhandling kann hier elegant abgebildet werden. Entweder als Act oder als Invoking.

[TestMethod]
public void Exceptions()
{
    Action act = () =>
        {
            throw new InvalidOperationException("...",
                new ArgumentException("A error message..."));
        };

    act.ShouldThrow&lt;InvalidOperationException&gt;()
            .WithInnerException<ArgumentException>()
            .WithInnerMessage("A error message...");

    BusinessObject subject = new BusinessObjectA();
    subject.Invoking(x => x.DoSomething())
            .ShouldThrow<InvalidOperationException>()
            .WithMessage("An explicit message...");
}

Dann gibt es noch ein paar Möglichkeiten, die über normale Assertion hinausgehen. Zum Beispiel können bei den TypeAssertions bestimmte Bedingungen für Eigenschaften oder Methoden einer Klasse festgelegt werden.

[TestMethod]
public void TypeAssertions()
{
    typeof(BusinessObject).Methods()
            .ThatReturn<BusinessObject>()
            .ThatAreDecoratedWith<Attribute>()
            .Should()
            .BeDecoratedWith<AttributeB>;();
}

Auf diese Weise kann Tests verwenden, um Architekturen zu validieren.

Mit ExecutionTime oder ExecutionTimeOf können nichtfunktionale Anforderung per Test validiert werden. Das kann in verschiedenen Szenarien auch sehr hilfreich sein.

[TestMethod]
public void ExecutionTime()
{
    var subject = new BusinessObjectA();
    subject.ExecutionTimeOf(s => s.ExpensiveMethod()).ShouldNotExceed(500.Milliseconds());

    Action someAction = () => Thread.Sleep(99);
    someAction.ExecutionTime().ShouldNotExceed(100.Milliseconds());
}

Als letztes möchte ich noch kurz Möglichkeiten zur Erweiterung vorstellen. Man kann einfach selber statische Erweiterungsmethoden definieren in denen man die Methoden von FluentAssertions.Execution.Execute.Verification wie in folgendem Beispiel aufruft.

[TestMethod]
public void CustomAssertions()
{
    IEnumerable<string> collection = new[] { ";Item 1", "Item 2", "Item 3", "Item 4" };
    collection.Should().OnlyContainItems("because the control xy only supports items.");
}

//...

static class Extensions
{
    public static void OnlyContainItems<T>(
        this FluentAssertions.Collections.GenericCollectionAssertions<T> assertions,
        string reason,
        params object[] reasonArgs)
    {

        var x = assertions.Subject.As<IEnumerable<string>>();

        FluentAssertions.Execution.Execute.Verification
            .ForCondition(x.All(i => i.StartsWith("Item")))
            .BecauseOf(reason, reasonArgs)
            .FailWith("Expected {context:collection} only to contain items{reason}.", reason);
    }
}

Fazit

Das Projekt gefällt mit sehr gut und ich werde es definitiv zukünftig in Projekten einsetzen. Besonders die Unabhängigkeit von den einzelnen Test-Frameworks macht eine spätere Portierung auf andere Lösungen sehr einfach. Das eine oder andere hätte ich selber sich anders gelöst – ich denke aber das man sich nach kurzer Einarbeitung schnell daran gewöhnt.

2 comments

  1. Hi there! I know this is kinda off topic but I was wondering
    if you knew where I could locate a captcha plugin for my comment form?

    I’m using the same blog platform ass yours
    and I’m having problems finding one? Thanks a lot!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s