Montag, 9. Mai 2011

Der Fluxkompensator und seine Bedeutung für den Software Test

Liebe Leser!

Jeder, der den Film "Zurück in die Zukunft" mit einem großartigen Michael J. Fox in der Hauptrolle gesehen hat, kennt auch die Basiserfindung für die Durchführung jeglicher Zeitreise, den
Fluxkompensator.

Aus [wikipedia], 08.05.2011:


"Der Fluxkompensator ist das Kernstück der 
Zeitmaschine, der nach Browns Aussage „Reisen durch die Zeit erst möglich macht“. Er besteht aus einem grauen, rechteckigen Kasten mit drei länglichen, blinkenden Glasröhrchen, die hinter einer Glasscheibe eben im 120°-Winkel zueinander angeordnet sind. Der Fluxkompensator ist an der Rückwand zwischen den beiden Sitzen im Fahrzeuginneren angebracht. Brown hat ihn am 5. November 1955 erfunden, als er beim Aufhängen einer Uhr in seinem Badezimmer von der Schüssel seiner Toilette abrutschte und unsanft mit dem Kopf aufschlug."



Nun - obwohl der ein oder andere von uns sicher auch schon mal unsanft mit dem Kopf auf diverse Nassraumeinrichtungsbestandteile geprallt ist (nein?), hat das - wie ich vermute - keine revolutionären Ideen zum Thema Zeitreisen ausgelöst (bei mir jedenfalls nicht).

Das Problem

Als ich jedoch ein "Mehrrunden-Mehrgüter" Auktionssystem als Scrummaster und Backend Architekt verantworten durfte, kam ich in die Verlegenheit, folgendes Problem zu lösen:
  • Eine Auktion startet mit der Katalogphase, in dieser werden zu versteigernde Güter im Katalog von den Eigentümern registriert - diese Phase dauert 2 Tage.
  • Nach der Katalogphase beginnt die Auktion, wobei diese in maximal 20 Runden zu je 2 Minuten 40 Sekunden Bietzeit und 20 Sekunden Berechnungszeit gegliedert ist.
  • Gibt es in einer Runde kein neues Gebot auf zumindest ein Gut in der Versteigerung, ist die Auktion beendet und die Höchstbieter erwerben jeweils die Güter.

Die Entscheidung


Ich traf nun - entgegen einigen Widerstands aus dem Team (was ich nicht gerne mache, aber manchmal muss man sich eben durchsetzen) - folgende weitreichende Designentscheidung:

Die einzelnen Auktionsstati (Katalogphase, Rundenphase, Berechnungsphase) werden aufgrund der aktuellen Uhrzeit in Bezug auf den Start der Katalogphase bestimmt (z.B. Start Katalogphase + 2 Tage = Start Auktionsphase). Der Vorteil hierbei ist, dass der Status nicht persistent gespeichert werden muss. Somit kann der aktuelle Auktionsstatus - aufgrund fehlerhafter bzw. vergessener Statusübergänge (z.B. Systemabsturz während eines Übergangs) - nie falsch sein und man spart sich einen Scheduler, der zum richtigen Zeitpunkt den Status "eins weiter" setzt (z.B. auf die nächste Runde). Dieser Scheduler könnte auch nur schwer auf die Millisekunde genau arbeiten und damit könnte ein Bieter auch kurz nach Beendigung der Runde bieten - für einen Architektur-Ästheten eine unerträgliche Einschränkung.

Soweit - so gut. Wie testet man so eine Applikation aber in der Praxis? Wenn ich Güter registriere, will ich nicht zwei Tage warten, bis ich darauf bieten kann. Selbst wenn ich die Zeitdauer der Katalogphase per Konfiguration für den manuellen Test auf 20 Sekunden setze, muss ich mich beeilen, um alle Güter rechtzeitig zu registrieren, ganz zu schweigen davon, dass z.B. ein Telefonat mit dem Chef einen Neustart des Tests bedingt (außer man kann nebenbei Güter erfassen, was sicher auch auf die Stimmung des Chefs ankommt). Beim Unit Test stellt sich dasselbe Problem - wie kann ich deterministisch testen? Wenn z.B. der Build Server während eines Tests mit 100% CPU Load gesegnet ist, könnte es sein, dass in Runde x gar kein Gebot einlangt, und somit die Auktion beendet ist. Non-Determinismus ist aber hier ein absolutes No-Go.

Aufgrund dieser Problematik dachte das Team wohl, dass ich die Design-Entscheidung zurücknehmen würde. Ich aber habe meine grauen Zellen angestrengt und kam zu folgender Idee: Wir brauchen einen Fluxkompensator! (Team: staunende Augen, Kopfschütteln). Sowohl im manuellen als auch automatischen Test will ich - gleich nach der Durchführung einer Aktion wie z.B. einer Gebotsabgabe - in die Zukunft reisen können, um zur richtigen Zeit meine nächste Aktion zu setzen und ohne unnütz Zeit mit Warten verbringen zu müssen. Die explizite Kontrolle der Zeit bringt den gewünschten Determinismus! Soviel zur Idee - fehlt nur noch die Umsetzung.

Der Lösungsansatz

Wie sieht der Lösungsansatz nun konkret aus? Software-technisch beginnt alles (wie meistens) mit einem Interface:

package at.coopXarch.flux;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * This service simply provides timestamps.
 * @author jwr
 */
public interface ITimestampProvider {
    /**
     * This formatter is required to achieve the same representation as java.lang.String of java.util.Date and
     * java.sql.Timestamp
     */
    final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    /**
     * Gets a notion of "now".
     * @return Timestamp that represents "now".
     */
    Date getTimestamp();
    /**
     * Notion of a time increment. In real time we would <code>sleep()</code>, but note that in a test bed we can control the sampling of time.
     * @return Timestamp after increasing "current" time with sample value.
     */
    Date sample();
    /**
     * Set the time increment we desire when calling <code>sample()</code>.
     * @param sampleIncrement Increment of time we desire.
     */
    void setSampleIncrement(long sampleIncrement);
    /**
     * Set a specific time as a notion of "now". We cannot do this with real time but in a test bed we can control it.
     * @param sampleValue Value of time representing "now".
     */
    void setSampleValue(long sampleValue);
    /**
     * Reset "now" timestamp to a specific absolute "begin" value. We cannot do this with real time but in a test bed we can control it.
     */
    void reset();
}

Die Implementierung - Variante 1 "Echtzeit"

Die folgende Implementierung liefert Echtzeit - die Zeit wird also, wie üblich, mittels new Date() bestimmt. In der Applikation muss man nun dafür sorgen, dass überall statt new Date() nun getTimestamp() aufgerufen wird. In der Produktion ist also folgende Implementierung des Interfaces zu verwenden:

package at.coopXarch.flux;

import java.util.Date;

/**
 * This service simply provides timestamps.
 * @author jwr
 */
public class TimestampProviderImpl implements ITimestampProvider {
    @Override
    public Date getTimestamp() {
        return new Date();
    }
    @Override
    public Date sample() {
        throw new UnsupportedOperationException("You cannot do this in real time!");
    }
    @Override
    public void setSampleIncrement(long sampleIncrement) {
        throw new UnsupportedOperationException("You cannot do this in real time!");
    }
    @Override
    public void setSampleValue(long sampleValue) {
        throw new UnsupportedOperationException("You cannot do this in real time!");
    }
    @Override
    public void reset() {
        throw new UnsupportedOperationException("You cannot do this in real time!");
    }
}

Die Implementierung - Variante 2 "Fluxzeit"

Die folgende Implementierung des Interfaces steuert jetzt den Fluxkompensator bei - sie wird also im manuellen und automatisierten Test verwendet:

package at.coopXarch.flux;

import java.util.Calendar;
import java.util.Date;

/**
 * This class acts as a FluxKompensator. Do not tell Albert Einstein!
 * @author jwr
 */
public class FluxKompensator implements ITimestampProvider {
    private final Calendar cal;
    private Date initialTimestamp;
    private static final long DEFAULT_SAMPLE_INCREMENT = 500;
    private long sampleIncrement = DEFAULT_SAMPLE_INCREMENT;
    private Date dateHolder = null;

    public FluxKompensator() {
     synchronized (this.getClass()) {
      cal = Calendar.getInstance();
         // D-Day: e.g. 31.03.11 10:10
         cal.clear();
         cal.set(2011, 2, 31, 10, 10, 0);
         initialTimestamp = cal.getTime();
     }
    }
    @Override
    public synchronized Date getTimestamp() {
        if (dateHolder == null) {
            sample();
        }
        System.out.println("getSampledTimestamp()=" + dateFormat.format(dateHolder));
        return new Date(dateHolder.getTime());
    }
    @Override
    public synchronized  Date sample() {
        final Date previous = dateHolder;
        final Date now = (previous != null) ? new Date(previous.getTime() + sampleIncrement) : initialTimestamp;
        dateHolder = now;
        System.out.println("sample()=" + dateFormat.format(now));
        return new Date(now.getTime());
    }
    @Override
    public void setSampleIncrement(long sampleIncrement) {
        this.sampleIncrement = sampleIncrement;
        System.out.println("setSampleIncrement() to " + (sampleIncrement / 1000) + "s");
    }
    @Override
    public synchronized void setSampleValue(long sampleValue) {
        final Date now = new Date(sampleValue);
        System.out.println("setSampleValue()=" + dateFormat.format(now));
        dateHolder = now;
    }
    @Override
    public synchronized void reset() {
        cal.clear();
        cal.set(2011, 2, 31, 10, 10, 0);
        initialTimestamp = cal.getTime();
        sampleIncrement = DEFAULT_SAMPLE_INCREMENT;
        dateHolder = null;
    }
}

Voila! Nun kann man in der Testumgebung mittels setSampleValue() jeden beliebigen Zeitpunkt setzen - während des manuellen Tests sogar im User Interface (siehe unten)! Und beliebig lange auf einem konkreten Zeitpunkt verweilen (ist schön im Log zu sehen, alle Aktionen haben auf die Millisekunde denselben Timestamp). Auch im Unit Test haben wir die Zeit vollkommen unter Kontrolle und man kann sie beliebig vorwärts und auch rückwärts setzen (Achtung: rückwärts fließende Zeit kommt im realen Zeitfluss nur sehr SELTEN vor und kann zu Risiken und Nebenwirkungen führen!).

Im GUI sieht das folgendermaßen aus:


Besonders die Option "Zeit vergehen lassen" finde ich besonders nett - dahinter steht natürlich ein Aufruf von sample().

Fazit

Mit diesem Ansatz haben wir alle Tests im gegenständlichen Projekt ohne Probleme durchgeführt und das Produkt erfolgreich ausgeliefert. Ich finde den Lösungsansatz kreativ und leistungsfähig, bin aber natürlich auf Eure Meinung gespannt.

Und zum Schluss der Dialog zwischen Marty McFly und Dr. Emmett L. „dem Doc“ Brown nach meiner absoluten Lieblingsszene zu Beginn des Films:
Marty: "Hören sie, Doc. Die Geräte waren die ganze Woche eingeschaltet."
Doc:
"Meine Geräte? Da fällt mir ein Marty. Schalte lieber nicht den Verstärker ein. Es könnte die Möglichkeit bestehen, daß er zuviel Spannung kriegt."
Insider wissen um die Vorgeschichte (Gitarre, Box, Explosion) und schicken mir bitte einen YouTube Link dazu. Ich habe nur den, wo Marty "Johnny B. Goode" spielt (auch nicht zu verachten: http://www.youtube.com/watch?v=d4Cr7kxjSBs)

Mit diesen Ausführungen verbleibe ich
Euer
JWR@coopXarch.