06 Dec
2008

Extension-Methods für schöneres Unit-Testing

 

Schönes Unit-Testing? Was soll denn dies sein.

Ok, schön ist Relativ. Ich finde dass die Lesbarkeit von Quelltexten sehr wichtig ist, auch die von Unit-Tests. Ich definiere dies dann als schön ;)

Die Assert-Syntax der Unit-Testing Frameworks war für mich nie so sonderlich einleuchtend. Da es ist einfach nicht so prickelnd liest.

[Test]
public void Fehlerhafte_EMail_mit_zwei_at_Zeichen()
{
    var validator =  new EMailValidator();

    Assert.IsFalse(validator.Validate("a@@bd.cd"));
    Assert.IsFalse(validator.Validate("a@.@bd.cd"));
}

Ich bin ein von Links-nach-Rechts Leser, mein ganzes Leben schon und hier muss ich mehr oder weniger umdenken. Klar, es geht, jedoch gibt es da Verbesserungspotential.

Mit Behavior-Driven-Development (BDD) erhalten die Unit-Tests eine neue Syntax. Es steht nicht der Test im Mittelpunkt, sondern die Spezifikation. Dazu gibt es auch eine verbesserte Schreibweise der Tests. Die jedoch hier nicht Thema ist, dazu später einmal mehr. In der Zwischenzeit verweise ich auf einen Blog-Eintrag von Stefan Lieser der dazu nächstes Jahr einige Vorträge halten wird. Jetzt nur die Empfehlung sich BDD näher anzusehen, da es die Denkweise für Unit-Tests verändert und leichter Zugänglich macht.

Jedoch kann man Teile des BDD-Style auch im klassischen Unit-Test verwenden. Wenn z.B. BDD nicht auf die zu testende Unit passt und man dort lieber Old School testet.

[Test]
public void Fehlerhafte_EMail_mit_zwei_at_Zeichen()
{
    var validator =  new EMailValidator();

    validator.Validate("a@@bd.cd").should_be_false();
    validator.Validate("aa@.@bd.cd").should_be_false();
}

In dieser Form ist der Test besser zu verstehen (für uns Links-nach-Rechts Leser). should_be_false() ist eine Extension-Method die auch nur einen Assert macht.

Diese Extension-Method habe ich von JP Boodhoo, auf einem Vortrag in Bonn hat er diese vorgestellt. Ich war sofort Feuer und Flamme und ich sagte zur mir; so einfach und nicht selbst darauf gekommen. Es wurde auch kein spezielles BDD-Test-Framework (wie MSpec oder NBhave) verwendet, sondern klassisches MbUnit.

Mittlerweile sind auch eigene dazu gekommen und vorhandene verbessert worden.

public static class BDDExtensions
{
    public static void force_traversal<T>(this IEnumerable<T> items)
    public static void should_be_null(this object item)
    public static void should_be_equal_to(this object item, object other)
    public static void should_contain<T>(this IEnumerable<T> items, T item)
    public static void should_be_greater_than<T>(this T item, T other) where T : IComparable<T>
    public static void should_not_be_equal_to<T>(this T item, T other)
    public static void should_be_equal_ignoring_case(this string item, string other)
    public static void should_contain<T>(this IEnumerable<T> items, IEnumerable<T> itemsToFind)
    public static void should_only_contain<T>(this IEnumerable<T> items, IEnumerable<T> itemsToFind)
    public static void should_only_contain_in_order<T>(this IEnumerable<T> items, IEnumerable<T> itemsToFind)
    public static void should_be_true(this bool item)
    public static void should_be_false(this bool item)
    public static void should_be_equal_to<T>(this T actual, T expected)
    public static ExceptionType should_throw_an<ExceptionType>(this Action workToPerform)
    public static void should_not_throw_any_exceptions(this Action workToPerform)
    public static void should_be_an_instance_of<Type>(this object item)
    public static void should_not_be_null(this object item)
    public static void should_not_be_null_or_empty(this string item)
    public static void should_be_null_or_empty(this string item)
    public static void should_be_empty<T>(this IEnumerable<T> items)
    public static void should_not_be_empty<T>(this IEnumerable<T> items)
}

Dies ist der aktuelle Stand von Basis-Extension-Methods die ich in Unit-Test und BDD-Tests verwende. Den kompletten Quelltext gibt es am Ende des Artikels.

Ich erweitere diese immer sobald es mir Sinnvoll erscheint. Auch mache mir spezielle Extension-Methods z.B. für ASP.NET MVC (dazu später mehr).

Hier noch ein kleines Beispiel um zu überprüfen ob in einer Liste bestimmte Elemente vorhanden sind.

[Test]
public void Wird_eine_Liste_mit_Namen_zurueckgeben()
{
    List<string> names = FooBar.GetNames();    
    names.should_only_contain(new List<string>{"Albert","Peter","Weinert"});
}

Der Einsatz von Extension-Methods anstelle von Assert.IsXYZ() führt zu einer besseren Lesbarkeit von Tests und somit zu schöneren Unit-Tests.

Hier nun der komplette Quelltext der Extension-Methods in Verbindung mit MbUnit, jedoch können diese einfach an andere Unit-Test-Frameworks angepasst werden.

/*
 * based on the BDD Extensions of Jean-Paul Boodhoo http://jpboodhoo.com
 * 
 */
using System;
using System.Collections.Generic;
using System.Linq;

using MbUnit.Framework;

namespace DerAlbert.UnitTest.Base.BDD
{
    public static class The
    {
        public static Action action(Action action)
        {
            return action;
        }
    }

    public static class BDDExtensions
    {
        public static void force_traversal<T>(this IEnumerable<T> items)
        {
            items.Count();
        }

        public static void should_be_null(this object item)
        {
            Assert.IsNull(item);
        }

        public static void should_be_equal_to(this object item, object other)
        {
            Assert.AreEqual(other, item);
        }

        public static void should_contain<T>(this IEnumerable<T> items, T item)
        {
            Assert.IsTrue(new List<T>(items).Contains(item));
        }

        public static void should_be_greater_than<T>(this T item, T other) where T : IComparable<T>
        {
            (item.CompareTo(other) > 0).should_be_true();
        }

        public static void should_not_be_equal_to<T>(this T item, T other)
        {
            Assert.AreNotEqual(other, item);
        }

        public static void should_be_equal_ignoring_case(this string item, string other)
        {
            StringAssert.AreEqualIgnoreCase(item, other);
        }

        public static void should_contain<T>(this IEnumerable<T> items, IEnumerable<T> itemsToFind)
        {
            var results = new List<T>(items);
            foreach (T itemToFind in itemsToFind)
            {
                results.Contains(itemToFind).should_be_true();
            }
        }

        public static void should_only_contain<T>(this IEnumerable<T> items, IEnumerable<T> itemsToFind)
        {
            itemsToFind.Count().should_be_equal_to(itemsToFind.Count());
            items.should_contain(itemsToFind);
        }

        public static void should_only_contain_in_order<T>(this IEnumerable<T> items, IEnumerable<T> itemsToFind)
        {
            itemsToFind.Count().should_be_equal_to(itemsToFind.Count());
            var results = new List<T>(items);
            var resultsToFind = new List<T>(itemsToFind);
            for (int i = 0; i < itemsToFind.Count(); i++)
            {
                resultsToFind[i].should_be_equal_to(results[i]);
            }
        }

        public static void should_be_true(this bool item)
        {
            item.should_be_equal_to(true);
        }

        public static void should_be_false(this bool item)
        {
            item.should_be_equal_to(false);
        }

        public static void should_be_equal_to<T>(this T actual, T expected)
        {
            Assert.AreEqual(expected, actual);
        }


        public static ExceptionType should_throw_an<ExceptionType>(this Action workToPerform)
            where ExceptionType : Exception
        {
            Exception resulting_exception = get_exception_from_performing(workToPerform);
            resulting_exception.should_not_be_null();
            resulting_exception.should_be_an_instance_of<ExceptionType>();
            return (ExceptionType) resulting_exception;
        }

        public static void should_not_throw_any_exceptions(this Action workToPerform)
        {
            workToPerform();
        }

        public static void should_be_an_instance_of<Type>(this object item)
        {
            Assert.IsInstanceOfType(typeof (Type), item);
        }

        public static void should_not_be_null(this object item)
        {
            Assert.IsNotNull(item);
        }

        public static void should_not_be_null_or_empty(this string item)
        {
            Assert.IsFalse(string.IsNullOrEmpty(item));
        }

        public static void should_be_null_or_empty(this string item)
        {
            Assert.IsTrue(string.IsNullOrEmpty(item));
        }

        public static void should_be_empty<T>(this IEnumerable<T> items)
        {
            Assert.IsTrue(items.Count() == 0);
        }

        public static void should_not_be_empty<T>(this IEnumerable<T> items)
        {
            Assert.IsFalse(items.Count() == 0);
        }

        private static Exception get_exception_from_performing(Action work)
        {
            try
            {
                work();
                return null;
            }
            catch (Exception e)
            {
                return e;
            }
        }
    }
}
Technorati-Tags: ,,

Der Eintrag ist mir etwas Wert
 

Feedback

# re: Extension-Methods für schöneres Unit-Testing

left by Peter Bucher at 12/8/2008 2:04 PM Gravatar
Hier der erste - von dir - konvertierte ;-)
Echt nett und bei ASP.NET MVC natürlich _echt_ Hammer!
Comments have been closed on this topic.