15.02.2010

Unit-Tests für Seam Anwendungen

Eine grössere Seam-Anwendung ist entstanden und muss getestet werden. Eingesetzt wurden: JBoss AppServer, Seam , Facelets, Hibernate, JBPM und Drools.

Für die wichtigsten Komponenten waren bereits Unit Tests entstanden. Das Problem hierbei: Die Tests stellen natürlich nur sicher, dass das Verhalten unter der definierten Fixture korrekt ist. Immer wieder jedoch wurden Komponenten nicht mit den erwarteten Daten versorgt und es kam zu Fehlern, die vorab durch die Unittests nicht detektiert wurden. Klar, müssen eben mehr Unittests her - wir haben uns nur gefragt, ob das auch effizienter geht und "automatisch" die Unittests geschrieben werden könnten, die für die Anwendung auch wirklich relevant sind.

Nächster Anlauf: Integrationstests der gesamten Anwendung - mit Selenium. Jetzt wird definitiv das Systemverhalten abgeprüft. Aber - die Tests sind abhängig vom Zustand einer Testdatenbank - ok, die kann man ja vor dem Test einspielen, dauert nur ein paar Minuten. Ganz schnell waren jedoch ein paar hundert Testfälle aufgezeichnet und die Suite lief acht Stunden und mehr. Definitiv wiederum nicht mehr als Unittest während der Entwicklung geeignet. Als ein grösseres Problem stellte sch jedoch das Management der asserts heraus. Wir hatten uns entschieden, Datenbank-Inhalte zu vergleichen. Keine gute Idee: Kleine Änderung im Code und hunderte von Dateien mit erwarteten Daten sind zu korrigieren. Bessere wäre, man hätte Zugriff auf die Ergebnis-Objekte und könnte modularen assert-Code schreiben.

Also: Wie kann eine komplette Seam-Anwendung in integrierter Form getestet werden, jedoch ohne Zugriff auf die Datenbank (Performance), dafür mit direktem Zugriff auf die Objekte (wg. asserts)?

Komponenten-Tests

Lesen hilft - in diesem Fall das Seam Handbuch. Die Klasse SeamTest mit ihrer inneren Klasse FacesRequest stellt eine Seam-Umgebung zur Verfügung, die für Unittests geeignet ist:
public class RegisterTest extends SeamTest
{

   @Test
   public void testRegister() throws Exception
   {
      new FacesRequest() {
         @Override
         protected void updateModelValues() throws Exception
         {
            setValue("#{user.username}", "1ovthafew");
            setValue("#{user.name}", "Gavin King");
            setValue("#{user.password}", "secret");
         }

         @Override
         protected void invokeApplication()
         {
            assert invokeMethod("#{register.register}").equals("success");
         }

         @Override
         protected void renderResponse()
         {
            assert getValue("#{user.username}").equals("1ovthafew");
            assert getValue("#{user.name}").equals("Gavin King");
            assert getValue("#{user.password}").equals("secret");
         }
      }.run();
   }
   ...

}

Für jede JSF Phase können die erforderlichen Aktionen implementiert werden:

  • Update Model Values: Hier werden typischerweise mittels setValue und einem dem facelet entnommenen EL-Ausdruck Benutzereingaben simuliert. Dies können nicht nur - wie im Beispiel - Strings sein, sondern komplette Objekte, z.B. im Fall von Auswahlboxen.
  • Invoke Application: Mittels invokeMethod und einem EL-Ausdruck wird die Aktion aufgerufen. Aktionen mit Parameter funktionieren so allerdings nicht - unten mehr dazu.
  • Render Response: Das ist die richtige Stelle für asserts - hier jedoch nicht umständlich im erzeugten HTML, sondern direkt per getValue und EL-Ausdruck im Objektgraphen.
Da wir mit Maven entwickeln, haben wir den Test brav unter src/test/java abgelegt und mit mvn test ausgeführt - mit Surefire unterstützt Maven TestNG ja optimal, sehr schick.

Mock Komponenten

Nur leider: Beim ersten Ausführen der Tests kam es zu einem Problem: Die Komponente zum Zugriff auf die Datenbank war nicht korrekt konfiguriert. Hier hat es sich als grosser Vorteil herausgestellt, dass wir eine Komponente haben, durch die alle DB-Zugriffe gehen (sie implementiert sie dann mittels Hibernate): Wir wollen ja gerade keine Datenbank-Zugriffe, sondern alle Objekte unmittelbar als Mocks zur Verfügung stellen. Das ist jetzt einfach: Wir erstellen eine Mock-Version der Zugriffs-Komponente, also eine Klasse, die das gleiche Interface implementiert, mit dem gleichen Namen versehen und mit 
@Install(precedence=MOCK)
annotiert ist. Lassen wir den Test erneut laufen - keine Änderung! Nun, erneut haben wir die Mock-Komponente brav unter srv/test/java abgelegt. Im Classpath ist sie zwar, jedoch sucht Seam nur dort nach Komponenten, wo die i.a. leere seam.properties-Datei liegt. Die darf also in src/test/resources nicht fehlen - nun wird tatsächlich die Mock-Komponente statt der eigentlichen verwendet!

(Die Bemerkung des Handbuches übrigens, man könnte die Mock-Komponente ruhig nach Produktion deployen, dort würde die MOCK-precendence nicht installiert, ist zumindest unter Seam 2.0.2 nicht korrekt - besser in src/test/java lassen!)

Unsere Zugriffs-Komponente kriegt nun eine Liste mit Objekten und eine register-Methode, und die getter-Methoden geben Objekte aus der Liste zurück - schon können wir der Anwendung beliebige Objekte aus der Fixture des Tests unterschieben.

Steht keine zentrale Komponente zur Verfügung, muss man für die HibernateSessionFactory selbst einen Mock bereit stellen - mehr Schreibaufwand, aber nach dem gleichen Prinzip machbar.

Goodies

Mit dem FacesRequest von Seam lassen sich keine Action-Methoden mit Parameter per EL aufrufen. Wir haben eine Basisklasse angeleitet und dort folgende Methode implementiert:

protected void invokeMethod(String method, Object... params)
    {
      Class[] types = new Class[params.length];

      for (int i = 0; i < params.length; i++) {
        types[i] = params[i].getClass();
      }
      Expressions.MethodExpression<Object> m = 
          Expressions.instance().createMethodExpression(
              method, Object.class, types);
      m.invoke(params);
    }

Jetzt kann mit der neuen invokeMethod-Methode auch eine Methode mit Parametern aufgerufen werden.

Pageflows sind offenbar ein Problem: Da sie Pages orchestrieren, die im Seam-Test so aber nicht verwendet werden, können sie auch nicht getestet werden.

Ebenso nested conversations: Wir müssen sie in den Tests explizit starten, wenn in der Anwendung nicht die Methode, sondern der Link oder der Pageflow-Knoten mit der Anweisung für die nested conversation versehen ist:

public class BookProductRequest extends PlosamRequest
  {

    @Override
    protected void updateModelValues() throws Exception
    {
      Manager.instance().beginNestedConversation();
    }

    @Override
    protected void invokeApplication() throws Exception
    {
        ...
    }
  }