Ich wurde getestet … und ganz gut gefunden!

Ich habe zufällig auf dem Blog von Gordon Breuer eine Webseite entdeckt, wo man seine Kenntnisse teilweise kostenlos testen kann. Ich habe mir den kostenlosen Test “C# 3.0 Fundamentals” ausgewählt und jetzt kann ich hier ganz stolz das Ergebnis melden:

C# 3.0 Fundamentals

Scored higher than 64% of all previous test takers.

Demonstrates a clear understanding of many advanced concepts within this topic. Appears capable of mentoring others on most projects in this area.

Das ist aber ein super Gefühl 🙂
Hier die Bewertung der Punkte:

How do I interpret my Brainbench test score?

Scores range from 1-5 with 5 being the highest in the Brainbench
scoring system. Within that range, Brainbench recognizes two levels
of achievement for standard assessments. A score between 2.75
– 3.99 earns the test taker the standard certification while
a score of 4.0 or higher certifies a master level of achievement.

Scores range from 1 to 5 and are divided into the
following proficiency level categories:

Test Overall Score
Range
Proficiency
Level
1.00 – 1.50 Novice
1.51 – 2.50 Basic
2.51 – 3.50 Proficient
3.51 – 4.50 Advanced Master
4.51 – 5.00 Expert

Während ich den Test gemacht habe, musste ich allerdings ziemlich oft wegen Resharper fluchen, weil der ja die ganze Arbeit macht, wenn es um sowas geht a = b ?? 0; aber bei Multiple Choice hat man ja kein Resharper 😆  (hallo René …)

SOLID Principles

Im vorherigen Artikel habe ich drei der fünf wichtigsten Prinzipien des OOD (Objektorientiertes Design) benannt, ohne mehr darüber zu schreiben. Das würde ich jetzt gerne nachholen.
Diese Prinzipien wurden von Robert C.Martin (a.k.a. Uncle Bob) unter dem Namen S.O.L.I.D. Principles zusammengefasst:

  • SRP: The Single Responsibility Principle
  • OCP: The Open Closed Principle
  • LSP: The Liskov Substitution Principle
  • DIP: The Dependency Inversion Principle
  • ISP: The Interface Segregation Principle

Das Thema wurde von Uncle Bob in mehreren Blogartikeln, Podcasts und vor allem in seinem Buch sehr ausführlich erklärt. Auch andere Entwickler haben darüber geschrieben, zum Beispiel hat Stefan Lieser darüber eine ganze Artikelserie in dotnetpro veröffentlicht.
Ich habe das erste Mal vor einem Jahr in diesem Podcast von Hanselman und Uncle Bob darüber gehört, und während der letzten 12 Monaten wurde ich durch die tägliche Arbeit überzeugt, dass man mit diesen Regeln solide Anwendungen bauen kann.

Was bedeuten also diese Akronyme:

SRP: The Single Responsibility Principle

Die Definition lautet:

A class should have only one reason to change.

Diese Regel ist wahrscheinlich die einfachste und wird wahrscheinlich am meisten verletzt. Wer kennt nicht Klassen, die sowas tun, wie Daten speichern, manipulieren, E-Mails versenden und all das eventuell auch noch loggen. Das war früher eine ganz normale Vorgehensweise. Was passiert aber, wenn die Datenbank-Struktur sich verändert hat? Dann musste man nicht nur diese ändern sondern auch all die Klassen – meistens Verwaltungen oder Manager genannt – die all diese Verantwortlichkeiten hatten. Und darum geht es hier: eine Klasse darf nur einen Grund für Änderungen haben, sie darf nur eine Verantwortlichkeit haben. Also wenn man mehrere Gründe erkennen kann, warum sich eine Klasse verändert, dann wurde dieses Prinzip verletzt. Und das gilt nicht nur für Klassen, sondern auch für andere Funktionseinheiten wie Funktionen, Klassen, Assemblies, Komponenten, alle auf ihre Abstraktionsebene betrachtet.

OCP: The Open Closed Principle

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Jede Funktionseinheit soll erweiterbar sein, also darf nicht zu viele Abhängigkeiten haben, weil die diese Freiheit stark oder ganz einschränken können. Wenn man allerdings ein verändertes Verhalten implementieren will, soll das nicht durch Veränderung des Codes sondern durch hinzufügen von neuen Funktionen passieren.
Das ist nur durch ausreichende Abstraktion zu erreichen. Wenn die Kernfunktionalität in eine abstrakte Basisklasse gekapselt ist, kann man das neue Verhalten in einer abgeleiteten Klasse implementieren.

LSP: The Liskov Substitution Principle

Dieses Prinzip wurde von Barbara Liskov definiert und es lautet so:

Subtypes must be substitutable for their base types.

Einfach übersetzt: jede abgeleitete Klasse einer Basisklasse muss diese Klasse so implementieren, dass sie durch diese jeder Zeit ersetzbar ist. Jedes Mal, wenn man eine Basisfunktion so implementiert, dass diese was ganz anderes tut, als man grundsätzlich erwarten würde, verletzt man dieses Prinzip. Das berühmteste Beispiel ist das Rechteck und das Quadrat. Auf den ersten Blick meint man, dass ein Quadrat ein spezialisiertes Rechteck ist. Was passiert aber, wenn man die Länge oder die Breite eines Quadrates setzt? Es muss jeweils die andere Eigenschaft auch gesetzt werden, sonst wäre es ja kein Quadrat ;). Das ist aber ein Verhalten, was man bei einem Rechteck niemals erwarten würde. Also würde diese Ableitung das Liskovsche Substitutionsprinzip grob verletzen.

DIP: The Dependency Inversion Principle

Die Definition lautet

1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
2. Abstractions should not depend upon details. Details should depend upon abstractions

Dieses Prinzip ist sehr einfach zu erklären (Beispiel für die Verletzung sieht man ja im vorherigen Artikel): Keine Klasse sollte fremde Klassen instanziieren, sondern diese als Abstraktionen (z.B. Interfaces) in Form eines Parameters bekommen. Das führt dazu, dass die fremde Klasse als Black Box fungieren kann, ihre Veränderungen würden nicht zu Veränderung dieser konkreten Klasse führen.

ISP: The Interface Segregation Principle

Das Prinzip bezieht sich auf “fette” Interfaces:

Clients should not be forced to depend on methods they do not use.

Ein Interface ist der veröffentlichte Kontrakt einer Klasse, eines Moduls. Je mehr Methoden darin registriert wurden, vor allem, wenn sie sehr ähnlich sind oder wenn sie keine selbsterklärende Namen haben, dann ist das eine Zumutung gegenüber der Clients, des Verwenders. Er könnte dazu gezwungen sein, den Code der dahinter stehenden Implementierung anzusehen, was die Erstellung des Interfaces sinnlos macht. Dieses soll ja als Black Box fungieren, nicht als eine Herausforderung für den Entwickler.

Jeder Code ist testbar

Immer wieder steht man vor der Herausforderung, zu einem bestehenden Legacy-Code neue Funktionalitäten hinzu zufügen oder vorhandene Bugs beheben zu müssen.
Ich möchte hier eine Möglichkeit dazu beschreiben, einen Weg den ich u.a. von Michael Feathers gelernt habe.

Nehmen wir an, der Code schaut so aus:

using System.Net.Mail;

namespace LegacyCode
{
public class KompletterWorkflow
{
public void SpeichereDatenUndVersendeMail( int id, string emailaddress, string text, string cc )
{
if( !string.IsNullOrEmpty( text ) )
{
Daten user;
DatenRepository repository = new DatenRepository();
user = new Daten { Id = id, Email = emailaddress };
repository.Save( user );

if( !SmtpRepository.IsValidEmailAddress( emailaddress ) ) return;
IEmailRepository emailrep = new SmtpRepository();
MailMessage message = new MailMessage( "myAddress@xy.com", emailaddress, "Testmail", text );
if( !string.IsNullOrEmpty( cc ) ) message.CC.Add( cc );
emailrep.SendMail( message );
}
}
}
}

Die referenzierten Klassen wären dann sowas wie:

public class DatenRepository
{
public void Save( Daten daten )
{
//Speichert die Daten ab
}
}

public interface IEmailRepository
{
void SendMail( MailMessage message );
}

public class SmtpRepository : IEmailRepository
{
public static bool IsValidEmailAddress( string address )
{
try
{
MailAddress mailAddress = new MailAddress( address );
return true;
}
catch
{
return false;
}
}

public void SendMail( MailMessage message )
{
//Versende die Email
}
}

public class Daten
{
public int Id { get; set; }
public string Email { get; set; }
}

Diese Klasse, so wie sie ist, verletzt jede Menge Prinzipien.
Die wichtigsten sind: The Single Responsibility Principle (SRP) und The Open-Closed Principle (OCP). Außerdem ist die Klasse in diesem Zustand nicht testbar, da man mittendrin Objekte erstellt, deren Verhalten auch mitgetestet werden müssten.
Wenn man das weißt, dann ist die Aufgabe einfach: die Implementierungen DatenRepository und SmtpRepository dürfen nicht in der Methode instanziert werden, sondern sie müssen nach der Regeln der Inversion of Control (IoC) der Klasse übergeben werden.
Zusätzlich werden auch die zusammenhängenden Parameter der Methode SpeichereDatenUndVersendeMail zum userDaten zusammengefasst.

public class KompletterWorkflow
{
private DatenRepository m_repository;
private IEmailRepository m_emailrep;

public KompletterWorkflow(DatenRepository repository, IEmailRepository emailrep)
{
m_repository = repository;
m_emailrep = emailrep;
}

public void SpeichereDatenUndVersendeMail( Daten userDaten, string text, string cc )
{
if( !string.IsNullOrEmpty( text ) )
{
m_repository.Save( userDaten);

if( !SmtpRepository.IsValidEmailAddress( userDaten.Email ) ) return;
MailMessage message = new MailMessage( "myAddress@xy.com", userDaten.Email, "Testmail", text );
if( SmtpRepository.IsValidEmailAddress( cc ) ) message.CC.Add( cc );
m_emailrep.SendMail( message );
}
}
}

Um SRP gerecht zu werden, müsste man auch die Methode in 2 teilen: DatenSpeichern und VersendeMail. Das ist allerdings aus der Sicht der Testbarkeit unwichtig und ich will den Artikel nicht unnötig in die Länge ziehen 😉

Jetzt müssen wir nur noch die Tests schreiben.
Da wir Unittests schreiben, also nur das Verhalten dieser eine Methode testen, müssen wir die fremden Objekte mocken: Das hbedeutet, wir werden ihr Verhalten nachspielen, so tun als ob.

Wir müssen 3 verschiedene Fälle behandeln: ein Interface IEmailRepository, eine konkrete Implementierung DatenRepository und eine statische Methode SmtpRepository.IsValidEmailAddress(string). Bei den ersten zwei empfehlt es sich ein Mocking-Framework wie z.B. RhinoMock zu nutzen.

using NUnit.Framework;
using Rhino.Mocks;
using System.Net.Mail;
namespace LegacyCode.Tests
{
[TestFixture]
public class DatenSpeichernUndVersendeMailTests
{
private IEmailRepository m_emailrep;
private DatenRepository m_repository;

[SetUp]
public void Init()
{
m_emailrep = MockRepository.GenerateStub<IEmailRepository>();
m_repository = MockRepository.GenerateStub<DatenRepository>();
}

[Test]
public void Daten_werden_gespeichert_wenn_Text_nicht_leer()
{
//Arrange
KompletterWorkflow workflow = new KompletterWorkflow( m_repository, m_emailrep );
Daten userDaten = new Daten{Id=1,Email="test@test.de"};

//Act
workflow.SpeichereDatenUndVersendeMail( userDaten , "Emailtext", "cc" );

//Assert
m_repository.AssertWasCalled( a => a.Save( userDaten ) );
m_emailrep.AssertWasCalled( a => a.SendMail( Arg<MailMessage>.Matches( b => b.To[0].Address == userDaten.Email && b.CC.Count == 0 ) ) );
}
}
}

Wenn wir jetzt die Tests mit NUnit ausführen, bekommen wir folgende Fehlermeldung:

LegacyCode.Tests.DatenSpeichernUndVersendeMailTests.Daten_werden_gespeichert_wenn_Text_nicht_leer:
System.InvalidOperationException : No expectations were setup to be verified, ensure that the method call in the action is a virtual (C#) / overridable (VB.Net) method call

Das heißt, die Methoden, worüber wir Annahmen treffen, müssen entweder in einem Interface veröffentlicht worden oder überschreibbar also virtual sein.

public virtual void Save( Daten daten )
{
//Speichert die Daten ab
}

Wenn wir jetzt die Tests durchführen, dann passt alles.

Für die statische Methode erstellt man am besten eine separate Testklasse. Da hier keine andere Komponenten wie Datenbank oder Filesystem angesprochen werden, wird die Methode ganz einfach zu testen sein:

[TestFixture]
public class IsValidEmailTests
{
[Test]
public void Leere_Adresse_ist_nicht_valide()
{
Assert.That( SmtpRepository.IsValidEmailAddress( string.Empty ), Is.False );
}
}

Das war’s !
Mit weiteren Tests kann das Verhalten unserer Klasse komplett abgedeckt und danach mit TDD die Klasse erweitert oder verändert werden.
Und das alles ohne die Befürchtung, dass diese Änderungen zu unvorhersehbaren Ergebnissen führen.