Thursday, December 19, 2013

Der C#-Compiler und Extensions auf anonymen Lambdas

Im folgenden Artikel zeige ich ein einfaches Idiom das dem C#-Compiler ermöglicht auch Erweiterungen auf anonymen Lambdas zu verstehen. So lassen sich UnitTests kompakter und verständlicher schreiben.

Beim Schreiben von UnitTests nach dem Muster Arrange-Act-Assert (AAA) sind die wunderbaren FluentAssertions immer wieder sehr hilfreich den Assert-Schritt leserlicher und verständlicher zu formulieren. Typischerweise sind dabei für alle möglichen Typen sogenannte Extension Methods definiert, sodass sich z.B. ganz komfortabel schreiben lässt

string actual = "ABCDEFGHI";
actual.Should().StartWith("AB").
  And.EndWith("HI").
  And.Contain("EF").
  And.HaveLength(9);

Unter all den vielen hilfreichen Erweiterungsmethoden gibt es auch welche für die vordefinierten Delegat-Typen Action und Func. So lässt sich z.B. schreiben

Action act = () => subject.Foo(null)); 
act.ShouldThrow<ArgumentNullException>()
   .WithMessage(“?did*”, ComparisonMode.Wildcard);

Func<IEnumerable<char>> func = () => obj.SomeMethodThatUsesYield("blah");
func.Enumerating().ShouldThrow<ArgumentException>();

Die ShouldThrow-Erweiterung ist hilfreich wenn man z.B. sicherstellen will dass in Konstruktoren sog. guard clauses vorhanden sodass so früh wie möglich auf die falsche Nutzung einer Klasse aufmerksam gemacht wird. Traditionellerweise wird das in vielen Frameworks mit Expected Exceptions gelöst.

[Test, ExpectedException(typeof(ArgumentNullException))]
public void Ctor_called_with_name_null_should_throw()
{
   new MyClass(null);
}

Gibt es dabei mehrere Bedingungen zu testen, muss man mit diesem Konstrukt auch mehrere Tests schreiben. Das entspricht der allgmeinen Richtlinie nach dem Tests immer nur eine einzige Bedingung überprüfen sollen. Bei derart einfachen sog. state-based Tests weiche ich allerdings gerne schon einmal davon ab. Dann würde ich gerne schreiben

[Test]
public void Ctor_called_with_name_null_empty_or_whitespace_should_throw()
{
   (() => new MyClass(null)).
     ShouldThrow<ArgumentNullException>();
   (() => new MyClass(String.Empty)).
     ShouldThrow<ArgumentException>();
   (() => new MyClass("\t\r\n")).
     ShouldThrow<ArgumentException>();
}


Leider akzeptiert der Compiler den Aufruf der ShouldThrow-Erweiterung auf den anonymen, d.h. inline-definierten Lambdas so nicht. Das liegt daran, dass er zu diesem Zeitpunkt nicht eindeutig auf den Typ schliessen kann. Man kann dem Compiler dann durch einen expliziten Cast helfen.

  ((Action)() => new MyClass(null)).ShouldThrow...

Das liest sich allerdings unschön. Schöner ist doch den Cast implizit in einem Methoden-Aufruf verschwinden zu lassen

[Test]
public void Ctor_called_with_name_null_empty_or_whitespace_should_throw()
{
   Calling(() => new MyClass(null)).
     ShouldThrow<ArgumentNullException>();
   Calling(() => new MyClass(String.Empty)).
     ShouldThrow<ArgumentException>();
   Calling(() => new MyClass("\t\r\n")).
     ShouldThrow<ArgumentException>();
}

static Action Calling(Action action) { return action; }

Durch die Namensgebung Calling ist der Test sogar noch verständlicher geworden. Ich habe dieses Konstrukt mittlerweile so oft verwendet dass ich das Gefühl habe es könnte Wiederverwendung finden. Wenn es dem einen oder anderen hilft würde ich mich freuen.

No comments:

Post a Comment