infinite state machine

Testbaren Code schreiben (Teil 1)

Introduction

user

Franziska Neumeister

Hat "Media Systems" studiert, entwickelt mobile Apps und will wissen, ob Androiden auch von elektrischen Schafen träumen


LATEST POSTS

Multithreading mit RxJava 18th April, 2016

Kotlin in Android Studio 13th March, 2015

Android

Testbaren Code schreiben (Teil 1)

Posted on .

Leicht testbarer Code ist gleichzeitig leicht zu ändernder Code, ist gleichzeitig leicht verständlicher Code. Wie schreibt man aber Code, der sich gut testen lässt?

Relativ einfach lassen sich noch Akzeptanz-Tests umsetzten, die die gesamte Anwendung starten und prüfen. Dabei müssen vielleicht nur einzelne Web-APIs oder Datenbanken ersetzt werden mit System für eine Testumgebung. Hat man eine bestehende Code-Basis und beschließt Unit-Tests einzuführen, trifft man wahrscheinlich auf große Schwierigkeiten auch nur einen einzelnen funktionierenden Test zu schreiben.

Das Problem liegt dann häufig darin, dass die Klasse, die man testen möchte, die komplette Anwendung um sicher herum benötigt, um zu funktionieren. Ein anderes Problem kann sein, dass man keine Möglichkeit findet, die Ergebnisse einer Operation in der zu testenden Komponente zu überprüfen. Es fehlt die Möglichkeit den Zustand der Komponente oder seiner direkten Kollaborateure zu messen.

Testbarer Code bedeutet also die Komponente, die man testen möchte, vom Rest der Anwendung isolieren zu können. Dazu muss man feste Bindungen an die Abhängigkeiten von anderen Komponenten verhindern. Die Komponenten, mit denen das Test-Subjekt interagiert, sollen austauschbar sein, damit sie durch Test-Implementierungen, z.B. Mock-Objekte ersetzt werden können.

Dependency Injection

Komponenten sollten nicht andere Komponenten konstruieren, mit denen sie interagieren. Ihre Abhängigkeiten sollten sie von außen erhalten, z.B. als Argumente im Konstruktor oder über Setter-Methoden. Fabriken oder ein ein Dependency Injection Framework sollten sich darum kümmern, Objekte zu konstruieren. So erhält man auch eine einfache Möglichkeit, Kollaborateure eines Test-Subjekts auszutauschen. Das Ziel sollte sein, möglichst selten das Schlüsselwort new im Code zu lesen, weil es gleichbedeutend ist mit einer festen Bindung an eine konkrete Implementierung einer Schnittstelle.

In diesem Beispiel ist die Editor Klasse fest an die Implementierung des Document-Interfaces durch WordDocument gebunden, weil es sie selbst erzeugt.

public class Editor{
   private IDocument  mDocument;
   public Editor(){
      mDocument = new WordDocument();
   }
}

Statt dessen wird hier Abhängigkeit von einer Document-Implementierung über den Konstruktor als Argument befriedigt:

public class Editor{
   private IDocument  mDocument;
   public Editor(IDocument  document){
      mDocument = document;
   }
}

oder die Abhängigkeit wird über ein Dependency Injection Framework wie Guice aufgelöst

public class Editor{
   @Inject
   private IDocument  mDocument;
}

Wenn die Abhängigkeiten einer Komponente nicht von ihr selbst erzeugt werden, sondern von außen geliefert werden, können sie beim Test einfach ersetzt werden, wie hier:

@Test
public void testEditorCreation(){
   IDocument doc = mock(WordDocument.class);
   Editor subject = new Editor(doc);
   // ...
}

Law of Demeter

Objekte sollen nur mit Komponenten arbeiten, die sie direkt kennen. Das Gesetz von Demeter wird gebrochen, wenn man sich z.B. durch eine Reihe von verketteten Getter-Methoden arbeitet, um ein Objekt zu erhalten, das am Ende dieses Objektgrafen liegt. Die klassiche Definition des Gesetzes ist, dass in einer Methode nur Methoden aufgerufen werden dürfen, die zur eigenen Klasse gehören, die zu Instanzvariablen der Klasse gehören oder zu Objekten gehören, die die Methode als Argument erhält.

public class Videoplayer{
   private mScreen;
   public void play(VideoFile file){
      // okay, weil Methode der selben Klasse
      this.clearDisplay();
      // okay, weil Methode eines Arguments der Methode
      VideoContainer video = file.getVideoContent();
      // okay, weil Methode einer Instanzvariable der Klasse
      mScreen.display(video)
   }
   private void clearDisplay(){...}
}

Wenn ein Objekt einen ganzen Objekt-Grafen „durchwühlt“, um an ein benötigtes Objekt zu kommen, ist es auch automatisch an den Aufbau dieses Objekt-Grafen gebunden. Um das eine gesuchte Objekt bei einem Test durch eine Test-Implementierung zu ersetzten, müsste man den gesamten Grafen nachbauen. Außerdem muss der Code jedes Mal aktualisiert werden, wenn sich der Aufbau des Objekt-Grafen ändert.

Statt selber nach dem Inventory-Objekt zu suchen, das die Methode benötigt…

public float calculateInventoryLoad(GameApplication app){
   Inventory = inventory = app.getGame().findPlayer(1).getInventory();
   // ...
}

… sollte die Methode direkt nach dem Objekt fragen, dass sie tatsächlich braucht:

public float calculateInventoryLoad(Inventory inventory){
   // ...
}

Folgendes Beispiel demonstriert, wieso es einfacher ist Code zu testen, der nur mit seinen direkten „Nachbarn“ spricht:

@Test
public void testWithLawOfDemeterViolation(){
   Inventory inventory = mock(Inventory.class);
   Player player = mock(Player.class);
   when(player.getInventory()).thenReturn(inventory);
   Game game = mock(Game.class);
   when(game.findPlayer(1)).thenReturn(player);
   GameApplication app = mock(GameApplication.class);
   when(app.getGame()).thenReturn(game);
   mTestSubject.calculateInventoryLoad(app);
   verify(inventory).someMethod();
}

Wenn das Gesetz von Demeter respektiert wird, müssen nicht ganze Objekt-Bäume für Tests nachgebaut werden

@Test
public void testWithoutLawOfDemeterViolation(){
   Inventory inventory = mock(Inventory.class);
   mTestSubject.calculateInventoryLoad(inventory);
   verify(inventory).someMethod();
}

Keine statischen Methoden

Statische Methoden sind kritisch, weil weil sie Abhängigkeiten von anderen Code-Komponenten verstecken. Sie setzen möglicherweise implizit einen bestimmten (globalen) Zustand voraus, um das gewünschte Ergebnis zu erzielen.

Ein statischer Methoden-Aufruf lässt sich nicht ersetzten wie ein Methoden-Aufruf auf einem Objekt, bei dem man das Objekt durch eine andere Implementierung ersetzten könnte. Daher erschweren statische Methoden die Trennung einer Komponente vom Rest des Programm-Codes.

In dieser Methode lässt sich der Aufruf des ServerHelper nicht verändern.

public void startGame(){
   ServerHelper.startSession();
   ServerHelper.createNewPlayer();
}

Wenn statt dessen die API in einer Instanz statt in einer statischen Methoden-Sammlung liegt, lässt sie sich für Tests durch Mocks ersetzen:

public void startGame(Server serverConnection){
   serverConnection.startSession();
   serverConnection.createNewPlayer();
}

Keine Singletons

Singletons oder statische Fabriken verstecken die Verwendung des new-Operators. Außerdem haben sie das selbe Problem wie andere statische Methoden, dass sie gegebenenfalls einen globalen Zustand verstecken, auf den sich eine Komponente vielleicht implizit verlässt.

Manche Dependecy Injection Bibliotheken bieten die Möglichkeit, eine Abhängigkeit als Singleton-Instanz zu definieren, so dass im gesamten Code die selbe Instanz einer Klasse verwendet wird. Trotzdem sollte der Code nicht so geschrieben werden, dass er sich darauf verlässt eine Singleton-Instanz vorliegen zu haben.

Singletons verhindern eine Komponente wirklich zu isolieren für Tests. Weil auch andere Komponenten auf die Singleton-Instanz zugriff haben, können sie sie manipulieren und damit auch versteckt Einfluss nehmen auf das Verhalten der zu testenden Komponente.

profile

Franziska Neumeister

Hat "Media Systems" studiert, entwickelt mobile Apps und will wissen, ob Androiden auch von elektrischen Schafen träumen

Navigation