
Faker sind ein Pattern um die Qualität von Unit-Tests zu erhöhen. Sie sollen dazu führen, dass Tests leichter zu schreiben, leichter zu erweitern und leichter zu lesen sind. Faker stellen zunächst einmal eine Investition dar, können das Schreiben der Tests danach aber erheblich beschleunigen.
Was macht einen Faker aus?
Faker sind Klassen, die ein bestimmtes Objekt zum Zweck von Unit-Tests bauen. Damit reduzieren sie den sonst notwendigen Setup-Code in Unit-Tests und machen Testdaten-Klassen überflüssig, da ein Faker selbst die Testdaten zur Verfügung stellt. Manchmal ist es nicht möglich ein Objekt direkt zu bauen, weil es z.B. spezifische Vorgaben in der Domäne zu den Statustransitionen gibt oder nicht der richtige Konstruktor bereitsteht, da die Objekte sonst von einem OR-Mapper oder aus einem SOAP- oder REST-Endpunkt heraus instanziert werden. In diesem Fall kann man den Faker ein „Fake“-Objekt bauen lassen, das das eigentliche Objekt erweitert oder das gleiche Interface implementiert. Die nachfolgenden Beispiele sind in Java geschrieben, können aber auch auf beliebige andere Sprachen mit Unterstützung für Objektorientierung angewandt werden.
Unit-Test ohne Faker
Im folgenden Beispiel zeige ich, wie ein Unit-Test zunächst ohne Faker aussehen kann:
@Test
public void shouldRejectBestellungIfMengeIsZero() {
// given
Bestellservice bestellservice = new Bestellservice();
Bestellung bestellung = new Bestellung();
bestellung.setKunde(new Kunde("Christian Pflugradt"));
bestellung.setPrioritaet(1);
Bestellposition position = new Bestellposition();
position.setArtikel(new Artikel("Schraubenzieher"));
position.setPreis(new Preis(10));
position.setMenge(0);
bestellung.add(position);
// when
bestellservice.abschliessen(bestellung);
// then
assertThat(bestellung.getStatus()).isEqualTo(Status.ABGEWIESEN);
}
Das ist eine Menge Setup-Code um einfach nur zu testen, dass eine Bestellung vom Bestellservice abgewiesen werden soll, wenn sie eine Position mit der Menge 0 enthält.
Unit-Test mit Setup-Methoden
Mit dedizierten Setup-Methoden lässt sich der Code lesbarer gestalten:
@Test
public void shouldRejectBestellungIfMengeIsZero() {
// given
Bestellservice bestellservice = new Bestellservice();
Bestellposition position = setupBestellposition();
position.setMenge(0);
Bestellung bestellung = setupBestellung(position);
// when
bestellservice.abschliessen(bestellung);
// then
assertThat(bestellung.getStatus()).isEqualTo(Status.ABGEWIESEN);
}
private Bestellung setupBestellung(Bestellposition... positionen) {
Bestellung bestellung = new Bestellung();
bestellung.setKunde(new Kunde("Christian Pflugradt"));
bestellung.setPrioritaet(1);
positionen.forEach(pos -> bestellung.add(pos));
return bestellung;
}
private Bestellposition setupBestellposition() {
Bestellposition position = new Bestellposition();
position.setArtikel(new Artikel("Schraubenzieher"));
position.setPreis(new Preis(10));
position.setMenge(1);
return position;
}
Die Setup-Methoden haben aber auch einige Nachteile:
- Eine lose Sammlung von Setup-Methoden in einer Testklasse ist nicht objektorientiert und wird schnell unübersichtlich.
- In der Realität werden Setup-Methoden eher nicht klassenübergreifend verwendet, sie stellen also nur einen Mehrwert innerhalb der gleichen Testklasse dar.
- Meist sind sie auf bestimmte Anwendungsfälle zugeschnitten. Um sie später für weitere Tests verwenden zu können, sind Anpassungen notwendig. Dadurch gehen ggf. bestehende Tests kaputt und müssen refactored werden oder die Setup-Methode wird dupliziert und das Duplikat angepasst.
- Es ist nicht transparent, was im Inneren einer Setup-Methode passiert und es entstehen leicht Abhängigkeiten von Tests zu ihren Setup-Methoden: Bei einer Methode
setupBestellung()
ohne Parameter, die auch Bestellpositionen anlegt, ist nicht klar, wie viele Positionen sich in der Bestellung befinden. Wenn ein Test erfordert, dass sich in der Bestellung mindestens zwei Positionen befinden, dann ist der Test kaputt, wenn jemand die Anzahl der Positionen in der Setup-Methode auf eins setzt.
Unit-Test mit Faker
Geht man nun also einen Schritt weiter, dann ist es naheliegend die Setup-Methoden in eigene Klasse auszulagern, und zwar eine Klasse für jedes zu testende Objekt, damit man nicht weiterhin eine lose unzusammenhängende, stetig wachsende Liste an Setup-Methoden pflegen muss. Faker setzen hier an, ein Beispiel:
public class BestellungFaker {
private Bestellung bestellung;
public BestellungFaker faker() {
return new BestellungFaker();
}
public BestellungFaker fakeBestellung() {
Bestellung bestellung = new Bestellung();
bestellung.setKunde(new Kunde("Christian Pflugradt"));
bestellung.setPrioritaet(1);
return this;
}
public BestellungFaker withBestellposition(Bestellposition position) {
bestellung.add(position);
}
public Bestellung fake() {
return bestellung;
}
}
Da wir auch eine Bestellposition brauchen und die Menge mit 0 überschreiben müssen, legen wir dafür eine zweite Faker-Klasse an.
public class BestellpositionFaker {
private Bestellposition position;
public BestellpositionFaker faker() {
return new BestellpositionFaker();
}
public BestellpositionFaker fakeBestellposition() {
Bestellposition position = new Bestellposition();
position.setArtikel(new Artikel("Schraubenzieher"));
position.setPreis(new Preis(10));
position.setMenge(1);
return this;
}
public BestellungFaker withMenge(int menge) {
position.setMenge(menge);
}
public Bestellung fake() {
return bestellung;
}
}
Mit diesen zwei Fakern sieht die Test-Methode deutlich übersichtlicher aus:
@Test
public void shouldRejectBestellungIfMengeIsZero() {
// given
int givenMenge = 0;
Bestellservice bestellservice = new Bestellservice();
Bestellung bestellung = BestellungFaker.faker()
.fakeBestellung()
.withBestellposition(BestellpositionFaker.faker()
.fakeBestellposition()
.withMenge(givenMenge)
.fake())
fake();
// when
bestellservice.abschliessen(bestellung);
// then
assertThat(bestellung.getStatus()).isEqualTo(Status.ABGEWIESEN);
}
Bei komplexeren Modellen bietet es sich darüber hinaus an, fake()
-Methoden für verschiedene fachliche Anwendungsfälle zu bauen. Bei der beschriebenen Bestellung könnte man etwa zwischen Filialbestellungen und Kundenbestellungen differenzieren. Diese beiden fachlich völlig verschiedenen Bestellungen unterscheiden sich ggf. durch eine Vielzahl unterschiedlicher Attribute, daher ist es sinnvoll, statt oder neben fakeBestellung()
auch gleich die Methoden fakeFilialbestellung()
und fakeKundenbestellung()
anzubieten. Der Fokus sollte hierbei immer auf der Fachlichkeit liegen, einzelne Attribute werden nach dem Builder-Pattern über entsprechende with..()
-Methoden überschrieben.
Fake-Objekte
Nehmen wir nun an, unsere Bestellung unterliegt bestimmten Restriktionen und wir können den Status nicht so einfach setzen, benötigen für einen anderen Test aber eine Bestellung im Status RETOURNIERT
. Wenn sich unser Objekt nicht über Konstruktoren und Setter nach Belieben anpassen lässt, kann man ein Fake-Objekt verwenden. Dies kann z.B. so aussehen:
public class BestellungFake extends Bestellung {
private Status status;
@Override
public Status getStatus() {
return status;
}
setStatus(Status status) {
this.status = status;
}
}
Verwaltet der Faker nun dieses BestellungFake
anstelle der Bestellung, kann er es nach außen hin als Bestellung zurückgeben, intern aber den Status nach Belieben manipulieren. Das kann dann so aussehen:
public class BestellungFaker {
private BestellungFake bestellung;
public BestellungFaker faker() {
return new BestellungFaker();
}
public BestellungFaker fakeBestellung() {
Bestellung bestellung = new BestellungFake();
bestellung.setKunde(new Kunde("Christian Pflugradt"));
bestellung.setPrioritaet(1);
return this;
}
public BestellungFaker withStatusRetourniert() {
bestellung.setStatus(Status.RETOURNIERT);
}
public BestellungFaker withBestellposition(Bestellposition position) {
bestellung.add(position);
}
public Bestellung fake() {
return bestellung;
}
}
Gemockte Abhängigkeiten mit Fakern verstecken
Nehmen wir nun noch an, unser Bestellservice hat externe Abhängigkeiten und ein Abschließen ist nicht möglich, wenn wir nicht den Bonitätscheck für den Kunden deaktivieren. Unser Test sieht dementsprechend so aus:
@Test
public void shouldRejectBestellungIfMengeIsZero() {
// given
int givenMenge = 0;
Bestellservice bestellservice = new Bestellservice();
Bestellung bestellung = BestellungFaker.faker()
.fakeBestellung()
.withBestellposition(BestellpositionFaker.faker()
.fakeBestellposition()
.withMenge(givenMenge)
.fake())
fake();
Bonitaetsservice bonitaetsservice = mock(Bonitaetsservice.class);
given(bonitaetsservice.checkKunde(bestellung.getKunde()).willReturn(true);
given(bestellservice.getBonitaetsservice()).willReturn(bonitaetsservice);
// when
bestellservice.abschliessen(bestellung);
// then
assertThat(bestellung.getStatus()).isEqualTo(Status.ABGEWIESEN);
}
Dann lässt sich auch dies in einem Faker verpacken. Der Faker für den Service sieht dann so aus:
public class BestellserviceFaker {
private Bestellservice bestellservice;
public BestellserviceFaker faker() {
return new BestellserviceFaker();
}
public BestellserviceFaker fakeBestellservice() {
Bestellservice bestellservice = mock(Bestellservice.class);
doCallRealMethod().given(bestellservice).abschliessen(any(Bestellung.class));
Bonitaetsservice bonitaetsservice = mock(Bonitaetsservice.class);
given(bestellservice.getBonitaetsservice()).willReturn(bonitaetsservice);
}
public BestellserviceFaker withPositiveBonitaet() {
given(bonitaetsservice.checkKunde(any(Kunde.class)).willReturn(true);
}
public Bestellservice fake() {
return bestellservice;
}
}
Der letzte Wurf des Unit-Tests sieht damit wie folgt aus:
public void shouldRejectBestellungIfMengeIsZero() {
// given
int givenMenge = 0;
Bestellservice bestellservice = BestellserviceFaker.faker()
.fakeBestellservice()
.withPositiveBonitaet()
.fake();
Bestellung bestellung = BestellungFaker.faker()
.fakeBestellung()
.withBestellposition(BestellpositionFaker.faker()
.fakeBestellposition()
.withMenge(givenMenge)
.fake())
fake();
// when
bestellservice.abschliessen(bestellung);
// then
assertThat(bestellung.getStatus()).isEqualTo(Status.ABGEWIESEN);
}
Faker und Fake-Objekte dienen ausschließlich dem Schreiben von Tests und gehören daher in den Testzweig. Im produktiven Code haben sie nichts verloren.
- Faker in PwMan3