Automatisiert Lokalisierungen von Webapplikationen testen

Schlagwörter

, ,

Aktuell bin ich dabei eine meiner Applikation zu lokalisieren, also Labels gegen Ressource-Bundle Keys austauschen. Das ist mir an einigen Stellen schon um die Ohren geflogen, da aus welchen Gründen auch immer ein genutzter Message-Key noch nicht als Message definiert wurde. Als Ergebnis hatte ich dann z.B. eine Tabellenüberschrift, in der ???retailer.header.name??? oder ???Händlername??? steht.

Nun habe ich die Suche nach diesen Vorkommen endlich automatisiert. Die Grundidee ist letztlich ganz einfach. Man öffnet die HTML-Seiten und sucht dort nach Vorkommen von ???.*???:

private static final Pattern UNREG_MESSAGE_PATTERN = Pattern.compile("[\\?]{3}([^\\?]+)[\\?]{3}", Pattern.MULTILINE);

private static void assertNoUnregKeysInHtml(String pageName, String html) {
  Matcher m = UNREG_MESSAGE_PATTERN.matcher(html);
  List<String> unknownKeys = new LinkedList<String>();
  while (m.find()) {
    unknownKeys.add(m.group(1));
  }
  if (unknownKeys.size() > 0) {
    Assert.fail("In der Seite " + pageName + " scheint es nocht nicht aufgelöste Message-Keys zu geben: " + unknownKeys);
  }
}

Die Überprüfung des HTMLs habe ich in die Unit-Tests meiner PageObjekte gehängt. Ist zwar nicht ganz sauber, da ich nun die Tests meiner PageObjekte mit den inhaltlichen Tests der Seite vermische, da bin ich jetzt aber nicht päpstlicher als der Papst.

Fazit
Wieder einmal eine von den Sachen, wo man einfach mal 5 Minuten nachdenken, dann 1 Stunden machen muss, und sich so tageweise Arbeit erspart. Schade, dass ich erst jetzt drauf gekommen bin. Insbesondere weil ich mit diesem Test noch einen ganzen Haufen Labels gefunden habe, die ich bisher nicht entdeckte (alt-text & Co).
In einem nächsten Test will ich die Testabdeckung der Lokalisierung noch erweitern, und im HTML nach Labels suchen, die noch gar nicht lokalisiert wurden und hart-coded ins HTML kommen. Ich werde berichten.

Advertisements

„Große“ Daten mit RegEx verarbeiten

Schlagwörter

,

Ich bin eine sehr lange Zeit in meinem Entwickler-Leben ohne RegEx ausgekommen. Durch Zufall habe ich mich dieses Jahr doch immer mal wieder damit beschäftigen müssen. Einerseits ist viel Zeit in das Thema reingeflossen, bis ich a) die Syntax halbwegs drauf hatte und b) mit den Java-Klassen zurechtgekommen bin. Inzwischen frage ich mich aber wie ich früher ohne Regex ausgekommen bin. Nichtsdestotrotz stecke ich aber immer noch regelmäßig größere Aufwände in das Thema, wie z.B. heute:

Ausgangssituation ist ein nicht formatiertes File, aus dem Urls ausgelesen und durch eine transformierte Version ausgetauscht werden. Eine Url erkenne ich an einem Tag, das um diese herum geschrieben wird:

<mibutec:url value="ich/bin/eine/url" />

Das konktete Testfile hatte eine Größe von 3,5 MB und ca. 4.000 Urls, die ich auslesen musste. Das ist noch weit weg von Big Data, aber das größte, was ich bisher in RegEx machen musste.

Zuerst müssen wir das Pattern definieren: Hier kann man schon einiges falsch machen.

Das offensichtliche Pattern

<mibutec:url\s+value=".*"\s*/>

wird nicht das geünschte Ergebnis bringen, da RegEx standardmäßig versucht ein Vorkommnis so weit wie möglich zu fassen und in das .* kann der gesamte Inhalt der Datei rein, so dass der gefundene Text vom ersten Auftreten von „<mibutec:url\s+value=“ bis zum letzten Auftreten von \s*/> führen würde, also im .* alle 4.000 Vorkommen meiner Url enthalten wären.

Der zweite Trick, der RegEx-Einsteigern auf die Füsse fallen kann: Wenn der zu verarbeitende String Zeilenumbrüche enthält, dann wird ohne weiteres Zutun RegEx nur die erste Zeile verarbeiten. Um das zu verhindern, muss man dem Pattern-Compiler mitteilen, dass es sich um einen Multiline-String handelt

String patternStr = "(<mibutec:url\\s+value=\")([^\"]*)(\"\\s*/>)";
Pattern pattern = Pattern.compile(patternStr, Pattern.MULTILINE);
Matcher matcher = pattern.matcher(urlString);

Warum wir das Pattern in 3 Groups zerteilt haben, sehen wir gleich noch.

Anschließend müssen wir durch alle Vorkommnisse iterieren und das Tag gegen die transformierte Url austauschen.

while (matcher.find()) {
  String url = matcher.group(2);
  String completeString = "<mibutec:url\\s+value=\"" + url + "\"\\s*/>"
  url = transformer.transformUrl(url);
  urlString = urlString.replaceAll(completeString, url);
}

Das würde schon mal funktionieren, aber Aufgrund der Länge des Strings dauert es ewig, da Stringmanipulationen immer eine neue String-Instanz erzeugen und das bei 3,5 MB Strings mit viel Overhead einhergeht. Als Konsequenz verwenden wir einen Stringbuilder, um die 4.000 Manipulationen vorzunehmen. Bei diesem sieht die Signatur der Replace-Methode aber etwas anders aus.

<code>Stringbuilder.replace(int, int, String);</code>

Die ersten beiden Parameter sind ints, die angeben welche Positionen innerhalb des Stringbuilders ausgetauscht werden, der dritte gibt den String an, der eingesetzt werden soll. Damit ergibt sich etwa folgender Aufbau für das Replacement

String url = matcher.group(2);
url = transformer.transformUrl(url);
int start = matcher.start();
int end = matcher.end();
builder.replace(start, end, url);

Wir müssen allerdings mehrfach im Stringbuilder Replacements vornehmen. Durch das erste Replacement haben sich aber die Positionen der Vorkommnisse verändert. Das bekommt der Matcher aber nicht mit und liefert nun mit start() und end() falsche Positionen innerhalb des Stringbuilders. Diese müssen korrigiert werden.

while (matcher.fund()) {
  String url = matcher.group(2);
  url = transformer.transformUrl(url);
  int start = matcher.start();
  int end = matcher.end();
  builder.replace(start + deltaLength, end + deltaLength, url);
  deltaLength += url.length() - (end - start);
}

Fertig sieht die Methode wie folgt aus:

public static final String transformStringWithUrls(String urlString, IUrlTransformer transformer) {
  String patternStr = "(<testo:url\\s+value=\")([^\"]*)(\"\\s*/>)";
  Pattern pattern = Pattern.compile(patternStr, Pattern.MULTILINE);
  Matcher matcher = pattern.matcher(urlString);
  StringBuilder builder = new StringBuilder(urlString);
  int deltaLength = 0;
  while (matcher.find()) {
    String url = matcher.group(2);
    url = transformer.transformUrl(url);
    int start = matcher.start();
    int end = matcher.end();
    builder.replace(start + deltaLength, end + deltaLength, url);
    deltaLength += url.length() - (end - start);
  }
  return builder.toString();
}

PrePersist mit Spring Data für MongoDB

Schlagwörter

, , ,

Bisher unterstützt Spring Data für MongoDB die @Pre- und Post-Annotations nicht. Insbesondere PrePersist und PreUpdate vermisse ich dabei sehr. Zum Glück gibt es eine Möglichkeit dies selber zu implementieren.

Wenn wir uns anschauen, wie ein Standard-Dao (Repository) in Spring Data aussieht, dann kommen diese Klassen zusammen:

@Repository
public interface EntityHome extends EntityHomeHomeCustom, PagingAndSortingRepository<Entity String> {
  // Hier stehen die Methoden-Definitionen drin, die von Spring Data eigenständig erstellt werden sollen
}

public interface EntityHomeCustom {
  // Hier stehen die Methoden-Definitionen drin, die selbst implementiert werden müssen
}

public class EntityHomeMongoImpl extends implements EntityHomeCustom {
  // Hier stehen die Implemtierungen der Definitionen aus EntityHomeCustom
}

Das Schöne ist, dass man in EntityHomeMongoImpl die von Spring Data selbst generierten Methoden überschreiben kann, u.a. auch die save-Metehode. Das machen wir uns zu Nutze, um die save-Methode um eine Annotation-Behandlung zu erweitern:

public interface EntityHomeCustom {
  public Entity save(Entity entity);
}

public class EntityHomeMongoImpl extends implements EntityHomeCustom {
  protected List<Method> prePersistMethods;

  protected EntityHomeMongoImpl() {
    super();
    this.prePersistMethods = getMethodsAnnotatedWith(c, PrePersist.class);
  }

  static List<Method> getMethodsAnnotatedWith(final Class<?> type, final Class<? extends Annotation> annotation) {
    final List<Method> methods = new ArrayList<Method>();
    Class<?> klass = type;
    while (klass != Object.class) {
      final Method[] allMethods = klass.getDeclaredMethods();
      for (final Method method : allMethods) {
        if (method.getReturnType() == void.class && method.getParameterTypes().length == 0){
          if (method.isAnnotationPresent(annotation)) {
            method.setAccessible(true);
            methods.add(method);
          }
        }
      }
      klass = klass.getSuperclass();
    }
    return methods;
  }

  @Override
  public Entity save(Entity entity) {
    try {
      for (Method prePersistMethod : prePersistMethods) {
        prePersistMethod.invoke(entity, new Object[0]);
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    getMongoOperations().save(entity);
    return entity;
  }
}

Da wir diese Behandlung nicht für jede Entity-Klasse neu schreiben wollen, kann man das ganze noch etwas generalisieren:

public interface GenericHomeCustom<T> {
  public T save(T entity);
}

public class GenericHomeMongoImpl extends implements GenericHomeCustom {
  protected List<Method> prePersistMethods;

  public EntityHomeMongoImpl(Class<T> clazz) {
    super();
    this.prePersistMethods = getMethodsAnnotatedWith(clazz, PrePersist.class);
  }

  @Override
  public T save(T entity) {
    try {
      for (Method prePersistMethod : prePersistMethods) {
        prePersistMethod.invoke(entity, new Object[0]);
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    getMongoOperations().save(entity);
    return entity;
  }
}

Fazit

Mit überschaubarem Aufwand haben wir eine PrePersist-Behandlung implementiert. Auf diese Weise kann auch die anderen Annotations bei Bedarf implementieren. Wenn irgendwann Spring Data den Support bringt, dann kann man den eigenen Code wieder entfernen.

Automatisierte Integrationstests mit MongoDB und Spring Data

Schlagwörter

, ,

Seit Hibernate und HSQL, H2 & Co sind wir Entwickler ziemlich verwöhnt was automatisierte Integrationstests angeht. Einfach die vorhandenen Datenzugriffsklassen und Business-Logik wie gehabt verwenden und Hibernate auf eine In-Memory-Datenbank konfigurieren. Schon können die Tests unabhängig von anderen Systemen und vor allem schnell durchlaufen.

Eine In-Memory-Variante der MongoDB habe ich bisher noch nicht gefunden, daher ist die Lösung nicht ganz so schön wie es Hibernate für relationale Datenbanken anbietet, aber immer noch praktikabel:

Auf meiner Entwicklungsumgebung und meinem CI-System habe ich eine MongoDB-Instanz laufen. Diese hat eine INT-Test-DB, die ich vor jedem Test droppe. Das Anlegen hinterher macht dann die Applikation on the fly.

Die Implementierung ist im Wesentlichen ganz einfach, man muss einfach nur dafür sorgen, dass irgendwann vor der Testausführung folgendes ausgeführt wird:

mongo.dropDatabase("DbName");

Für meine Tests will ich aber nicht manuell das dropDatabase schreiben müssen. Ich will, dass sich die Tests so verhalten, wie ich es von Hibernate + HSQL gewohnt bin: Sobald ich den Kontext neu lade, soll eine neue Instanz der DB erzeugt werden. Erreicht habe ich das über eine Klasse, die als InitializingBean den Zustand meiner DB initialisiert:

public class MongoCleaner implements InitializingBean {
  @Autowired(required=true)
  private MongoTemplate template;

  @Override
  public void afterPropertiesSet() throws Exception {
    template.getDb().dropDatabase();
  }
}

Jetzt muss dieses Stück Code nur noch zur Anwendung gebracht werden

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
  <!-- Default bean name is 'mongo' -->
  <mongo:mongo host="localhost" port="27017">

  <bean id="mongoTemplate">
    <constructor-arg ref="mongo" />
    <constructor-arg value="mibutecInt" />
  </bean>

  <!-- Cleaner sorgt dafür, dass die DB beim Start geleert wird  -->
  <bean id="mongoCleaner" class="de.mibutec.MongoCleaner" />
</beans>

Fazit

Durch diese Methode kann ich auch mit der Mongo relativ unabhängige Integrationstests laufen lassen. Durch das Droppen und Neuanlegen der DB verlängert sich die Laufzeit meiner Tests pro Test um ~0,75 Sekunden. Die Tests selber laufen 3 Sekunden im Schnitt, also ist das schon eine merkliche Verzögerung von 25%.

Ich nehme das als Herausforderung mehr Unit-Tests und weniger Integrationstests zu schreiben, würde mich aber freuen, wenn bald jemand eine In-Memory-Variante der MongoDB in Angriff nehmen würde.

Verwendung von Cookies als Sessionstore

Schlagwörter

, , ,

Mein aktuelles Projekt ist sehr content-lastig. Soll heißen es geht mehr darum redaktionellen Content aus dem CMS anzuzeigen bzw. Produktdaten aus dem PIM anzuzeigen als um Interaktion mit dem User. Tatsächlich gibt es so wenig Interaktion mit dem User, dass wir bisher komplett stateless laufen und auf ein Session-Handling verzichten konnten. Die Server laufen ohne zentralen Sessionstore und ohne Session-Stickyness.

In Zukunft soll das ggf. anders werden: Abhängig vom Erfolg der Seite sollen auch noch Ecommerce-Funktionen dazu kommen. Wenn das passiert, werden wir uns auch über das Session-Handling Gedanken machen müssen, das ist aber erst mal aufgeschoben. Wie so oft, hält sich das Leben wieder mal nicht an die Spielregeln, und so kommen mit dem aktuellen Sprint zwei Anforderungen, die einen State in der Applikation benötigen. Sie sind klein genug, dass es sich dafür niemals lohnt eine neue Server-Komponente als zentralen Sessionstore aufzubauen. Ich will aber auch meine stateless Server nicht aufgeben. Da die zu speichernden Informationen absolut unkritisch sind, kamen wir im Team schnell zum Ergebnis, dass Cookies das Mittel der Wahl sind.

Und dann kam eine Idee zum Einsatz, die mir von Theo Schlossnagle in seinem großartigen Buch Scalable Internet Architectures eingepflanzt wurde, und nun 5 Jahre Zeit zum Gären hatte: Um der Möglichkeit Rechnung zu tragen, dass wir ggf. in Zukunft doch unseren Sessionstore bekommen und die Infos dann in der Session anstatt in Cookies stehen können, werden wir den Abstraktionsgrad der Cookies erhöhen und auf die Informationen über die HttpSession zugreifen. Die Cookies sind für uns nur noch ein technisches Implementierungsdetail der HttpSession.

Die Implementierung erfolgt auf Basis eines eigenen Session-Handlings. Das einzige, war wir brauchen ist ein eigener ISessionPersistor, der die Session-Werte aus den Cookies liest und zum Ende der Verarbeitung wieder in die Cookies schreibt:

public class CookieSessionPersistor implements ISessionPersistor {
  private HttpServletRequest request;
  private HttpServletResponse response;
  private Map<String, Converter<String, ?>> converters = new HashMap<String, Converter<String,?>>();

  public CookieSessionPersistor(HttpServletRequest request, HttpServletResponse response) {
    this.request = request;
    this.response = response;
    converters.put("lang", Converters.stringConverter);
    converters.put("track", Converters.booleanConverter);
  }

  @Override
  public Map<String, Object> getSession(String sid) {
    Map<String, Object> map = new HashMap<String, Object>();
    for (Cookie cookie : request.getCookies()) {
      String key = cookie.getName();
      Converter<String, ?> converter = converters.get(key);
      if (converter != null) {
        map.put(key, converter.convert(cookie.getValue()));
      }
    }
    return map;
  }

  @Override
  public void saveSession(String sid, Map<String, Object> sessionMap) {
    for (Entry<String, Object> entry : sessionMap.entrySet()) {
      if (converters.keySet().contains(entry.getKey())) { // nur vorab definierte Werte in Cookies speichern
        Cookie cookie = new Cookie(entry.getKey(), entry.getValue().toString());
        response.addCookie(cookie);
      } // else { throw Something(); }
    }
  }

Der SessionPersistor ist recht straightforward. Eine Besonderheit ist die Map mit den Convertern. Diese hat zwei Bedeutungen:

  1. In einem Cookie können nur Strings gespeichert werden, aber irgendwie müssen die Strings in den ursprünglichen Datentypen innerhalb der Session konvertiert werden. Dies erfolgt über die zum Key definierten Converter.
  2. Eine HttpSession ist eigentlich eine sichere Datenquelle, in der ein Entwickler üblicherweise vertrauliche Objekte speichern und Objekte ungeprüft auslesen können. Diese haben wir nun kompromitiert. Um sicherzustellen, dass nicht ungewollt vetrauliche Informationen in einem Cookie landen, müssen alle Werte, die rausgegeben werden können, explizit im CookieSessionPersistor „freigeschaltet“ werden.

Dem aufmerksamen Leser ist aufgefallen, dass der CookieSessionPersistor einen HttpRequest und eine HttpReponse im Construktur übergeben bekommt. Das passt mit der Idee diesen als Bean im MySessionFilter zu wiren nicht zusammen. Daher müssen wir am Filter auch noch Anpassungen vornehmen:

public class MySessionFilter implements Filter {
  @Override
  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) resp;
    String sid = request.getId();
    ISessionPersistor sessionPersistor = new CookieSessionPersistor(request, response);
    Map<String, Object> sessionMap = sessionPersistor.getSession(sid);
    OwnSessionHttpServletRequest wrappedRequest = new OwnSessionHttpServletRequest(request, sid, sessionMap);

    try {
      chain.doFilter(wrappedRequest, response);
    } finally {
      sessionPersistor.saveSession(sid, sessionMap);
    }
  }
}

Fazit
Mit ein paar Codezeilen haben wir es geschafft verteilten Sessionstore zu implementieren. Bei Bedarf kann die Lebensdauer der Objekte über das Sessionende hinaus erhöht werden, dazu muss das Expiry-Date des Cookies entsprechend erhöht werden.

In meiner Einleitung tu ich diesem Pattern sogar unrecht, da es wie eine aus der Not heraus geborene Idee erscheint. In Wirklichkeit haben wir einen hochperformanten, skalierbaren und stark verteilten Sessionstore eingerichtet, der quasi mit beliebiger Last umgehen kann.

Man kann dieses Pattern noch weiterspinnen und sich entscheiden die Map z.B. in JSON zu serialisieren und dann ins Cookie zu schreiben. Um zu verhindern, dass User die Werte manipulieren, können diese noch verschlüsselt und mit einem Hash abgesichert werden.

Zwei Haken werden wir aber nicht wegdiskutiert bekommen:

  • Die zu speichernde Datenmenge ist begrenzt => Wir wollen keine riesigen Cookies durch die Welt versenden.
  • Selbst wenn wir die Cookies verschlüsseln, hashen und durch den Fleischwolf drehen, bleibt es dabei, dass ein User diese hacken und uns kompromierte Werte unterschieben könnte. Für den Hochsicherheitsbereich ist das sicher kein geeignetes Pattern.

Überall dort, wor wir mit diesen Einschränkungen leben können, können wir mit ganz wenig Aufwand eine hochskalierbare Lösung fürs Sessionhandling implementieren.

Nutzen von Spring in JEE-verwalteten Komponenten

Schlagwörter

, , , , ,

Im Großen und Ganzen integriert sich Spring gut in die JEE-Welt. Es gibt aber einen Bruch an der Stelle, an der man in die Welt von Komponenten eintritt, die von JEE, oder konkret vom Applicationserver selbst, verwaltet werden.

Damit Spring das Wiring eines Objektes vornehmen kann, muss es dieses Objekt selbst erzeugt haben. Dieses trifft z.B. auf Tags und Filter nicht zu, da diese vom Applicationserver instanziiert werden. Wie kann man trotzdem auf Beans innerhalb von Tags zugreifen?

Die Idee ist eigentlich ganz einfach: Wir schreiben die Bean, auf die Wiring angewendet werden soll, in eine statische Variable und instanziieren dieses Tag einmalig per Spring. Da Spring statische Varaiblen nicht ohne weiteres initialisiert, müssen wir noch etwas nachhelfen:

public class MyTag extends BodyTagSupport {
  private static Bean myBean;

  @Autowired
  public void setMyBean(Bean myBean) {
    MyTag.myBean = myBean;
  }
}

Innerhalb des Codes haben wir eine statische Variable definiert, dieser aber einen nicht-statischen Setter gegeben, so kann Spring diese zum Wiring verwenden. Nun muss dieses Tag nur noch einmalig per Spring instanziiert werden.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
  <bean id="myTag" class="de.mibutec.MyTag" />
</beans>

Fazit

Indem Verweise auf Spring-verwaltete Beans statisch definiert und über eine Spring-Instanziierung gewired wurden, haben wir eine Möglichkeit aus Tags und anderen Applicationserver-verwalteten Komponenten auf Beans zuzugreifen.

Wie schnell ist eine MongoDB als Sessionstore?

Schlagwörter

, , , ,

In meinem letztem Beitrag habe ich beschrieben wie man ein eigenes Session-Handling in einer Servlet-Umgebung implementiert und beliebige Systeme als Sessionstore verwenden kann. Die Frage ist nun: Welche Performance-Auswirkungen hat das auf meine Applikation?

Dies habe ich anhand der MongoDB als Sessionstore evaluiert. Für diesen Test habe ich eine kleine Applikation geschrieben, die einen String aus der Session liest, diesen (sofern vorhanden) als Antwort in die generierte Seite schreibt und einen neuen String in die Session schreibt. Die Strings sind jeweils mit einer Wahrscheinlichkeit von 1/3 entweder 100 Zeichen, 200 Zeichen oder 300 Zeichen lang. Unterschiedliche Zeichenlängen speichern zu müssen macht es für die MongoDB noch mal schwerer intern ihre Daten zu verwalten.

Der Sourcecode dafür sieht wie folgt aus


public class TestServlet extends HttpServlet {
  private static String[] strings = new String[] {
    shortString,
    middleString,
    longString
  };

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    if (req.getSession().getAttribute("map") != null) {
      resp.getWriter().write(req.getSession().getAttribute("map").toString());
    }
    Map<String, String> map = new HashMap<String, String>();
    String s = strings[new Random().nextInt(3)];
    map.put("bla", s);
    req.getSession().setAttribute("map", map);
  }
}

Den Test habe ich auf meinem Notebook gemacht, dieser hat eine Intel Core(TM) i7-2640M CPU (4 Kerne a 2.800 MHz) und 8 GB RAM. Zur Festplatte kann ich nicht viel sagen, außer dass es keine SSD ist (sprich IO ließe sich in einer Serverumgebung noch erhöhen). Als Appserver habe ich einen Jetty out-of-the-box verwendet, sprich nichts an den Konfigurationen oder sonstigem geändert. Die MongoDB läuft auf dem gleichen Rechner und ist auch out-of-the-box konfiguriert.

Den Test habe ich mit JMeter 2.9 gemacht, dieser lief auf einem anderen Rechner, dieser war per WLan mit meinem Rechner verbunden. Der Test ruft die oben beschriebene Seite 10x hintereinander auf, wobei JMeter die Session hält (sprich: es merkt sich die Cookies). Für den Test habe ich 75 parallele Threads laufen lassen, die unendlich lange wiederholen.

Zuerst führte ich den Test durch ohne das Session-Handling zu überschreiben. Das Ergebnis war schon ziemlich cool: Mit meinem kleinen Laptop kann ich ca. 1900 Request / Sekunde beantworten. Diese werden im Mittel in 35 ms beantwortet:

nomongo

Dann habe ich auf meinen MongoSessionstore umgestellt. Die Java-Api hatte ich ursprünglich auch out-of-the-box belassen. Doch damit habe ich eine böse Überraschung erlebt:

ScreenHunter_06 Sep. 24 12.13

Die Anzahl der Fehler ist ab 1.000 Anfragen / Sekunde plötzlich in die Höhe geschossen und mein Log war plötzlich voll mit


com.mongodb.DBPortPool$SemaphoresOut: Out of semaphores to get db connection
  at com.mongodb.DBPortPool.get(DBPortPool.java:206)
  at com.mongodb.DBTCPConnector$MyPort.get(DBTCPConnector.java:437)
  at com.mongodb.DBTCPConnector.innerCall(DBTCPConnector.java:277)
  at com.mongodb.DBTCPConnector.call(DBTCPConnector.java:256)
  at com.mongodb.DBApiLayer$MyCollection.__find(DBApiLayer.java:289)
  at com.mongodb.DBApiLayer$MyCollection.__find(DBApiLayer.java:274)
  at com.mongodb.DBCursor._check(DBCursor.java:368)
  at com.mongodb.DBCursor._hasNext(DBCursor.java:459)
  at com.mongodb.DBCursor.hasNext(DBCursor.java:484)
...

Das lag daran, dass die Standard-Größe des Connection-Pools für meinen Test zu klein war. Der Mongo-Client hatte damit keine Connections mehr über, die er verwenden konnte. Ein beherztes

  MongoOptions options = new MongoOptions();
  options.setConnectionsPerHost(100);
  Mongo mongo = new Mongo("localhost", options);

führte dann zu folgendem Ergebnis:

mongo

Fazit

Auf einem handelsüblichen Rechner habe ich mit der MongoDB Standardkonfiguration einen Durchsatz von 1778 Seitenaufrufen / Sekunde geschafft, die je einen lesenden und einen schreibenden Zugriff auf die Datenbank machten. Das ist gerade mal 7% weniger Durchsatz als die Standardimplementierung der Session macht. Wenn man bedenkt, dass der normale Anwendungsfall in Webapplikation komplexer ist als einen Wert aus der Session zu lesen und auszugeben, wird der „Overhead“ durch den Zugriff auf die MongoDB noch weniger ins Gewicht fallen.

Die Antwortzeit hat sich durch den Einsatz der MongoDB von im Mittel 35 ms auf 38 ms verschlechtert, das entspricht einer Verschlechterung von ca. 9%. Aber hier sei wieder auf realistischere Anwendungsfälle und eine übliche Anbindung an die Webapplikation verwiesen. So sind hier die absoulten 3 ms Verschlechterung aussagekräftiger als die 9%.

Eigenes Session-Handling in Webapplikationen

Schlagwörter

, , , , ,

Immer noch sehe ich es häufig in Projekten, dass der State von Webbapplikationen über Session-Stickyness und den Standard-Session-Handler vom Tomcat hergestellt wird. Spätestens wenn es dann um Server-Downtime (geplant oder ungeplant) geht, fangen Bauchschmerzen und Workarounds an. Dabei ist es ganz einfach die Implementierung des Session-Handlings gegen eine eigene Implementierung auszutauschen und dann einen zentralen Session-Speicher (z.B. MongoDb) zu verwenden.

Was wir brauchen ist eine eigene Implementierung des Interfaces HttpSession, diese könnte etwa wie folgt aussehen:

public class MyOwnSession implements HttpSession {
  private Map<String, Object> sessionMap;
  private final String id;
  private ServletContext servletContext;
  private boolean isNew = false;
  private boolean isInvalid = false;

  public MongoSession(String id, ServletContext servletContext, Map<String, Object> sessionMap) {
    this.id = id;
    this.servletContext = servletContext;
    if (sessionMap == null) {
      sessionMap = new Hashtable<String, Object>();
      isNew = true;
    }
    this.sessionMap = sessionMap;
  }

  private void checkValidation() {
    if (getContext().isInvalided()) {
      throw new IllegalStateException("Session is invalided");
    }
  }

  @Override
  public void invalidate() {
    checkValidation();
    isInvalid = true;
  }

  @Override
  public Object getAttribute(String key) {
    checkValidation();
    return sessionMap.get(key);
  }

  @Override
  public void putValue(String key, Object value) {
    checkValidation();
    sessionMap.put(key, value);
  }
  ...
}

Der wichtigste Part des Ganzen ist die Map<String, Object>, die die Session-Daten enthält. Die muss nicht threadsafe sein, da nur der Request-Thread drauf zugreift. Ansonsten sind noch ein paar Member als Meta-Daten zur Session drin, die den aktuellen Status der Session beschreiben und benötigt werden, um alle Methoden von HttpSession implementieren zu können. Zu den oben aufgeführten kommen noch ein paar zum Timeout-Handling. Aber im Wesentlichen ist das alles straightforward. Spannender ist die Frage wie wir denn nun unsere Session-Implementierung in die Applikation bekommen. Der Zugriff erfolgt ja über HttpServletRequest.getSession().

Also müssen wir diese Methode irgendwie überschreiben. Es ist aber nicht ohne weiteres möglich die Implementierung von HttpServletRequest zu überschreiben: Zum einen, weil es gar keine Möglichkeit gibt auf die Erzeugung des Objekte Einfluss zu nehmen, und vor allem, weil wir in unserem Sourcecode keine konkreten Appserver-Klassen referenzieren wollen. Dafür liefert die servlet.jar eine interessante Klasse, den HttpServletRequestWrapper, dieser implementiert das HttpServletRequest-Interface und reicht alle Aufrufe an ein eingebettete HttpServletRequest-Objekt durch. Dort wo man Funktionalität überschreiben möchte, kann man das tun.

public class OwnSessionHttpServletRequest extends HttpServletRequestWrapper {
  private final MyOwnSession session;

  public OwnSessionHttpServletRequest(HttpServletRequest request, String sid, Map<String, Object> sessionMap) {
    super(request);
    this.session = new MyOwnSession(sid, getServletContext(), sessionMap);
  }

  @Override
  public MongoSession getSession() {
    return getSession(true);
  }

  @Override
    public MongoSession getSession(boolean create) {
    return session;
  }

  @Override
  public boolean isRequestedSessionIdValid() {
    return true;
  }
}

Nun sind wir einen Schritt weiter und haben ein Request-Objekt, dass unsere Session-Implementierung verwendet, aber auch die wird noch nicht in der Applikation verwendet. Hier hilft uns ein Filter weiter:

public class MySessionFilter implements Filter {
  private ISessionPersistor sessionPersistor;

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    ApplicationContext ac = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext());
    this.sessionPersistor = ac.getBean(ISessionPersistor.class);
  }

  @Override
  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) resp;
    String sid = request.getId();
    Map<String, Object> sessionMap = sessionPersistor.getSession(sid);
    OwnSessionHttpServletRequest wrappedRequest = new OwnSessionHttpServletRequest(request, sid, sessionMap);

    try {
      chain.doFilter(wrappedRequest, response);
    } finally {
      sessionPersistor.saveSession(sid, sessionMap);
    }
  }

  @Override
  public void destroy() {
    // nothing to do
  }
}

Nun haben wir es geschafft unsere eigene Session-Implementierung der Applikation unterzuschieben. Wie und wo die Sessions nun persitiert werden, ist im Interface ISessionPersistor hinterlegt. Dieses sieht im einfachsten Fall so aus:

public interface ISessionPersistor {
  public Map<String, Object> getSession(String sid);
  public void saveSession(String sid, Map<String, Object> sessionMap);
}

Bei der Implementierung des Interface kann man sich austoben und die Daten in einem Memcached, einer MongoDb, Hazelcast oder wonach einem der Sinn steht, speichern.

Das Interface wird mit der Implementierung in Zeile 7 verdrahtet. Dazu habe ich im Beispiel Tools verwendet, die von Spring MVC mit an die Hand gegeben werden. Für die, die nicht Spring MVC verwenden, kommt in einem der nächsten Beiträgen noch eine Idee, wie man Spring in JEE verwalteten Komponenten (Tags, Filter, …) verwendet.

Fazit
Mit einem überschaubaren Aufwand haben wir es geschafft eine eigene Implementierung der Sessiondaten-Verwaltung zu erstellen und können dafür nun beliebige Systeme nutzen.
Natürlich ist das nur die einfachste Art das zu implementieren, es fehlen noch Sachen wie:

  • Housekeeping-Jobs, die alte Sessions abräumen
  • Man könnte ein Lazy-Loading einbauen, das eine Sessin nur dann lädt, wenn diese auch wirklich benötigt wird
  • Man könnte sich merken, ob die Session in diesem Request verändert wurde und nur in diesem Fall die SessionMap am Ende schreiben

Aber wie immer gilt: First make it run, then make it fast!

Mailversand in Oberflächentests testen mit Spring

Schlagwörter

, ,

Zu einigen Workflows, die man mit Oberflächentests testet, gehört auch das Versenden von Emails. Dies wird in in den Tests entweder gar nicht berücksichtigt, oder nur unter größten Schmerzen bewerkstelligt, wo Sachen eine Rolle spielen wie: Mailclient in den Tests, zeitliche Synchronisation bis die Email eintrifft, Abhängigkeiten zu Mailservern und andere schreckliche Sachen.

Dabei ist es so einfach den Mailversand mit den Mitteln, die man für die Oberflächentests eh schon zur Hand hat, zu testen (vorausgesetzt man hat in seiner zu testenden Applikation ein Mindestmaß an sauberem Software-Design walten lassen):

Denn dann hat man in seiner Applikation irgendwo ein Interface, das im einfachsten Fall etwa so aussieht:

public interface Mailer {
  public void sendMail(String recipient, String subject, String body);
}

Dieses hat dann eine Implementierung, die ganz viel mit javax.mail.* macht und in Spring etwa wie folgt verdrahtet ist:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
  <bean id="mailer" class="de.mibutec.MailerImpl">
    <property name="smtpHost" .../>
  </bean>
</beans>

Um das Versenden von Mails sauber testen zu können, werden wir diese nun nicht mehr an den Smtp-Server senden, sondern in der Usersession speichern und dann mit einer View zur Anzeige bringen:


public class MailArchiver implements Mailer, Serializable {
  private static final long serialVersionUID = 1L;
  private List<ArchivedEmail> emails = new LinkedList<MailArchiver.ArchivedEmail>();

  @Override
  public void sendMail(String recipient, String subject, String body) {
    ArchivedEmail email = new ArchivedEmail();
    email.setReceiver(recipient);
    email.setSubject(subject);
    email.setBody(text);
    emails.add(email);
  }

  public List<ArchivedEmail> getEmails() {
    return emails;
  }

  public static class ArchivedEmail implements Serializable {
    private static final long serialVersionUID = 1L;
    private String receiver;
    private String subject;
    private String body;

    // getter /setter
  }
}

Die zugehörige Spring Konfiguration sieht dann wie folgt aus:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
  <bean id="sessionArchiver" scope="session">
    <aop:scoped-proxy/>
  </bean>
</beans>

Der wichtige Part hierbei ist, dass wir diese Bean im Scope „session“ definieren, damit auch wirklich nur die Mails der aktuellen Session in unserer View angezeigt werden.
Im Controller müssen wir nur noch auf unseren MailArchiver zugreifen um die gespeicherten Mails der View bereitzustellen:

@Controller
@RequestMapping("debugController")
public class DebugController {
  @Autowired(required=false)
  private MailArchiver mailArchiver;

  @RequestMapping(value="/mails")
  public String showMails(Model model) throws IOException, ServletException {
    model.addAttribute("mails", mailArchiver.getEmails());
    return "debug/mail";
  }
}

Den Aufbau der View überlasse ich der Phantasie des Lesers.

Fazit
Mit geringem Aufwand ist es uns gelungen Mails, die aus unserer Webapplikation versendet werden, in eine für eine Oberflächentests auswertbare Form umzuwandeln, ohne viel von der Aussagekraft des Tests zu verlieren.
Mit diesem Ansatz ist es natürlich wichtig weiterhin auch noch das reale Versenden von Mails zu testen, um zu prüfen, dass die eigentliche Mail-Implementierung funktioniert. Dies reicht dann allerdings für einige exemplarisch Fälle und muss nicht für alle Konstalltionen, in denen Mails versendet werden, erfolgen und kann auch manuell gemacht werden.

Automatisierte Tests von Pageobjekten

Schlagwörter

, ,

Über Pageobjekte ist schon viel geschrieben worden (z.B. http://martinfowler.com/bliki/PageObject.html), und ich will an dieser Stelle weder anfangen Sinn und Unsinn von PageObjekten zu diskutieren noch einen neuen Ansatz diese zu implementieren. Ich möchte lediglich eine Lücke schließen, die mir bei automatisierten Oberflächentests öfters über den Weg gelaufen ist.

Oberflächentests sind in der Regel aus dem eigentlichen Webentwicklungsprojekt ausgelagert und haben ein eigenes (Eclipse-) Projekt (oder noch schlimmer ein eigenes organisatorisches Projekt mit einem eigenen Team). Zumindest das eigene Eclipse-Projekt ist OK, da Oberflächentests viele eigene Abhängigkeiten (Webdriver & Co.) mitbringen, die man in seinem Webprojekt gar nicht haben möchte. Da Oberflächentests länger laufen als Unit-Tests und mehr Abhängigkeiten haben (Browser, Selenium-Server, laufende Applikation) haben diese oft auch eine andere (geringere) Frequenz, in der sie ausgeführt werden. Da Pageobjekte eher den Oberflächentests zugeordnet werden als der Webapplikation, werden diese auch entsprechend seltener getestet.

Leider passiert es häufig, dass die Funktion von Pagepbjekten durch die Weiterentwicklung der Oberfläche zerstört wird. Daher wäre es schön diese öfters zu testen. Mein Ansatz ist für Pageobjekte automatisierte Tests zu erstellen und diese im Rahmen der Entwicklung zu testen. Dabei ist mir folgendes wichtig:

  1. Die Laufzeit der Tests soll möglichst gering sein, damit Entwickler diese auch regelmäßig ausführen.
  2. Möglichst geringe Abhängigkeiten der Tests zu anderen Systemen und installierter Software, um die Entwicklungsumgebung simpel zu halten.
  3. Möglichst geringer Aufwand diese Tests zu implementieren.

Die Projektaufteilung sind dabei wie folgt aus.

po-unit-projects

  • Es gibt das zu testende Webprojekt, das hat als Libraries z.B. Spring, Hibernate und Sachen, die ein Webprojekt halt so als Abhängigkeiten hat.
  • Davon unabhängig gibt es das Pageobjekt-Projekt (technisch unabhängig, es besteht aber natürlich eine logische Abhängigkeit, da die Pageobjekte auf die Seitenstruktur des Webprojektes passen müssen). Das hat als Abhängigkeit WebDriver, um seine Selektoren zusammenbauen zu können.
  • Dann gibt es noch das Projekt, in dem die Tests ablaufen. Das hat Abhängigkeiten zu den o.g. Projekten, sowie Jetty als Laufzeitumgebung für das Webprojekt und HTMLUnit als Implementierung von WebDriver.

Im PoTest-Projekt sind Unit-Tests für die Pageobjekte definiert. Daneben kan dort das Webprojekt innerhalb einer Laufzeitumgebung gestartet werden. Die Tests führen keinerlei schreibende Operationen auf der Webapplikation aus, sondern öffnen nur Seiten und prüfen, ob die in den Pageobjekten definierten Elemente vorhanden sind. Daher muss die Applikation auch nicht für jeden einzelnen Test neu gestartet oder die Datenbank neu initialisert werden. Es reicht ein Start der Applikation für alle Tests.

Durch einmaliges Starten des Servers und die Verwendung von HTMLUnit als Browser haben wir eine gute Performance der Tests und Abhängigkeiten (zum Selenium-Server und einen realen Browser) reduziert.

Um für die Erstellung von den Unit-Tests nicht viel laufende Arbeit investieren zu müssen, müssen wir jetzt initial Arbeit investieren. Nehmen wir als Beispiel folgende Seite:

ScreenHunter_01 Sep. 20 13.02

Für die oben gezeigt Seite könnte ein Pageobjekt wie folgt aussehen:

public class FooOverviewPo extends AbstractPageobject {
  public void open() { ... }
  public WebElement getSpecialFeatureButton() { ... }
  public FooElement getFooElementById(String id) { ... }

  public static class FooElement extends AbstractPageobject {
    public WebElement getId() { ... }
    public WebElement getName() { ... }
    public WebElement getBeschreibung() { ... }
  }
}

Das Ziel der Tests ist es alle Elemente der Seite auf Existenz zu prüfen. Die Gemeinsamkeit der meisten Methoden ist, dass diese den Ergebnistp „Webelement“ und keine Parameter haben. Dies läßt sich automatisiert gut über Reflections abbilden

public class PoTest {
  @BeforeClass
  public static void setup() throws Exception {
    TestServer.start();
    String baseUrl = TestServer.getBaseUrl();
    TestContext.INSTANCE.setBaseUrl(baseUrl);
    TestContext.INSTANCE.setBrowserName(TestContext.HTML_UNIT_NAME);
  }

  @AfterClass
  public static void tearDown() throws Exception {
    TestServer.stop();
  }

  @Test
  public void testFooOverview() throws Exception {
    FooOverviewPo fooOverviewPo = new FooOverviewPo();
    fooOverviewPo.open();
    testAllPoMethods(fooOverviewPo);
    testAllPoMethods(fooOverviewPo.getElementById("4711");
  }
}

Die Methode testAllPoMethode macht nichts anderes als durch alle Methoden des übergebenen Objekts zu gehen und die Methoden aufzurufen, die als Result ein Webelement liefern und keine Parameter haben. Diese werden aufgerufen und geprüft, dass als Ergebnis kein null geliefert wird.
Nun kann man sich noch vorstellen, dass sich manche Seiten nicht einfach so öffnen lassen, da diese hinter einem Login versteckt sind, oder man z.B. die Id einer Entität, die auf dieser Seite angezeigt werden soll, übergeben muss. Dafür würde der Test mit den entsprechenden Anpassungen an den POs etwa wie folgt aussehen:

@Test
public void testFooOverview() throws Exception {
  FooOverviewPo fooOverviewPo = new FooOverviewPo();
  fooOverviewPo.open("username=admin&password=secret");
  testAllPoMethods(fooOverviewPo);
  testAllPoMethods(fooOverviewPo.getElementById("4711");
}

Fazit

Mit überschaubarem Aufwand ist es gelungen Unit-Tests für Pageobjekte zu erstellen, die die Entwickler ohne großen Einrichtungsaufwand und mit akzeptabler Laufzeit bei sich lokal laufen lassen können. Dadurch konnte ich in meinen Projekten die Anzahl der False-Positives bei Oberflächentests deutlich reduzieren.
Die Grenzen dieses Ansatzes will ich auch nicht verscheigen: Der Einsatz von HTMLUnit reicht aus, um in recht statischen Seiten den Aufbau des DOMs (das Vorhandensein bestimmter Lokatoren) zu testen. Bei sehr dynamsichen Seiten, die schon viel JS laufen lassen, um den initialen DOM überhaupt aufbauen zu können, trägt dieser Ansatz nicht mehr. Daneben muss es in der Applikation möglich sein alle Seiten per URL-Eingabe direkt zu erreichen, so muss man z.B. Username/Passwort in der URL übertragen oder die Authentifizierung innerhalb der App für den Test ausschalten können. Auch das ist nicht immer gegeben.
Wenn man mit diesen Restriktionen leben kann, handelt es sich um einen schönen Ansatz die Qualität seiner Oberflächentests deutlich zu steigern.