Official End of the RowTest Extension

Standard

I published my RowTest extension for NUnit in 2007. I developed it to experiment
with the NUnit extension mechanism which was new in NUnit 2.4. It got some users
and the extension was included in NUnit 2.4.8. NUnit 2.5 had the new Test Case feature
which was built on the experience gained by the RowTest extension.

To switch from RowTest to TestCase you just have to replace the RowTest attribute by the TestCase attribute.

Since the RowTest extension isn’t needed anymore I stopped its development long time ago. I just didn’t publish an official statement about its end. I will remove the RowTest project page on my blog, but the project pages on Google Code and Launchpad will still be available:

Von Rot zu Grün – Green Bar Patterns

Standard

Ich wende testgetriebene Entwicklung bereits seit über 10 Jahren an. Kurz zur Erinnerung: Bei TDD (Test-Driven Development) erfolgt die Implementierung in vielen kleinen Iterationen, die aus drei Teilen bestehen:

  • Schreibe einen Test für das erwünschte Verhalten. Der Programmcode erfüllt diesen Test noch nicht oder es gibt ihn noch gar nicht. Manche Testrunner stellen diesen Zustand durch einen roten Balken dar. Daher wird diese Phase oft mit Red bezeichnet.
  • Ändere oder schreibe den Programmcode mit dem kleinstmöglichen Aufwand, damit er den Test erfüllt. Manche Testrunner stellen diesen Zustand durch einen grünen Balken dar. Daher wird diese Phase oft mit Green bezeichnet.
  • Refaktorisiere den Code. Entferne Code-Duplizierung, führe Abstraktionen ein, wenn nötig, usw. Danach führe die Tests nochmal aus. Der Code muss die Tests noch immer erfüllen. Diese Phase wird oft mit Refactor bezeichnet.

Ich habe am Beginn viel über TDD gelesen. Meistens waren es Artikel im Internet. Es gab noch nicht viele Bücher über dieses Thema. Eine Ausnahme war Kent Becks Test-Driven Development. Es ist ein vergleichsweise dünnes Buch, das ich sehr schnell gelesen hatte.

Vor kurzem habe ich das Buch wieder gelesen und dabei wurden mir einige Details wieder bewusst. Dazu gehören die sogenannten “Green Bar Patterns”. Diese Patterns beschreiben, wie wir den Programmcode ändern können, damit er die Tests erfüllt:

  • Triangulation
  • Fake It (‘Til You Make It)
  • Offensichtliche Implementierung

Triangulation

Bei der Triangulation verallgemeinern wir den Code erst, wenn wir zwei oder mehr Tests geschrieben haben. Daraus ergibt sich folgender Ablauf:

  • Zuerst schreiben wir einen Test.
  • Dann machen wir die kleinstmögliche Änderung am Programmcode, damit er diesen Test erfüllt, z.B. geben wir nur den erwarteten Wert zurück.
  • Dann schreiben wir noch einen Test.
  • Erst jetzt abstrahieren wir die Implementierung.

Nehmen wir an, dass wir eine Funktion schreiben wollen, die zwei Zahlen addiert. Der erste Test könnte so aussehen:

    [Test]
    public void Sum_3plus4_Returns7()
    {
      Assert.That(Plus(3, 4), Is.EqualTo(7));
    }

Dann würde diese Implementierung ausreichen, um den Test zu erfüllen:

    public int Plus(int a, int b)
    {
      return 7;
    }

Um den Code verallgemeinern zu können, benötigen wir einen weiteren Test:

    [Test]
    public void Sum_9plus15_Returns24()
    {
      Assert.That(Plus(9, 15), Is.EqualTo(24));
    }

Jetzt können wir die allgemeine Implementierung schreiben:

    public int Plus(int a, int b)
    {
      return a + b;
    }

Beide Tests müssen zu diesem Zeitpunkt funktionieren.

Triangulation wird gerne verwendet, weil die Regeln sehr klar sind. Kent Beck empfiehlt diese Methode nur dann, wenn man sich über die korrekte Implementierung völlig unsicher ist.

Fake It (‘Til You Make It)

Auch bei “Fake It” machen wir nach dem ersten Test nur die kleinstmögliche Änderung am Programmcode, um den einen Test zu erfüllen. Im Unterschied zur Triangulation schreiben wir kein zweites Testbeispiel, sondern verallgemeinern den Code im Refaktorisierungsschritt.

  • Zuerst schreiben wir einen Test.
  • Dann machen wir die kleinste Änderung am Programmcode, damit dieser den Test erfüllt, z.B. durch Rückgabe des erwarteten Werts.
  • Jetzt abstrahieren wir die Implementierung durch Refaktorisierung.

Der erste Test und die zugehörige Implementierung für das obige Beispiel sieht wieder so aus:

    [Test]
    public void Sum_3plus4_Returns7()
    {
      Assert.That(Plus(3, 4), Is.EqualTo(7));
    }

    public int Plus(int a, int b)
    {
      return 7;
    }

Im Refaktorisierungsschritt eliminieren wir die Code-Duplizierung zwischen Test und Implementierung und ersetzen sie durch den richtigen Code:

    public int Plus(int a, int b)
    {
      return a + b;
    }

Wichtig bei “Fake It” ist, den Refaktorisierungsschritt ernst zu nehmen. Nach meiner Beobachtung wird gerne aufs Refaktorisieren vergessen, obwohl es einen so wichtigen Beitrag zum Software-Design darstellt.

Offensichtliche Implementierung

Bisher bestand die Implementierung nach dem ersten Test daraus, dass wir den erwarteten Rückgabewert als Konstante zurückgeben. Aber müssen wir das tun, wenn doch ganz klar ist, wie die Implementierung aussieht? Vor allem in unserem Beispiel mit der Addition zweier Parameter? Natürlich nicht. Wenn vollkommen klar ist, wie die Implementierung aussehen soll, schreiben wir sie einfach.

In unserem Beispiel sieht das so aus:

    [Test]
    public void Sum_3plus4_Returns7()
    {
      Assert.That(Plus(3, 4), Is.EqualTo(7));
    }

Und dann schreiben wir gleich:

    public int Plus(int a, int b)
    {
      return a + b;
    }

Wann verwendet man welche Methode?

Viele TDD-Neulinge fangen mit der offensichtlichen Implementierung an und verzetteln sich dann, weil sie zu große Schritte machen. Andere tendieren zur Triangulation und wundern sich, warum TDD so mühsam ist.

Tatsächlich haben alle drei Methoden ihre Berechtigung. Wir können mit “Fake It” anfangen und zur offensichtlichen Implementierung wechseln, wenn wir merken, dass der Code sehr einfach ist. Wenn die Tests öfter unbeabsichtigt rot bleiben, dann sind wir zu schnell unterwegs und sollten zu “Fake It” oder Triangulation wechseln.

Oft wissen wir nach längerem Nachdenken noch nicht, wie der Code aussehen soll. Dann werden wir mit der Triangulation beginnen und wenn wir sicherer geworden sind, zu einer schnelleren Methode wechseln.

Wenn der Code Schleifen oder Verzweigungen enthalten soll, dann ist er sehr wahrscheinlich nicht mehr einfach. Offensichtliche Implementierung ist dann offensichtlich die falsche Methode.

Linktipp: Das Pac-Man-Dossier

Standard

Vor kurzem habe ich einen interessanten Link entdeckt: The Pac-Man Dossier. Darin erzählt Jamey Pittman, was er über eines der populärsten Arcade-Spiele, Pac-Man, zusammengetragen hat:

  • die Hintergrundgeschichte von Namco, Pac-Mans Designer Toru Iwatani und der Entwicklung von Pac-Man,
  • eine detaillierte Beschreibung des Gameplays,
  • Details zum Pathfinding,
  • das Verhalten der Ghosts und
  • eine Beschreibung des “Split-Screens” in Level 256.

Pac-Man sollte endlos weiterspielbar sein, d.h. es sollte keine maximale Punkteanzahl geben. Allerdings gibt es einen Fehler im Pac-Man-Code, der dazu führt, dass das Level 256 nicht zu Ende gespielt werden kann. Die Details über diesen Bug und einen Bugfix beschreibt Don Hodges auf seiner Webseite. Dank dieses Fehlers kann man in Pac-Man maximal 3.333.360 Punkte erreichen.

Im Original-Pac-Man-Code gibt es einen weiteren Fehler, der die Pathfinding-Algorithmen der Ghosts Pinky und Inky betrifft. Auch darüber hat Don Hodges auf seiner Webseite eine Fehlerbeschreibung und eine Lösung geschrieben.

Wer vom Z80-Assembler-Code noch nicht genug hat, kann sich ja anschauen, was Alessandro Scotti über die Pac-Man-Hardware herausgefunden hat.

Chad Birch hat in einem Blogpost das unterschiedliche Verhalten der vier Ghosts Blinky, Pinky, Inky und Clyde analysiert. Einfache Regeln führen zu einem anscheinend komplexen Verhalten.

Tyler Neylon hat sich an eine Reimplementierung des Spiels gewagt. Der Anlass dafür war eine Bemerkung von ihm, dass die alten Spiele heute viel einfacher zu implementieren seien, sogar an einem Tag. Er hat es geschafft und Pacpac als Open-Source veröffentlicht.

Vor drei Jahren hat Google anlässlich des 30. Geburtstags von Pac-Man das Spiel als Google-Doodle präsentiert. Zum ersten Mal setzte Google ein in JavaScript geschriebenes interaktives Spiel als Doodle ein. Viele Benutzer wunderten sich, woher die Geräusche aus ihrem Computer kamen. Übrigens, das Doodle lässt sich heute noch spielen.

Key-Value-Store in PostgreSQL

Standard

Vor kurzem habe ich mir die relationale Open-Source-Datenbank PostgreSQL angesehen. Dabei sind mir einige Features aufgefallen, die normalerweise mit NoSQL-Datenbanken in Verbindung gebracht werden. Eines davon ist die hstore-Erweiterung.

Die hstore-Erweiterung

Neuere PostgreSQL-Versionen enthalten die hstore-Erweiterung, die einen neuen Datentyp hstore einführt. Darin können Schlüssel-Wert-Paare gespeichert werden, wobei die Erweiterung nur String als Datentyp für Schlüssel und Wert kennt. Die Daten in dem hstore-Feld müssen keinem felstgelegtem Schema folgen.

Erstellen der Datenbank

Um die Erweiterung zu verwenden, muss sie zunächst in der Datenbank installiert werden.

    CREATE EXTENSION hstore;

Danach steht der neue Datentyp hstore für Tabellen zur Verfügung:

    CREATE TABLE account
    (
      id serial PRIMARY KEY,
      login varchar(40) NOT NULL UNIQUE,
      password varchar(40) NOT NULL,
      info hstore
    );

Das SQL-Kommando INSERT kann neue Daten für die hstore-Spalte einfügen. Die Schlüssel-Wert-Paare werden als String durch Kommas getrennt. Der String kann mit ::hstore in den richtigen Datentyp konvertiert werden:

    INSERT INTO account (login, password, info)
        VALUES ('andreas', 'securepassword', 'location => "Wien", age => 38'::hstore);
    INSERT INTO account (login, password, info)
        VALUES ('hugo', 'othersecurepassword', 'location => "Wien", age => 42, married_with => "daniela"'::hstore);
    INSERT INTO account (login, password, info)
        VALUES ('daniela', 'pqwaösdkjf', 'location => "Linz", married_with => "hugo"'::hstore);
    INSERT INTO account (login, password, info)
        VALUES ('agnes', 'krzlzaf', 'age => 39'::hstore);

Operatoren für den Key-Value-Store

Die hstore-Erweiterung implementiert neue Operatoren, um die Schlüssel-Wert-Paare in SQL-Abfragen zu verwenden. So kann man z.B. mit dem Operator ‘->’ auf die Werte in der hstore-Spalte mit dem Schlüssel zugreifen. Den Operator kann man z.B. in Abfragen verwenden:

    SELECT login, info->'age' as age, info->'married_with' as married_with
        FROM account WHERE info->'location' = 'Wien';

Der Operator kann auch in Joins verwendet werden:

    SELECT a1.login, a1.info->'location' as location,
           a2.login as partner, a2.info->'location' as partnerlocation
      FROM account a1 INNER JOIN account a2
        ON a1.info->'married_with' = a2.login;

Der Operator ‘?’ überprüft, ob ein Schlüssel in der hstore-Spalte verfügbar ist.

    SELECT login FROM account WHERE info ? 'married_with';

Der Operator ‘-‘ entfernt ein Schlüssel-Wert-Paar aus einem hstore:

    UPDATE account SET info = info::hstore - 'married_with'::text
        WHERE info->'married_with'='daniela';
    UPDATE account SET info = info::hstore - 'married_with'::text
        WHERE info->'married_with'='hugo';

Der Operator ‘||’ verkettet zwei hstores:

    UPDATE account SET info = info || 'married_with=>"agnes"' WHERE login='hugo';
    UPDATE account SET info = info || 'married_with=>"hugo", location=>"Wien"'
        WHERE login='agnes';

Bestehende Werte werden dabei geändert:

    UPDATE account SET info = info || 'location=>"Graz"' WHERE login in ('hugo', 'agnes');

Mehr Informationen über die hstore-Erweiterung gibt es in der PostgreSQL-Dokumentation.

Zugriff auf die Datenbank aus C#

Von C# aus kann man mit Npgsql auf eine PostgreSQL-Datenbank zugreifen. Neue Daten kann man dann so in eine Tabelle mit einer hstore-Spalte einfügen:

    var cmdText = "INSERT INTO account (login, password, info) VALUES (:login, :password, :info::hstore)";
    var info = "location => \"Wien\", age => 42, married_with => \"daniela\"";
    using (var command = new NpgsqlCommand(cmdText, connection))
    {
        command.Parameters.Add(new NpgsqlParameter("login", NpgsqlDbType.Varchar, 40));
        command.Parameters.Add(new NpgsqlParameter("password", NpgsqlDbType.Varchar, 40));
        command.Parameters.Add(new NpgsqlParameter("info", NpgsqlDbType.Text));

        command.Parameters[0].Value = "hugo";
        command.Parameters[1].Value = "pqwesdf";
        command.Parameters[2].Value = info;
    
        command.ExecuteNonQuery();
    }

Den Wert für die hstore-Spalte muss man selbst in einen String serialisieren und als Datentyp NpgsqlDbType.Text an das Parameterobjekt übergeben. Im SQL-Kommando muss man den Wert auf den Datentyp hstore casten (“:info::hstore“).

    var cmdText = "SELECT login, password FROM account WHERE data->'location' = :location";
    using (var command = new NpgsqlCommand(cmdText, connection))
    {
        command.Parameters.Add(new NpgsqlParameter("location", NpgsqlDbType.Varchar, 40));
        command.Parameters[0].Value = "Wien";

        using (var reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                var login = reader.GetString(reader.GetOrdinal("login"));
                var password = reader.GetString(reader.GetOrdinal("password"));

                Console.WriteLine("Login: '{0}', Password: '{1}'", login, password);
            }
        }
    }

Natürlich können auch andere Operatoren verwendet werden:

    var cmdText = "SELECT login, password FROM account WHERE NOT data ? 'age'"; 
    using (var command = new NpgsqlCommand(cmdText, connection))
    {
        using (var reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                var login = reader.GetString(reader.GetOrdinal("login"));
                var password = reader.GetString(reader.GetOrdinal("password"));

                Console.WriteLine("Login: '{0}', Password: '{1}'", login, password);
            }
        }
    }

Zugriff mit Python

In Python kann man psycopg2 für den Zugriff auf eine PostgreSQL-Datenbank benutzen:

    import psycopg2

    connection_string = <INSERT CONNECTION STRING>
    conn = psycopg2.connect(connection_string)
    cur = conn.cursor()
    cmdText = 'INSERT INTO account (login, password, info) VALUES (%s, %s, %s::hstore)'
    cur.execute(cmdText, ('andreas', 'qwpoeir', 'location => "Wien", age => 38'))
    cur.execute(cmdText, ('daniela', 'askdjf', 'location => "Wien", married_with => "hugo"'))
    cur.execute(cmdText, ('agnes', 'asdfoi', 'age => 37'))
    cur.execute(cmdText, ('hugo', 'ypxoiu', 'location => "Linz", age => 42, married_with => "daniela"'))
    conn.commit()
    cur.close()
    conn.close()

Zugriff aus Node.js

Für Node.js kann das Modul pg für den Zugriff auf PostgreSQL verwendet werden:

    var pg = require('pg');
    var conString = <INSERT CONNECTION STRING>;

    var client = new pg.Client(conString);
    client.connect();

    var cmdText = "INSERT INTO account (login, password, info) VALUES ($1, $2, $3)";
    client.query(cmdText, ['andreas', 'asdf', 'location=>"Wien", age=>38']);
    client.query(cmdText, ['daniela', 'qwre', 'location=>"Wien", married_with=>"hugo"']);
    client.query(cmdText, ['agnes', 'lkj', 'age=>37']);
    client.query(cmdText, ['hugo', 'ppoiu', 'location=>"Linz", age=>42, married_with=>"daniela"']);

    var query = client.query("SELECT login, password FROM account WHERE info->'location' = $1", ['Wien']);

    query.on('row', function(row) {
      console.log('Login: "%s", Password: "%s"', row.login, row.password);
    });

    query.on('end', function() {
      client.end();
    });

Zusammenfassung

Die hstore-Erweiterung fügt einen neuen Datentyp, neue Operatoren und neue Funktionen zu einer Datenbank hinzu. Damit kann mit der relationalen Open-Source-Datenbank PostgreSQL eine Art Key-Value-Store realisiert werden.

In C# muss man auf die Datentypen achten, wenn man mit Npgsql die Datenbank verwendet. In Python und mit Node.js können die als String serialisierten Informationen direkt als Parameter übergeben werden. Ich würde mir wünschen, dass ich die Daten für die hstore-Spalte als Dictionary übergeben könnte. Leider wird das von den Datenbankbibliotheken (noch) nicht unterstützt.