Kategorie-Archiv: Tests

Reflektion KW 48

Image courtesy of "marcolm" / FreeDigitalPhotos.net

Image courtesy of “marcolm” / FreeDigitalPhotos.net

Die Performance-Verbesserungen haben wir in der vergangenen Woche in sauberen Code umgesetzt und die Aufrufe an den verschiedenen Codestellen angepasst. Dabei zeigte sich wieder einmal, dass Unit Tests hilfreich und sinnvoll sind, da wir durch sie Fehler finden konnten, die ansonsten vermutlich frühestens im Endtest aufgefallen wären.

Bei einem anderen Test stellte sich heraus, dass der Algorithmus mit den Performance-Verbesserungen “richtiger” rechnete als die vorherige Implementation. Der Fehler hatte zwar keine Auswirkungen, da die Geräte den Wert nicht auswerten sollten, könnte aber bei einem Fehler auf der Geräteseite zu unerwartetem Verhalten führen.

Die 9 unbeliebtesten Entdeckungen in fremdem Code

Image courtesy of "David Castillo Dominici" / FreeDigitalPhotos.net

Image courtesy of “David Castillo Dominici” / FreeDigitalPhotos.net


Sehr wahrscheinlich hat jeder, der sich beruflich mit Software-Entwicklung befasst, schon einmal fremden Code übernehmen und weiterentwickeln und/oder darin Fehler beheben müssen. Und die Chance ist gross, dass der übernommene Code nicht dem entsprochen hat, was man von gut wartbarem Code erwartet.

Hier ist meine persönliche und subjektive Liste der unbeliebtesten Entdeckungen in fremden Code. Mit dieser Liste will ich niemanden angreifen oder blossstellen. Und auch ich habe schon die eine oder andere Sünde begangen, die hier aufgelistet ist. Ich möchte mich deshalb an dieser Stelle bei allen meinen Nachfolgern entschuldigen, die solchen Code von mir geerbt haben.

Hier nun aber die Liste:

  1. Riesige Dateien
    Wenn eine Datei mehrere Tausend Zeilen Code enthält wurde zu viel in diese Datei gepackt. Wenn die Datei dann nur eine Klasse enthält ist entsprechend auch die Klasse zu gross. Meist enthält die Klasse dann zu viel Know-How, d.H. sie behandelt mehrere Aspekte, anstatt sich auf ihre Kernkompetenz zu konzentrieren.
    Ob die Datei nun eine oder mehrere Klassen enthält, das Resultat ist schlussendlich das selbe: die Übersicht geht verloren und das Verhalten ist dementsprechend schwierig nachzuvollziehen.
    Generierter Code ist von dieser Regel ausgenommen.
  2. Mehrere Klassen in einer Datei
    Rang 9 ist manchmal das Resultat von diesem Vergehen. Die beiden können aber auch getrennt auftreten.
    Ich gehe gerne, wenn mir eine Klasse mehrmals aufgefallen ist, über den Solution Explorer in Visual Studio zu der entsprechenden Klasse. Wenn die gesuchte Klasse aber zusammen mit anderen Klassen in einer Datei liegt ist die Chance gross, dass die Datei nicht den passenden Namen hat und die Klasse deshalb nicht (einfach) auffindbar ist.
    Generierter Code ist, wie schon bei Rang 9, von dieser Regel ausgenommen.
  3. Viele Warnungen
    Warnungen sind da, um mögliche Probleme aufzuzeigen. Das können z.B. unbenutzte Variablen oder nicht erreichbare Code-Abschnitte sein, aber auch veraltete (obsolete) Funktionen oder fehlende XML-Kommentare. Einzelne Warnungen können normalerweise sehr einfach behoben oder kurzfristig auch mal ignoriert werden, wenn z.B. dieser Codeabschnitt gerade überarbeitet wird. Wenn die Warnung langfristig ignoriert werden soll kann dies mit einer Compilerdirektive (pragma) angegeben werden.
    Wenn die Warnungen aber im Rudel auftreten kann nicht mehr unterschieden werden, welche Warnung nun wichtig und welche unwichtig ist bzw. welche Warnung schon seit langem auftritt und welche neu hinzugekommen ist. Wenn man Code vom Vorgänger übernommen hat kann man auch nicht abschätzen, welche Warnungen ein mögliches Problem aufzeigen und welche Warnungen ignoriert werden können.
  4. Irreführende Formatierungen
    Wahrscheinlich ist schon jedem ein Fundstück wie das folgende unter die Augen gekommen:

    1
    2
    3
    4
    if(doCheck())
      doSomething();
      doAnotherThing();
    doAThirdThing();

    Auf den ersten Blick könnte man anhand der Formatierung meinen, dass die if-Abfrage die nächsten beiden Funktionen beeinflusst. Dem Compiler ist aber die Formatierung meistens (Python ist da z.B. eine Ausnahme) egal. Viel Spass beim Fehler suchen!

  5. Undokumentierte Workarounds
    Manchmal muss man weniger schöne Lösungen benutzen, um Fehler in 3rd Party Libraries zu umgehen. Oder man findet einfach keine bessere Lösung und die Deadline rückt näher. Für diese Situationen habe ich Verständnis. Aber bitte nicht irgendwo im Code versteckt, so dass man beim Fehler suchen an dieser Stelle sucht, da der Code hier so “schräg” aussieht (Dieser Code sieht so falsch aus, der Fehler muss hier liegen…).
    Wenn ein Workaround eingesetzt werden muss darf der Kommentar nicht fehlen, dass es ein Workaround ist. Und aus welchem Grund er nötig ist. Im Idealfall mit einem Verweis auf den Bugreport im Bugtracker der 3rd Party Library. So kann der Workaround entfernt werden, wenn eine neue Version der Library verwendet wird, in welcher der Bug behoben ist und der Workaround nicht mehr nötig ist.
  6. Versteckte “globale” Variablen
    Code-Fundstücke wie das folgende liebe ich:

    1
    2
    3
    4
    5
    if(doCheck())
    {
        DataBaseAccess dba = new DataBaseAccess();
        dba.CurrentDB = "myDataBase";
    }

    Mein erster Impuls ist hier: “Das Objekt dba fällt gleich wieder aus dem Scope, dieser ganze Block kann entfernt werden.”
    Beim Untersuchen der Klasse DataBaseAccess wird dann die Aufgabe dieses Code-Blockes sichtbar:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class DataBaseAccess
    {
        private static string currentDB = "";

        public string CurrentDB
        {
            get { return currentDB; }
            set { currentDB = value; }
        }
    }

    DataBaseAccess hat eine static Membervariable, auf die mit einem non-static Property zugegriffen wird. Obwohl das Objekt aus dem Scope fällt bleibt deshalb der Wert weiterhin gesetzt. Für einen Anwender der Klasse sind anhand der Schnittstelle weder die Funktionalität des ersten Code-Blocks noch die damit verbundenen Seiteneffekte ersichtlich. Wenn schon static Membervariablen verwendet werden müssen sollte auch der Zugriff auf diese über static Properties erfolgen.

  7. Irreführende Methoden- und Properties-Bezeichnungen
    Wenn z.B. eine Methode den Namen SetReportMode() hat, dann aber noch fünf andere Dinge gleichzeitig erledigt – oder im schlimmsten Fall nur die fünf anderen Dinge erledigt aber den Report Mode gar nicht einschaltet – sind die Probleme im wahrsten Sinn des Wortes vorprogrammiert.
  8. Business-Logik (nur) im User Interface
    Da wurde ein schönes Schichtenmodell aufgestellt, aber die Business-Logik befindet sich zur Hälfte oder komplett im User Interface Layer und nicht im Business Layer. Zum einen sucht man dadurch Fehler oder die Ansatzpunkte für Erweiterungen am “falschen” Ort, zum andern kann auch das User Interface nicht einfach gewechselt werden (z.B. Windows Forms durch WPF oder ASP.NET ersetzen), wie es mit einem sauberen Schichtenmodell möglich wäre. Wenn die Business Logik dann noch verteilt ist dürften auch viele Business Regeln mehrfach implementiert sein, vermutlich sogar mit kleinen Unterschieden (sprich Inkonsistenzen).
  9. Keine Unit Tests
    Diese Entdeckung tritt meist in Gesellschaft von vorher aufgeführten Entdeckungen auf. In diese Kategorie gehört auch die Variante “als Unit Test getarnter Spike“, bei der erste Versuche an einer Technologie mit Hilfe des Test Runners gemacht werden. Auch andere “Missbräuche” des Test Runners gehören in diese Kategorie. Diese zeichnen sich dadurch aus, dass die keine Asserts in den Testmethoden haben.
    Alle Varianten haben aber eines gemeinsam: Es fehlt die Absicherung, um Refactorings vornehmen zu können ohne Angst haben zu müssen, etwas kaputt zu machen. Und wenn die Unit Tests fehlen ist normalerweise der Code auch nicht so geschrieben, dass Unit Tests einfach hinzugefügt werden können. Dadurch muss der Code zuerst umgebaut werden, dabei fehlt aber wieder die Unterstützung der Unit Tests… Und um die vorher aufgeführten Entdeckungen beheben zu können wären die Unit Tests auch eine willkommene Absicherung.

Wie schon oben gesagt ist dies meine subjektive Top 9. Was sind Eure schlimmsten Entdeckungen in fremdem Code oder wie würde Eure Reihenfolge aussehen?

IList Dojo Retrospektive

Retrospektive

Image courtesy of loop_oh/


In diesem Artikel führe ich eine Nachbetrachtung zum IList Dojo durch.

Nachdem ich meine fünfteilige Artikelreihe (Links zu den einzelnen Teile am Ende dieses Artikels) beendet hatte, habe ich die Lösung von Stefan Lieser angeschaut. Seine darin beschriebenen Erfahrungen habe ich mit meinen verglichen und seine Umsetzung meiner Umsetzung gegenübergestellt. Dabei geht es mir nicht hauptsächlich darum, ob meine Lösung richtig oder falsch bzw. besser oder schlechter ist. Ich möchte vielmehr betrachten, ob es andere Herangehensweisen und andere Lösungsansätze gibt und weitere Schlüsse aus dem Dojo ziehen können.

Dojo-Start

Der Solution-Aufbau beinhaltet besteht bei beiden aus einem Implementations- und einem Test-Projekt. Auch Stefan Lieser hat sich dafür entschieden, die Test-first-Vorgehensweise anzuwenden. Er verwendet aber nicht Visual T# sondern C# um die Tests zu implementieren. Zudem hat er noch NCrunch eingesetzt, ein Tool das die Tests automatisch im Hintergrund ausführt. Das Resultat wird dabei direkt in Visual Studio im Source Code Editor auf der jeweiligen Zeile angezeigt. Sicher ein Tool, das sich lohnt einmal genauer anzuschauen.

Die erste Methode

Bei den ersten Methoden sind wir auch ähnlich vorgegangen. Wir haben uns beide entschieden, mit Tests zu Count und Add() zu beginnen, um erste Resultate zu haben. Auch den internen Datencontainer haben wir in diesem Schritt eingeführt.

Hier trennen sich unsere Wege

Als nächstes hat sich Stefan Lieser an den Enumerator heran gewagt, den ich erst in Teil 4 implementiert hatte. Ich hatte mich als nächstes auf den Indexer konzentriert, welcher dafür von Stefan Lieser erst zum Schluss implementiert wurde.

Dementsprechend sind dann auch in den nächsten Schritten unterschiedliche Methoden angepackt worden. Stefan Lieser hat auch noch Themen angedacht, die ich nicht bedacht hatte, wie z.B. Performance-Betrachtungen.

Kreuzverhör

Doch wie sieht das Resultat aus?

Machen wir den Kreuztest: Meine Tests führen die Bibliothek von Stefan Lieser aus und seine Tests benutzen meine Library. Im Idealfall würden beide Testsuiten keine Fehler melden, alle Tests wären grün und wir könnten zufrieden Feierabend machen.

Aber wie so oft entspricht die Realität nicht dem Idealfall. In diesem Fall heisst das, dass meine Library in Stefan Liesers Testsuite 6 Fehler verursacht (von insgesamt 31 Tests) und seine Bibliothek in meiner Testumgebung 11 Fehler provoziert (bei einem Total von 39 Tests).

Doch was sind die Gründe für diese Fehler. Haben wir Fehlerfälle vergessen, interpretieren wir die Vorgaben des Interfaces IList<T> anders oder gibt es andere Gründe?

Untersuchung 1: Meine Bibliothek

Meine Library verursacht zwei fehlschlagende Tests, da ich beim Aufruf von RemoveAt() mit einem Index ausserhalb der erlaubten Werte eine ArgumentOutOfRangeException werfe. Die Testfälle erwarten aber eine IndexOutOfRangeException. Laut der Dokumentation von Microsoft soll aber die ArgumentOutOfRangeException geworfen werden, auch wenn die IndexOutOfRangeException wohl passender wäre.

Ein Fehler tritt auf, da ich diesen Test vergessen hatte: Löschen eines Elementes aus der Liste mittels Remove() wenn die Liste noch leer ist. Dieser Aufruf warf bei mir bei der Suche in IndexOf() wegen einer vergessenen Überprüfung eine Exception, statt wie erwartet false zurückzugeben. Dieser Fehler hatte auch zur Folge, dass zwei weitere Tests fehlschlugen, die ebenfalls eine leere Liste verwendeten.

Der letzte Test schlägt fehl, da Stefan Lieser die Methode CopyTo() anders interpretiert als ich. Der zweite Parameter dieser Funktion gibt laut Dokumentation von Microsoft an, ab welcher Position im Array die Daten eingefüllt werden sollen (“The zero-based index in array at which copying begins.”). Stefan Lieser hat diesen Parameter so interpretiert, dass dies der Startindex in der LinkedList ist und die Werte vorher ignoriert werden sollen. Hier eine Grafik, die den Unterschied darstellen soll:
CopyTo
Ein kurzer Test des Verhaltens von List<T>‘s CopyTo() zeigt das von mir beschriebene Verhalten, dementsprechend entspricht aus meiner Sicht das Verhalten von Stefan Liesers Implementation nicht den Anforderungen des Interfaces. Aber die Dokumentation von IList<T> dürfte in diesem Bereich noch klarer sein, besonders da der Parametername Interpretationen zulässt. Bei der Dokumentation von List<T>’s CopyTo() hat es ein Beispiel, welches das Verhalten ebenfalls aufzeigt.

Untersuchung 2: Stefan Liesers Bibliothek

Bei meinen Tests, losgelassen auf Stefan Liesers Bibliothek, kommen die beiden Diskrepanzen auch wieder zum Vorschein. Die unterschiedlichen Exceptions sind für zwei fehlgeschlagene Tests verantwortlich. Ebenfalls für zwei fehlgeschlagene Tests ist die unterschiedliche Interpretation bei der CopyTo()-Methode verantwortlich.
Die restlichen 7 Tests schlagen Aufgrund fehlender Überprüfung der Parameter fehl. Dies betrifft den Indexer, die Insert()- und die CopyTo()-Methode, wobei bei der letzten ein Test von mir auch nicht optimal ist, da ich Nichts (eine leere Liste) Nirgends (null als array) hin kopieren will.

Fazit

Diese Nachbetrachtung fand ich lehr- und aufschlussreich. Zum einen habe ich in meinem Code noch Fehler entdeckt und dadurch gesehen, dass mir trotz Test-first-Vorgehensweise mögliche Fehlerfälle durch die Lappen gingen. Zum andern wurde mir wieder einmal aufgezeigt, dass die Schnittstellenbeschreibungen unterschiedlich aufgefasst werden können und am besten mit einer öffentlich zugänglichen Beispielimplementation gefestigt werden. Dies trifft natürlich entsprechend auch für APIs und Protokolle zu.

Trotz identischem Start haben sich die Wege getrennt und es sind teilweise unterschiedliche Resultate herausgekommen. Grosse Unterschiede bei der Herangehensweise oder bei den Lösungsansätzen sind mir nicht aufgefallen. Bei den Details gab es aber Unterschiede, die im Einsatz der Bibliothek dann auch zu Problemen führen könnten.

Artikelreihe “LinkedList Dojo mit Visual T#”

LinkedList Dojo mit Visual T# – Teil 5

Karate

Image courtesy of “luigi diamanti” / FreeDigitalPhotos.net


Von den am Anfang des LinkedList Dojo Teil 4 aufgeführten noch fehlenden Implementationen haben wir erst die GetEnumerator()-Funktionen abgehakt. Packen wir nun die noch fehlenden Funktionen und Properties an. Den in Teil 4 implementierten Enumerator wollen wir nun auch nutzen, um einige von diesen Funktionen zu schreiben.

Die Kopie ist besser als das Original

Nehmen wir uns der CopyTo-Methode an. Als erstes die Tests (ich erlaube mir, gleich zwei Tests zu erstellen).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
test CopyTo(int[], int)  
{
    sut.Add(5);
    sut.Add(8);
    int[] res = new int[2];
    runtest sut.CopyTo(res, 0);

    assert res[0] == 5;
    assert res[1] == 8;
}
       
test CopyTo(int[], int)  
{
    sut.Add(5);
    sut.Add(8);
           
    int[] res = new int[6];
    runtest sut.CopyTo(res, 4);

    assert res[4] == 5;
    assert res[5] == 8;
}

Dank dem Enumerator ist die Implemetation der Funktion fast schon geschenkt:

1
2
3
4
5
6
7
public void CopyTo(T[] array, int arrayIndex)
{
    foreach (T item in this)
    {
        array[arrayIndex++] = item;
    }
}

Und noch ein paar Test für die Fehlerfälle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
test CopyTo(int[], int)  
{
    runtest sut.CopyTo(null, 0);

    assert thrown ArgumentNullException;
}
       
test CopyTo(int[], int)  
{
    int[] res = new int[5];
    runtest sut.CopyTo(res, -1);

    assert thrown ArgumentOutOfRangeException;
}
               
test CopyTo(int[], int)  
{
    sut.Add(5);
    sut.Add(8);
           
    int[] res = new int[5];
    runtest sut.CopyTo(res, 4);

    assert thrown ArgumentException;
}

Und die dazugehörige Implementation mit den Überprüfungen:

1
2
3
4
5
6
7
8
9
10
public void CopyTo(T[] array, int arrayIndex)
{
    if (array == null) throw new ArgumentNullException("array");
    if (arrayIndex  array.Length) throw new ArgumentException("The number of elements in the source collection is greater than the available space from arrayIndex to the end of the destination array.");

    foreach (T item in this)
    {
        array[arrayIndex++] = item;
    }
}

Ich möchte da noch etwas hinzufügen

Unter anderem fehlt noch die Funktion Insert. Auch hierzu zuerst wieder die Tests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(1, 4);

    assert sut[1] == 4;
    assert sut[2] == 8;
}  
       
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(0, 4);

    assert sut[0] == 4;
    assert sut[1] == 5;
}  
       
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(2, 4);

    assert sut[1] == 8;
    assert sut[2] == 4;
}

Auch hier wieder die Implementation der Funktion, welche die Tests erfüllt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void Insert(int index, T item)
{
    if (index == Count)
    {
        Add(item);
        return;
    }

    LinkedListElement element = new LinkedListElement();
    element.Element = item;

    if(index == 0)
    {
        element.Next = first;
        first = element;
        return;
    }

    LinkedListElement priorIndex = GetAt(index - 1);
    element.Next = priorIndex.Next;
    priorIndex.Next = element;
}

Wiederum benötigen wir noch Tests für die Fehlerfälle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(-1, 4);

    assert thrown ArgumentOutOfRangeException;
}
       
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(3, 4);

    assert thrown ArgumentOutOfRangeException;
}

Diese führen dann zu der folgenden Implementation der Insert-Funktion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void Insert(int index, T item)
{
    if (index  Count) throw new ArgumentOutOfRangeException();

    if (index == Count)
    {
        Add(item);
        return;
    }

    LinkedListElement element = new LinkedListElement();
    element.Element = item;

    if(index == 0)
    {
        element.Next = first;
        first = element;
        return;
    }

    LinkedListElement priorIndex = GetAt(index - 1);
    element.Next = priorIndex.Next;
    priorIndex.Next = element;
}

Weg damit

Für die RemoveAt-Funktion implementiere gleich alle Tests auf einmal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.RemoveAt(0);

    assert sut.Count == 1;
    assert sut[0] == 8;
}
             
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.RemoveAt(1);

    assert sut.Count == 1;
    assert sut[0] == 5;
}      
         
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
    sut.Add(4);
           
    runtest sut.RemoveAt(1);

    assert sut[0] == 5;
    assert sut[1] == 4;
}
       
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.RemoveAt(2);

    assert thrown ArgumentOutOfRangeException;
}      
       
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.RemoveAt(-1);

    assert thrown ArgumentOutOfRangeException;
}

Dann schaue ich, dass alle Tests grün werden.

1
2
3
4
5
6
7
8
9
10
11
12
public void RemoveAt(int index)
{
    if (index = Count) throw new ArgumentOutOfRangeException();
    if (index == 0)
    {
        first = first.Next;
        return;
    }

    LinkedListElement priorIndex = GetAt(index - 1);
    priorIndex.Next = priorIndex.Next.Next;
}

Und wenn wir schon am entfernen sind packen wir die Remove-Funktion auch noch an. Wie gewohnt zuerst die Tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
test Remove(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest bool res = sut.Remove(5);

    assert res;
    assert sut.Count == 1;
    assert sut[0] == 8;
}
         
test Remove(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest bool res = sut.Remove(8);

    assert res;
    assert sut.Count == 1;
    assert sut[0] == 5;
}
       
test Remove(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest bool res = sut.Remove(3);

    assert res == false;
    assert sut.Count == 2;
    assert sut[0] == 5;
    assert sut[1] == 8;
}
       
test Remove(int)  
{
    sut.Add(5);
    sut.Add(8);
    sut.Add(5);
                       
    runtest bool res = sut.Remove(5);

    assert res;
    assert sut.Count == 2;
    assert sut[0] == 8;
    assert sut[1] == 5;
}

Der letzte Test überprüft dabei, dass das erste Auftreten eines Elementes entfernt wird. In der dazu passenden Implementation können wir von bereits bestehenden Funktionen profitieren:

1
2
3
4
5
6
7
8
public bool Remove(T item)
{
    int pos = IndexOf(item);
    if (pos &lt; 0) return false;

    RemoveAt(pos);
    return true;
}

Bitte nicht berühren!

Für das letzte Property kann man sicherlich darüber streiten, ob ein Test nötig ist. Aber der Vollständigkeit halber auch hier zuerst der Test.

1
2
3
4
5
6
test IsReadOnly get
{
    runtest bool res = sut.IsReadOnly;
           
    assert res == false;
}

Und das implementierte Property IsReadOnly:

1
2
3
4
public bool IsReadOnly
{
    get { return false; }
}

Somit ist die Klasse LinkedList fertig implementiert. Doch bevor wir uns wieder dem Tagesgeschäft zuwenden wollen wir noch einen Blick zurück werfen und schauen, was wir gelernt haben und wo noch Potential für Verbesserungen besteht.

Retrospektive

Als erstes stellt sich die Frage: Haben wir das Ziel erreicht?
Das dürfen wir mit gutem Gewissen mit ja beantworten, wir haben eine funktionstüchtige Klasse implementiert, die durch Tests abgesichert ist.

Was aber noch wichtiger ist: Was haben wir auf dem Weg zum Ziel gelernt?
Der Test-First-Ansatz hat mir gut gefallen, auch wenn ich mich manchmal zurückhalten musste, gleich mit der Implementation vorzupreschen, ohne den Test geschrieben zu haben. Einige wenige male habe ich schon eine zusätzliche Überprüfung eingebaut, bevor ich diese durch einen Test nötig gemacht hatte. Aber wenn es sich um eine einzelne Überprüfung handelt und man den Test gleich nachliefert ist das aus meiner Sicht legitim. Wir wollen ja nicht päpstlicher als der Papst sein…
Was den Einsatz von Visual T# für die Tests anbelangt bin ich nicht so begeistert. Einige Sachen gefallen mir (z.B. die klare Trennung zwischen Vorbereitung, Ausführung und Überprüfung), aber ich frage mich, ob sich der Aufwand lohnt, eine neue Sprache dafür zu lernen. Mit Unit Tests in C# kann man die Tests ebenfalls schreiben und man muss dann nicht andauernd zwischen den beiden Sprachen wechseln. Vielleicht habe ich mich aber auch noch zu wenig mit Visual T# auseinander gesetzt und es verbergen sich noch interessante Konzepte darin, welche die Arbeit mit Unit-Tests vereinfachen oder verbessern. Was aber an Visual T# noch verbessert werden muss ist die Stabilität. Es ist mir einige Male abgestürzt und manchmal hat es unerklärliche Kompilierfehler gemeldet, die mit einem Neustart des Visual Studios behoben werden konnten.
Den Aufwand, die Klasse komplett zu implementieren, hatte ich etwas unterschätzt. Anhand der Properties und Methoden, die IList verlangt, habe ich die Dauer in etwa geschätzt, dabei aber nicht beachtet, dass ich noch einen Enumerator implementieren muss. Dass ich die Blog-Artikel parallel dazu geschrieben habe hat einen zusätzlichen Sprachwechsel erfordert, so dass ich immer zwischen C#, T# und Deutsch wechseln musste. Hier hat mir der Test-First-Ansatz geholfen, bei den einzelnen Schritten fokussiert zu bleiben und nicht in einer Sprache zu weit vorzupreschen und die anderen Sprachen dann mühsam (und fehleranfällig) “nachzuziehen”.
Das einfache Beispiel erlaubt es auch, einmal ein grobes Verhältnis zwischen Test-Code und Produktiv-Code zu schätzen. Sowohl bei der LinkedList als auch beim LinkedListEnumerator ist der Test-Code etwas doppelt so lang wie der Produktiv-Code. Das Verhältnis der Anzahl Test-Methoden gegenüber der Anzahl Properties und Methoden in den beiden Klassen ist dabei noch grösser (circa 2,5 mal so viele Test-Methoden wie produktive Methoden und Properties). Kurzfristig denkende Manager werden sich bei solchen Zahlen vermutlich die Haare raufen, wenn zwei drittel des Codes nicht im produktiven System verwendet wird. Wenn aber dadurch die Fehler, die den Kunden bei seiner Arbeit mit unserer Software behindern, verhindert oder zumindest stark reduziert werden können, ist dieser “unproduktive” Code schlussendlich ebenso wertvoll wie der Produktiv-Code.

Den entwickelten Code der LinkedList will ich niemandem Vorenthalten. Was für einen produktiven Code noch fehlt sind die XML-Kommentare.

Die Beispiel-Lösung von Stephan Lieser ist in der Juni-Ausgabe erschienen.

Fazit
Test-First-Prinzip erfolgreich angewandt und als gut befunden, die Unit Tests werde ich aber wohl weiterhin mit C# schreiben.

Hat euch dieses Dojo gefallen? Habt ihr ähnliche Erfahrungen mit Visual T# gemacht? Gerne erwarte ich eure Kommentare.

LinkedList Dojo mit Visual T# – Teil 4

Karate

Image courtesy of “luigi diamanti” / FreeDigitalPhotos.net


Im vierten Teil von “LinkedList Dojo mit Visual T#” nehmen wir uns den im Teil 3 noch fehlenden Implementierungen an. Folgende Methoden werfen immer noch die NotImplementedException:

  • Insert(int index, T item)
  • RemoveAt(int index)
  • CopyTo(T[] array, int arrayIndex)
  • IsReadOnly
  • Remove(T item)
  • GetEnumerator() in den Varianten “Generics” und “Non-Generics”

Wagen wir und gleich an den wahrscheinlich grössten Brocken, die zwei GetEnumerator()-Methoden. Die Implementierung der Methoden wird dabei nicht das schwierige und aufwändige sein, sonder der Enumerator, der zurückgegeben wird.

Erstellen wir also eine neue Klasse für den Enumerator und im Testprojekt die zugehörigen Tests.

Als erstes die zwei Tests, die den Enumerator verlangen:

1
2
3
4
5
6
7
8
9
10
11
12
13
test GetEnumerator()
{
    runtest System.Collections.Generic.IEnumerator enumerator = sut.GetEnumerator();
           
    assert enumerator != null;
}
               
test GetEnumerator()
{
    runtest System.Collections.IEnumerator enumerator = ((System.Collections.IEnumerable)sut).GetEnumerator();
           
    assert enumerator != null;
}

Für den nicht generischen Enumerator muss unsere Liste auf das nicht generische Interface gecastet werden, bevor die Funktion aufgerufen werden kann.

Um die Test grün werden zu lassen benötigen wir nun die Implementation des Enumerators. Ich habe mich dabei entschieden, eine eigene Klasse auf der selben Ebene wie die Liste zu erstellen und keine verschachtelte Klasse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace RolandBaer.LinkedList
{
    public class LinkedListEnumerator : IEnumerator
    {
        public T Current
        {
            get { throw new NotImplementedException(); }
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        object System.Collections.IEnumerator.Current
        {
            get { throw new NotImplementedException(); }
        }

        public bool MoveNext()
        {
            throw new NotImplementedException();
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }
    }
}

Jetzt noch die beiden GetEnumerator-Methoden implementieren:

1
2
3
4
5
6
7
8
9
public IEnumerator GetEnumerator()
{
    return new LinkedListEnumerator();
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
    return GetEnumerator();
}

Da das generische Interface das nicht-generische beinhaltet kann in der nicht-generischen Methode auf die generische Methode zurückgegriffen werden. So habe ich schon eine Redundanz verhindert.

Durchnummerieren!

Viel sinnvolles können wir aber mit der Implemetation des Enumerators noch nicht anfangen. Schreiben wir deshalb ein paar Tests, die den Enumerator testen.

Als erstes testen wir, ob MoveNext false zurückliefert, wenn wir eine leere Liste haben. Wie wir die Liste übergeben, haben wir noch gar nicht überlegt, aber wenn keine Liste bekannt ist muss sicher false zurückgegeben werden.

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace RolandBaer.LinkedListTests
{
  testclass for LinkedListEnumerator
  {
    test MoveNext()
    {
       LinkedListEnumerator enumerator = new LinkedListEnumerator();
       runtest bool res = enumerator.MoveNext() ;
       
       assert res == false;      
    }
  }
}

Um den Test grün werden zu lassen müssen wir MoveNext anpassen.

1
2
3
4
public bool MoveNext()
{
    return false;
}

Wie gesagt, die Liste muss dem Enumerator auf irgend eine Art bekannt gemacht werden. Ich bevorzuge in solchen Fällen den Konstruktor, da ich so nicht ein zusätzliches Attribut einführen muss. Der Compiler verhindert auch, dass ich vergesse, die Liste anzugeben wenn der Default-Konstruktor fehlt.

Zuerst aber wieder der Test, der das ganze nötig macht.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test MoveNext()
{
   LinkedListEnumerator enumerator = new LinkedListEnumerator(new LinkedList());
   runtest bool res = enumerator.MoveNext() ;
       
   assert res == false;      
}
       
test MoveNext()
{
   LinkedList list = new LinkedList();
   list.Add(5);
   LinkedListEnumerator enumerator = new LinkedListEnumerator(list);
   runtest bool res = enumerator.MoveNext() ;
       
   assert res == true;      
}

Der erste Test ist der schon vorher eingeführte Test, erweitert um den Parameter im Konstruktor. Der zweite Test ist der neue Test, welcher einen Zugriff auf die Liste verlangt. Im LinkedListEnumerator muss der Konstruktor und eine Variable für die Liste hinzugefügt werden und MoveNext wird angepasst:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LinkedListEnumerator : IEnumerator
{
    LinkedList list;

    public LinkedListEnumerator(LinkedList list)
    {
       this.list = list;
    }

    public bool MoveNext()
    {
        return list.Count &gt; 0;
    }

    // (...)
}

GetEnumerator in der LinkedList muss auch noch angepasst werden, wir übergeben dem Konstruktor einfach this und schon kompiliert wieder alles. Und die Test sind auch wieder alle grün.

Den zweiten Test passe ich nun noch an, so dass eine vernünftige Implementation von MoveNext nötig wird.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test MoveNext()
{
    bool res1, res2;
    LinkedList list = new LinkedList();
    list.Add(5);
    LinkedListEnumerator enumerator = new LinkedListEnumerator(list);
    runtest
    {
        res1 = enumerator.MoveNext() ;
        res2 = enumerator.MoveNext() ;
    }
 
    assert res1 == true;      
    assert res2 == false;      
}

Für die Implementierung sind noch ein paar Umbauarbeiten nötig, doch zuerst die neue Version von MoveNext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private LinkedListElement current;

public bool MoveNext()
{
    if(list.Count == 0) return false;

    if (current == null)
    {
        current = list.GetAt(0);
        return true;
    }

    current = current.Next;
    return current != null;
}

Neben der oben gezeigten neuen Membervariable in der Klasse LinkedListEnumerator verschieben wir auch die Klasse LinkedListElement, welche früher eine geschachtelte Klasse war, in den normalen Namespace und deklarieren sie als internal. Die Methode GetAt(int) deklarieren wir auch als internal, so dass der Enumerator sie verwenden kann.

Dank der Unit-Tests gehen wir bei diesen Umbauarbeiten kein Risiko ein, was wir auch jederzeit überprüfen können.

Und ja, ich habe wieder zu viel implementiert, also noch einen allgemeineren Test, um alle Abläufe durchzugehen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test MoveNext()
{
    int count = 0;
    LinkedList list = new LinkedList();
    list.Add(5);
    list.Add(8);
    list.Add(2);
    list.Add(4);
    LinkedListEnumerator enumerator = new LinkedListEnumerator(list);
    runtest
    {
        while(enumerator.MoveNext())
            count++;
    }
       
    assert count == 4;      
}

Faster, Faster!

Den Rest des Enumerators erledigen wir im Schnellzugtempo. Ein paar Tests und nur noch seht wenig zu implementieren.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
test Current get
{
    int res1;
    int res2;
    LinkedList list = new LinkedList();
    list.Add(5);
    list.Add(8);
    list.Add(2);
    list.Add(4);
    LinkedListEnumerator enumerator = new LinkedListEnumerator(list);
    runtest
    {
        enumerator.MoveNext();
        res1 = enumerator.Current;
        enumerator.MoveNext();
        enumerator.MoveNext();
        res2 = enumerator.Current;
    }
       
    assert res1 == 5;      
    assert res2 == 2;      
}

test Current get
{
    int res1;
    int res2;
    LinkedList list = new LinkedList();
    list.Add(5);
    list.Add(8);
    list.Add(2);
    list.Add(4);
    System.Collections.IEnumerator enumerator = new LinkedListEnumerator(list);
    runtest
    {
        enumerator.MoveNext();
        res1 = (int)enumerator.Current;
        enumerator.MoveNext();
        enumerator.MoveNext();
        res2 = (int)enumerator.Current;
    }
     
    assert res1 == 5;      
    assert res2 == 2;      
}
       
test Current get
{
    LinkedList list = new LinkedList();
    LinkedListEnumerator enumerator = new LinkedListEnumerator(list);
    runtest int res = enumerator.Current;
   
    assert thrown InvalidOperationException;      
}
       
test Reset()
{
    int res1;
    int res2;
    LinkedList list = new LinkedList();
    list.Add(5);
    list.Add(8);
    list.Add(2);
    list.Add(4);
    System.Collections.IEnumerator enumerator = new LinkedListEnumerator(list);
    runtest
    {
        enumerator.MoveNext();
        res1 = (int)enumerator.Current;
        enumerator.Reset();
        enumerator.MoveNext();
        res2 = (int)enumerator.Current;
    }
     
    assert res1 == 5;      
    assert res2 == 5;      
}

Und hier die dazugehörigen Implementationen. Für Dispose habe ich keinen Test geschrieben, da Dispose auch nichts macht. Wenn man mit Sicherheit verhindern will, dass noch die NotImplementedException geworfen wird, kann man einen Test schreiben, es würde aber spätestens auffallen, wenn man die Liste ein erstes mal mit foreach verwendet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public T Current
{
    get
    {
        if (current == null) { throw new InvalidOperationException(); }
        return current.Element;
    }
}

object System.Collections.IEnumerator.Current
{
    get
    {
        return Current;
    }
}

public void Dispose()
{
}

public void Reset()
{
    current = null;
}

Die Tests benötigen noch ein Refactoring und der Konstruktor des Enumerators sollte nicht zugänglich sein, da nur die Liste den passenden Enumerator erstellen soll. Nach den Änderungen sieht dann die Testklasse wie folgt aus.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
testclass for LinkedListEnumerator
{
    LinkedList list;
   
    testcontext
    {
        test
        {
            list = new LinkedList ();
            list.Add(5);
            list.Add(8);
            list.Add(2);
            list.Add(4);
            runtest;
        }
    }
       
    test MoveNext()
    {
        System.Collections.Generic.IEnumerator enumerator = new LinkedList().GetEnumerator();
        runtest bool res = enumerator.MoveNext() ;
       
        assert res == false;      
    }
       
    test MoveNext()
    {
        bool res1, res2;
        LinkedList list = new LinkedList();
        list.Add(5);
        System.Collections.Generic.IEnumerator enumerator = list.GetEnumerator();
        runtest
        {
            res1 = enumerator.MoveNext() ;
            res2 = enumerator.MoveNext() ;
        }
       
        assert res1 == true;      
        assert res2 == false;      
    }
       
    test MoveNext()
    {
        int count = 0;
        System.Collections.Generic.IEnumerator enumerator = list.GetEnumerator();
        runtest
        {
            while(enumerator.MoveNext())
                count++;
        }
       
        assert count == 4;      
    }
       
    test Current get
    {
        int res1;
        int res2;
        System.Collections.Generic.IEnumerator enumerator = list.GetEnumerator();
        runtest
        {
            enumerator.MoveNext();
            res1 = enumerator.Current;
            enumerator.MoveNext();
            enumerator.MoveNext();
            res2 = enumerator.Current;
        }
       
        assert res1 == 5;      
        assert res2 == 2;      
    }

    test Current get
    {
        int res1;
        int res2;
        System.Collections.IEnumerator enumerator = list.GetEnumerator();
        runtest
        {
            enumerator.MoveNext();
            res1 = (int)enumerator.Current;
            enumerator.MoveNext();
            enumerator.MoveNext();
            res2 = (int)enumerator.Current;
        }
     
        assert res1 == 5;      
        assert res2 == 2;      
    }
       
    test Current get
    {
        LinkedList list = new LinkedList();
        System.Collections.Generic.IEnumerator enumerator = list.GetEnumerator();
        runtest int res = enumerator.Current;
   
        assert thrown InvalidOperationException;      
    }
       
    test Reset()
    {
        int res1;
        int res2;
        System.Collections.IEnumerator enumerator = list.GetEnumerator();
        runtest
        {
            enumerator.MoveNext();
            res1 = (int)enumerator.Current;
            enumerator.Reset();
            enumerator.MoveNext();
            res2 = (int)enumerator.Current;
        }
     
        assert res1 == 5;      
        assert res2 == 5;      
    }    
}

Den Enumerator haben wir nun, die restlichen Funktionen fehlen aber immer noch. Wir nehmen uns diesen im fünften Teil an.

LinkedList Dojo mit Visual T# – Teil 3

Karate

Image courtesy of “luigi diamanti” / FreeDigitalPhotos.net


Dies ist der dritte Teil von “LinkedList Dojo mit Visual T#”. In dieser Übungsaufgabe implementieren wir eine verknüpfte Liste nach dem Test-First Ansatz mit Hilfe von Visual T#.

Am Ende von Teil 2 habe ich ja bereits angekündigt, dass als Erstes ein Refactoring ansteht. Dabei werden wir nicht nur die LinkedList überarbeiten sondern auch die dazugehörigen Tests (ohne natürlich die Test-Funktionalitäten zu ändern).

Was riecht hier so streng?

Code Smell: Suchschleifen

Im Indexer haben wir zwei mal dieselbe Suchschleife, einmal im get- und einmal im set-Block. Diese Schleife extrahieren wir in eine eigene Methode, die wir dann aufrufen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public T this[int index]
{
    get
    {
        if (index &gt;= Count) throw new ArgumentOutOfRangeException();

        LinkedListElement temp = GetAt(index);

        return temp.Element;
    }
    set
    {
        if (index &gt;= Count) throw new ArgumentOutOfRangeException();

        LinkedListElement temp = GetAt(index);

        temp.Element = value;
    }
}

private LinkedListElement GetAt(int index)
{
    LinkedListElement temp = first;

    for (int i = 0; i &lt; index; i++)
    {
        temp = temp.Next;
    }
    return temp;
}

Wieder alle Tests ausführen und beruhigt sehen, dass immer noch alle Tests funktionieren.

Code Smell: Initialisierung in Tests

In jedem Test wird die Liste neu erzeugt. Dies kann in eine Methode ausgelagert werden, den sogenannten testcontext. Der Codeabschnitt vor dem runtest entspricht dabei dem TestInitialize- bzw. dem SetUp-Attribut, der Codeabschnitt nach dem runtest dem TestCleanup- bzw. dem TearDown-Attribut beim Microsoft Unit Test Tool bzw. bei NUnit.
Das sieht dann wie folgt aus (nur ein Test als Beispiel).

testclass LinkedListTest for LinkedList
{
    LinkedList sut;
       
    testcontext
    {
        test
        {
            sut = new LinkedList ();
            runtest;
        }
    }
       
    test Count get
    {
        runtest int count = sut.Count;

        assert count == 0;
    }
    (...)
}

Die beiden schlimmsten Code Smells sind behoben, aber die zwei Schleifen in Count und Add riechen auch noch ein wenig. Ich erlaube mir aber, die Nase noch ein bisschen zuzuklemmen und noch etwas Funktionalität hinzuzufügen.

Leeren Sie Ihren Kopf

Als erstes möchte ich die Liste leeren können, so dass ich wieder neue Elemente hinzufügen kann.

1
2
3
4
5
6
7
8
9
10
test Clear
{
    sut.Add(5);
    sut.Add(8);
    assert sut.Count == 2;
           
    runtest sut.Clear();

    assert sut.Count == 0;
}

Der assert-Ausdruck vor dem runtest ist eine kleine Zusatzsicherheit, dass die Liste auch Elemente enthält bevor wir sie löschen. Das ist erlaubt und auch vernünftig, da so auch wirklich das getestet wird, was man testen will. Der Test schlägt natürlich fehl, da Clear noch die Standard-Implementation enthält, die eine Exception wirft. Lassen wir den Test grün werden.

1
2
3
4
public void Clear()
{
    first = null;
}

Ganz nach dem Motto nach uns die Sintflut hängen wir einfach das erste Element der verketteten Liste ab und wir sind die Liste los. Der Garbage Collector kann sich dann um die Überreste kümmern.

Haben Sie ..?

Da dies so einfach war fügen wir noch eine Funktionalität mehr hinzu, die Methode Contains(). Wiederum zuerst den Test, der dann fehlschlägt:

1
2
3
4
5
6
7
8
9
test Contains(int)
{
    sut.Add(5);
    sut.Add(8);

    runtest bool res = sut.Contains(5);
           
    assert res;
}

Und die einfachste Implementation, die den Test erfüllt:

1
2
3
4
public bool Contains(T item)
{
    return true;
}

Ein zweiter Test soll den Fall abdecken, dass das Element nicht in der Liste vorhanden ist:

1
2
3
4
5
6
7
8
9
test Contains(int)
{
    sut.Add(5);
    sut.Add(8);

    runtest bool res = sut.Contains(6);
           
    assert res == false;
}

Natürlich kann die Überprüfung auch mit

assert !res;

erfolgen.

Hier die (nun sinnvolle) Implementierung, die den neuen Test erfüllt, ohne die anderen Tests fehlschlagen zu lassen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public bool Contains(T item)
{
    LinkedListElement temp = first;

    if (EqualityComparer.Default.Equals(temp.Element, item))
        return true;
    while (temp.Next != null)
    {
        temp = temp.Next;
        if (EqualityComparer.Default.Equals(temp.Element,item))
            return true;
    }

    return false;
}

Das Konstrukt in der if-Abfrage ist dem Umstand geschuldet, dass unsere LinkedList sowohl mit Value- als auch mit Reference-Typen funktionieren soll. Weiter Infos sind z.B. bei StackOverflow zu finden.

Bei der Implementation dieser Funktion habe ich gemerkt, dass noch nicht beide Randbedingungen überprüft werden. Also fügen wir noch einen zusätzlichen Test hinzu:

1
2
3
4
5
6
7
8
9
test Contains(int)
{
    sut.Add(5);
    sut.Add(8);

    runtest bool res = sut.Contains(8);
           
    assert res;
}

Dieser Test ist von Anfang an grün, wir haben unsere Implementation also richtig gemacht.

Wo finde ich ..?

Verwandt mit Contains() scheint IndexOf() zu sein. Nehmen wir uns diese Implementation auch noch gleich vor. Wie immer zuerst der Test. Wir übernehmen gleich die drei Tests von Contains() und passen sie an:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
test IndexOf(int)
{
    sut.Add(5);
    sut.Add(8);

    runtest int pos = sut.IndexOf(5);
           
    assert pos == 0;
}
       
test IndexOf(int)
{
    sut.Add(5);
    sut.Add(8);

    runtest int pos = sut.IndexOf(6);
           
    assert pos == -1;
}
       
test IndexOf(int)
{
    sut.Add(5);
    sut.Add(8);

    runtest int pos = sut.IndexOf(8);
           
    assert pos == 1;
}

Natürlich schlagen alle drei Tests fehl. Nehmen wir nun auch den Code von Contains() und passen ihn für IndexOf() an:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int IndexOf(T item)
{
    int pos = 0;
    LinkedListElement temp = first;

    if (EqualityComparer.Default.Equals(temp.Element, item))
        return pos;
    while (temp.Next != null)
    {
        temp = temp.Next;
        pos++;
        if (EqualityComparer.Default.Equals(temp.Element, item))
            return pos;
    }

    return -1;
}

Mit minimalen Anpassungen sind wieder alle Tests grün. Aber wir haben wieder einen Code Smell, den wir auch gleich beheben:

1
2
3
4
public bool Contains(T item)
{
    return IndexOf(item) &gt; -1;
}

Contains() überprüft nur noch, ob IndexOf() einen gültigen Index zurück gibt. Und dieses Refactoring hat nichts kaputt gemacht, es sind immer noch alle Tests grün.

Immer noch warten einige Methoden auf ihre Implementation. Diesen werden wir uns im vierten Teil annehmen.

LinkedList Dojo mit Visual T# – Teil 2

Karate

Image courtesy of “luigi diamanti” / FreeDigitalPhotos.net


Dies ist der zweite Teil von “LinkedList Dojo mit Visual T#”. In dieser Übungsaufgabe implementieren wir eine verknüpfte Liste nach dem Test-First Ansatz mit Hilfe von Visual T#.

Greifen Sie ruhig zu

Im ersten Teil haben wir die Funktion Add() und das Property Count implementiert. Nun wollen wir überprüfen, ob die Daten auch richtig abgelegt wurden. Dazu benötigen wir den Index-Operator (manchmal auch Indexer genannt) this[int index].

Als erstes wiederum ein Test, um eine erste Implementation zu erzwingen.

1
2
3
4
5
6
7
8
9
test this[int index] get
{
    LinkedList sut = new LinkedList ();
    sut.Add(5);

    runtest int value = sut[0];

    assert value == 5;
}

Und hier die zum Test passende Implementation.

1
2
3
4
5
6
7
8
9
10
11
public T this[int index]
{
    get
    {
        return first.Element;
    }
    set
    {
        throw new NotImplementedException();
    }
}

Schon beim Schreiben des Codes ist die nächste Frage aufgetaucht: Wie soll sich die Liste verhalten, wenn noch kein Element vorhanden ist oder wenn ein Index verwendet wird, der grösser ist als die Anzahl der Elemente in der Liste?
Die Dokumentation von IList<T> verrät es uns (im Bereich Exceptions):

ArgumentOutOfRangeException:
index is not a valid index in the IList<T>.

Dies hat zwei weitere Tests zur Folge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
test this[int index] get
{
    LinkedList sut = new LinkedList ();

    runtest int value = sut[0];

    assert thrown ArgumentOutOfRangeException;
}
       
test this[int index] get
{
    LinkedList sut = new LinkedList ();
    sut.Add(5);
    sut.Add(8);

    runtest int value = sut[2];

    assert thrown ArgumentOutOfRangeException;
}

Und dazu natürlich noch die passende Implementation, so dass die Tests auch wieder grün werden.

1
2
3
4
5
6
7
8
9
10
public T this[int index]
{
    get
    {
        if (index &gt;= Count) throw new ArgumentOutOfRangeException();
        return first.Element;
    }
    set
    (...)
}

Gezielter Zugriff

Natürlich funktioniert der Indexer bisher nur beim ersten Element richtig. Fügen wir also einen Test hinzu, um den wahlfreien Zugriff zu ermöglichen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test this[int index] get
{
    int value1, value2;  
    LinkedList sut = new LinkedList ();
    for(int i = 0; i = Count) throw new ArgumentOutOfRangeException();

        LinkedListElement temp = first;

        for(int i = 0; i &lt; index; i++)
        {
            temp = temp.Next;
        }

        return temp.Element;
    }
    set
    (...)
}

Fehlt noch die Implementation des Setters, als erstes wieder der Test.

test this[int index] set
{
    int value1, value2;  
    LinkedList sut = new LinkedList ();
    for(int i = 0; i = Count) throw new ArgumentOutOfRangeException();

        LinkedListElement temp = first;

        for (int i = 0; i &lt; index; i++)
        {
            temp = temp.Next;
        }

        temp.Element = value;
    }
}

OK, bei der Implementation habe ich etwas geschummelt, ich habe das Test-First Prinzip missachtet und bereits die Überprüfung, ob der übergebene index gültig ist, eingebaut. Hier noch der Test, um die Überprüfung auch zu rechtfertigen.

 test this[int index] set
{
    LinkedList sut = new LinkedList ();
    sut.Add(5);
    sut.Add(8);
           
    runtest sut[2] = 3;

    assert thrown ArgumentOutOfRangeException;
}

Was mich aber mehr stört als die kleine Prinzip-Verletzung sind die Redundanzen im Code, am offensichtlichsten im zuletzt implementierten Indexer. Deshalb starten wir den dritten Teil mit einem Refactoring der bisher implementieren Funktionalitäten und Tests.

LinkedList Dojo mit Visual T# – Teil 1

Karate

Image courtesy of “luigi diamanti” / FreeDigitalPhotos.net


In der Mai-Ausgabe der dotnetpro war die Dojo-Aufgabe (mehr Infos zu den Dojo-Aufgaben) eine verkettete Liste zu implementieren. Eine Aufgabe, bei der keine Ressourcenzugriffe (Datenbank, Filesystem …) und keine Abhängigkeiten das Testen erschweren. Also ein ideales Tummelfeld, um Unit Tests zu verwenden und dabei Visual T# einzusetzen (Einstieg in Visual T#). Dabei versuche ich nach dem Test-First Prinzip vorzugehen.

Aller Anfang ist leicht

Die Aufgabenstellung beschreibt die Anforderungen und die “Architektur” der Liste schon relativ präzise, so dass ich mir erlaube gleich Visual Studio zu starten, ohne erst eine Skizze zu Papier zu bringen.

Die Visual Studio Solution enthält keine Überraschungen. Als erstes Project die Klassenbibliothek mit der zu erstellenden verketteten Liste (LinkedList) und als zweites das Visual T# Testprojekt.

Als erstes erfüllen wir die Anforderung, dass unsere LinkedList das Interface IList<T> implementiert. Die Methodenrümpfe erstellt uns Visual Studio auf Mausklick, so dass unser Projekt bereit für die ersten Test-Implementationen ist.

Erster Test

Als ersten Test erstellen wir die Überprüfung, dass eine leere Liste beim Abfragen der Anzahl der Elemente null zurück gibt.

1
2
3
4
5
6
7
8
9
10
11
testclass LinkedListTest for LinkedList< int >
{
    test Count get
    {
        LinkedList< int > sut = new LinkedList < int >();

        runtest int count = sut.Count;

        assert count == 0;
    }
}

Die einfachste Implementierung, die diesen Test erfüllt sieht so aus:

1
2
3
4
public int Count
{
    get { return 0; }
}

Noch nicht wirklich eine sinnvolle Implementierung aber aus dem Test-First Ansatz bzw. dem YAGNI-Prinzip richtig.

Ein Fuss vor den Andern

Für den nächsten Schritt müssen wir etwas mehr investieren. Wenn wir ein Element in der Liste haben möchten müssen wir zuerst das Element hinzufügen.

1
2
3
4
5
6
7
8
9
test Count get
{
    LinkedList< int > sut = new LinkedList < int >();
    sut.Add(5);
           
    runtest int count = sut.Count;

    assert count == 1;
}

Da wir noch keine Implementation für Add() haben wirft dieser Test eine NotImplementedException. Fügen wir also die Implementation hinzu und bauen auch das Property Count so um, dass der Test erfogreich ist. Dabei implementieren wir (vielleicht ein bisschen entgegen dem YAGNI- und KISS-Prinzip) auch gleich den internen Container, da dieser ja als Anforderung verlangt wird.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class LinkedList< T > : IList< T >
{
    private class LinkedListElement
    {
        public T Element { get; set; }
        public LinkedListElement Next { get; set; }
    }

(...)

    public void Add(T item)
    {
        first = new LinkedListElement() { Element = item };
    }

    public int Count
    {
        get { if (first == null) return 0; return 1; }
    }

(...)

    private LinkedListElement first;
}

Dieser Code erfüllt die in den Tests definierten Anforderungen, aber da fehlt noch einiges. Machen wir also den nächsten Schritt, noch ein Element hinzufügen und die Anzahl überprüfen.

1
2
3
4
5
6
7
8
9
10
test Count get
{
    LinkedList< int > sut = new LinkedList < int >();
    sut.Add(5);
    sut.Add(8);
                       
    runtest int count = sut.Count;

    assert count == 2;
}

Kleine Anmerkung zu den Tests: Auch wenn es vielleicht den Anschein macht, ich habe keine Tests geändert sondern neue hinzugefügt. Bei T# dürfen mehrere Tests die selbe Signatur (in diesem Fall ‘test Count get’) haben, das Testprojekt enthält jetzt also drei Tests.

Unbegrenztes Wachstum

Nun ist es Zeit, bei Add() und Count eine allgemeine Implementation einzufügen, die für beliebig viele (im Rahmen der Schnittstelle, Count ist z.B. vom Typ int) Elemente funktioniert.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void Add(T item)
{
    if (first == null)
    {
        first = new LinkedListElement() { Element = item };
    }
    else
    {
        LinkedListElement temp = first;
        while(temp.Next != null)
        {
            temp = temp.Next;
        }
        temp.Next = new LinkedListElement { Element = item };
    }
}

public int Count
{
    get
    {
        if (first == null) return 0;

        int count = 1;
        LinkedListElement temp = first;
        while (temp.Next != null)
        {
            temp = temp.Next;
            count++;
        }
        return count;
    }
}

Vertrauen ist gut, Kontrolle ist besser

Mit einem weiteren Test überprüfen wir die allgemeine Gültigkeit der Implementation.

1
2
3
4
5
6
7
8
9
10
11
12
test Count get
{
    LinkedList< int > sut = new LinkedList < int >();
    for(int i = 0; i < 10; i++)
    {
        sut.Add(i);
    }
                 
    runtest int count = sut.Count;

    assert count == 10;
}

Somit haben wir einen Stand erreicht, bei dem wir die Liste befüllen können und die Werte scheinbar auch gespeichert werden. Um dies aber überprüfen zu können müssen wir eine Möglichkeit implementieren, auf die gespeicherten Werte wieder zugreifen zu können. Diese Funktionalität werden wir im zweiten Teil realisieren.

Visual T# (TSharp) – Allererster Schritt

Pretty Objects hat Visual T# in der Version 3.0 veröffentlicht. Zeit, dieses Tool einmal näher anzuschauen.

Die Installation erfolgt über ein Setup und muss hier wohl nicht weiter beschrieben werden.

Ich starte das Ganze mit einem kleinen Vergleich zwischen dem Testing Framework von Microsoft und Visual T# an einem einfachen Beispiel. Dieses Beispiel hatte ich vor einiger Zeit erstellt, um NUnit vorzustellen.

Die zu testende Bibliothek

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Calculator
{
    public class Calc
    {
        public byte Add(byte b1, byte b2)
        {
            int res = b1 + b2;
            if (res > Byte.MaxValue)
            {
                throw new OverflowException();
            }

            return (byte)res;
        }
    }
}

Die Funktion beinhaltet bereits die Überlauf-Absicherung für den zweiten Test, ansonsten nicht viel spannendes an diesem Code…

Erster Test mit Microsoft Test Framework

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace CalculatorMSTest
{
    [TestClass]
    public class CalcTest
    {
        [TestMethod]
        public void AddTest()
        {
            Calc calc = new Calc();

            byte sum = calc.Add(12, 26);

            Assert.AreEqual(38, sum);
        }
    }
}

Auch hier gibt es nicht viel spannendes: Initialisierung (Zeile 9), Ausführung (Zeile 11) und Validierung (Zeile 13) wie wahrscheinlich schon tausend mal gesehen.

Der erste Test mit T#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace CalculatorTSharp
{
  testclass CalculatorTest for Calc
  {
    test Add( byte, byte )
    {
      Calc calc = new Calc();

      runtest byte sum = calc.Add(12, 26);

      assert sum == 38;      
    }
  }
}

Auf den ersten Blick könnte man meinen, es sei auch normaler C#-Code, aber auf den zweiten Blick fallen dann die Unterschiede auf:

  1. testclass anstelle von class (Zeile 4)
    Mit dem Keyword testclass wird angegeben, dass es sich um eine Klasse für Tests handelt.
  2. test als Bezeichner für Testmethode (Zeile 6)
    Das Keyword test bezeichnet eine Testmethode. Mit dem nachfolgenden Add(byte, byte) kann angegeben werden, welche Methode mit diesem Test überprüft werden soll. Dies ist aber optional.
  3. Deklarierung des Stimuli mit runtest (Zeile 10)
    Die Anweisung, mit welcher der Stimuli des Tests ausgelöst wird, wird mit runtest bezeichnet. Somit ist klar ersichtlich, was Initialisierung und was Ausführung der Tests ist.
  4. Überprüfung mit Keyword assert. (Zeile 12)
    Für die Überprüfung gibt es das Keyword assert und nicht ein Objekt wie in den bekannten Testing Frameworks
  5. keine Attribute
    Da T# extra für Tests entwickelt wurde sind keine zusätzlichen Attribute nötig (siehe auch Punkt 1).

Eine etwas andere Syntax aber keine spektakulären Neuerungen. Ein paar eingesparte Attribute, sonst etwa der gleiche Aufwand. Bisher also kein richtiger Grund, eine neue Syntax zu lernen.

Schauen wir und noch einen zweiten Test an:

Zweiter Test mit Microsoft Test Framework

1
2
3
4
5
6
7
        [TestMethod, ExpectedException(typeof(System.OverflowException))]
        public void BigAddTest()
        {
            Calc calc = new Calc();

            calc.Add(150, 230);
        }

Der zweite Test validiert die Überlauf-Prüfung. Die erwartete Ausnahme wird als Attribut (Zeile 1) angegeben.

Der zweite Test mit T#

1
2
3
4
5
6
7
8
    test Add( byte a, byte b)
    {
      Calc calc = new Calc();

      runtest calc.Add(150, 230);

      assert thrown System.OverflowException;      
    }

Der wichtigste Unterschied betrifft die Ausnahme. Bei T# wird das Auftreten der Ausnahme ebenfalls mit einem assert Befehl validiert. Der Test folgt somit dem bekannten Schema Initialisierung (Zeile 3), Ausführung (Zeile 5) und Validierung (Zeile 7).

In diesem Beispiel haben wir aus meiner Sicht einen Mehrwert mit T#: Das Schema wird beibehalten, während bei Tests mit dem Microsoft Test Framework die Validierung in ein Attribut an den Anfang des Tests geschoben wird.

Ausblick:
T# bietet noch weitere Vorteile, die ich in einem weiteren Artikel noch beleuchten möchte.