Blog

Super-Aufrufe verspotten

Jeroen van Erp

Jeroen van Erp

Aktualisiert Oktober 23, 2025
4 Minuten

Eine weitere Folge unseres Mocking-Abenteuers. Nachdem ich Ihnen letzte Woche einige mögliche Lösungen für das Mocking statischer Methodenaufrufe vorgestellt habe. Heute stehe ich vor einem anderen Problem.

JSF ist ein Framework, das auf der Idee von (wiederverwendbaren) Komponenten aufbaut. Wenn wir eine neue Komponente einführen möchten, ist es möglich (und sinnvoll), eine der vorhandenen Komponenten zu erweitern, was uns einen Teil der Arbeit abnimmt. Allerdings wollen wir nicht zu viel über die Interna der Komponente wissen, die wir erweitern, vor allem nicht, wenn wir sie testen wollen. Eigentlich sollten wir davon ausgehen können, dass die Basiskomponente gründlich getestet wurde. Wenn Sie keine Methoden überschreiben, funktioniert die EasyMock-Funktion für partielles Mocking, die in diesem Blogeintrag vorgestellt wurde, hervorragend. Wenn wir jedoch eine Komponente erweitern, möchten wir wahrscheinlich einige ihrer Methoden außer Kraft setzen und sie mit einem etwas anderen Verhalten ausstatten. Werfen wir einen Blick auf ein kleines Beispiel:

public class CustomTree extends org.apache.myfaces.custom.tree2.HtmlTree {
  private String nodeId;
  @Override
  public Object saveState(FacesContext context) {
  Object[] state = new Object[2];
  state[0] = super.saveState(context);
  state[1] = nodeId;
  Status zurückgeben;
  }
  public void setNodeId(String nodeId) {
  this.nodeId = nodeId;
  }
  // Die restliche Implementierung entfällt.
}

Wie können wir die Methode CustomTree.saveState testen? Wir möchten uns im Test nicht damit befassen müssen, was die Superklasse mit dem (gespotteten) FacesContext macht. Am liebsten würden wir den folgenden Unit-Test schreiben:

public class CustomTreeTest extends TestCase {
  private CustomTree customTree;
  private FacesContext facesContext;
  public void setUp() {
  facesContext = EasyMock.createMock(FacesContext.class);
  // TODO: Erstellen Sie eine neue (teilweise gespottete) Instanz von customTree.
  }
  public void testShouldSaveNodeIdInState() {
  customTree.setNodeId("someId");
  verify(facesContext);
  Object[] state = (Object[]) customTree.saveState(facesContext);
  replay(facesContext);
  assertEquals("someId", state[1]);
  }
}

Klingt ganz einfach, oder? Aber beachten Sie das große TODO da drin. Wie erstellen wir eine Instanz von CustomTree? Versuchen wir zunächst das Einfachste.

customTree = new CustomTree();

Dies führt jedoch zu einem Stacktrace, der mehr oder weniger wie folgt aussehen wird: java.lang.NullPointerException at org.apache.myfaces.custom.tree2.UITreeData.saveState(UITreeData.java:91) at org.apache.myfaces.custom.tree2.HtmlTree.saveState(HtmlTree.java:58) at com.xebia.jsf.CustomTree.saveState(CustomTree.java:58) at com.xebia.jsf.CustomTree.testShouldSaveNodeIdInState(CustomTreeTest.java:38) Die NullPointerException ist aufgetreten, weil die UITreeData-Klasse den Status von einem DataModel abrufen will. Wir haben das DataModel jedoch nicht auf unseren CustomTree gesetzt, da es in der Methode, die wir testen, nicht direkt benötigt wird. Wenn es uns gelingt, dies zu beheben, werden wir sicherlich auf eine weitere "fehlende" Abhängigkeit stoßen, die für den Code, den wir testen, nicht von Nutzen ist. Daraus können wir schließen, dass dies nicht der bevorzugte Weg war, um das zu testende Objekt zu erstellen. Aber was können wir uns dann ansehen? 1. Indirektion Wenn wir dieses Problem auf eines reduzieren, für das wir bereits eine Lösung haben, sind wir fertig. Wir können eine Umleitung einführen, um den Aufruf von super.saveState(FacesContext) darin zu verpacken. Die resultierende Klasse könnte dann wie folgt aussehen:

public class CustomTree extends org.apache.myfaces.custom.tree2.HtmlTree {
  private String nodeId;
  @Override
  public Object saveState(FacesContext context) {
  Object[] state = new Object[2];
  state[0] = saveSuperState(context);
  state[1] = nodeId;
  Status zurückgeben;
  }
  protected Object saveSuperState(FacesContext context) {
  return super.saveState(context);
  }
  public void setNodeId(String nodeId) {
  this.nodeId = nodeId;
  }
  // Die restliche Implementierung entfällt.
}

Mit dieser Umleitung ist es plötzlich ganz einfach, entweder die EasyMock-Funktion für partielles Mocking zu verwenden oder eine Instanz zum Testen wie folgt zu erstellen:

customTree = new CustomTree() {
  @Override
  protected Object saveSuperState(FacesContext context) {
  return new Object();
  }
}

Profis

  • Einfach zu implementieren durch ein einfaches Refactoring wie "extract method" in Eclipse

Nachteile

  • Der zu testende Code muss angepasst werden und wird sich der Testprobleme "bewusst".

2. AspectJ / AOP Mit einem geschickt geschriebenen Aspekt könnte es möglich sein, den Aufruf super.saveState(FacesContext) zu überlisten. Ich habe das noch nicht ausprobiert, bin aber gespannt, ob jemand von Ihnen eine Lösung findet. Profis

  • Der zu testende Code muss nicht angepasst werden

Nachteile

  • Wenn AspectJ Weaving für Ihr Projekt nicht aktiviert ist, müssen Sie es in Ihr Build und Ihre IDE integrieren
  • Viele Projekte möchten die Kompilierzeitverflechtung nicht verwenden, da sie den geschriebenen Code verändert und somit unbeabsichtigte Nebeneffekte hervorrufen kann, wenn sie nicht korrekt verwendet wird.

Kann mir jemand ein Muster nennen, das ich übersehen habe? Oder eine sauberere Lösung als eine der beiden hier vorgestellten? Wiederum fühlt sich keine der beiden Lösungen richtig an. Und im Moment habe ich keine Funktion in EasyMock (oder einem anderen Mocking-Framework) gefunden, die dieses Problem gut löst.

Verfasst von

Jeroen van Erp

Contact

Let’s discuss how we can support your journey.