Erste Schritte TDD testen einer Methode mit injezierten Services?

lam_tr

Top Contributor
Hallo zusammen,

da ich jetzt wieder umfangreicher Test schreiben muss, habe ich gleich von vorne herein eine Frage. Wenn ich eine Methode habe, die mehrere Services benutzen, wie im folgenden Code

Code:
public Result export(String exportPath){
...
Contact contact = contactService.findByName("Max", "Mustermann");
String name = userNameService.findUsernameBy(contact);
...
}

Wie kann ich den Rückgabewert der Methode contactService#findByName und userNameService#findUsernameByContact mocken? Damit die export Methode ein Ergebnis von Result zurückgibt? Bei dem Beispiel greifen die Services auf eine Datenbank und holen sich da die Werte, wie kann ich in mein Test diesen Datenbankzugriff mocken?

Kann ich das mit Mockito @mock und @InjectMock erreichen? Soweit ich es verstanden habe, kann man doch ein gemockte Klasse in ein andere gemockte Klasse verpacken oder? Wenn ich folgendes mache, würde es dann für das folgende Beispiel gehen? Was passiert denn mit der Datenbankzugriff?

Code:
@Mock
ContactRepository contactRepository

@InjectMock
ContactService contactService

Grüße
lam
 
Zuletzt bearbeitet:
Der contactService und der userNameService sind ja vermutlich private Fields in deiner Klasse.

Schritt 1:
* Es muss möglich sein, in Tests diese Felder mit Instanzen zu versehen, die nicht den Original-Instanzen entsprechen. Wie genau das am besten geht, hängt stark von der verwendeten Technologie (Spring Boot, CDI, Plain Java, etc.) ab. Die einfachste Möglichkeit ist, dass es einen Konstruktor gibt, der Instanzen dieser Klassen entgegen nimmt, Beispiel:

Java:
public class MyClassZuTesten {

public MyClassZuTesten(ContactService contactService, UserNameService userNameService) {
  this.contactService = contactService;
  this.userNameService = userNameService;
}

Dann kannst du einfach im Test mit Mockito arbeiten (hier mal alles in eine Test-Methode gepackt, in der Regel wird man den Setup-Code auslagern)

Java:
public void testMeineMethode() {
ContactService contactService = Mockito.mock(ContactService.class);
UserNameServer userNameService = Mockito.mock(UserNameService.class);

MyClassZuTesten meineKlassen = new MyClassZuTesten(contactService, userNameService);
Contact testContact = new Contact();
Mockito.when(contactService.findByName(Mockito.anyString(), Mockito.anyString()).thenReturn(testContact);
}
 
Der contactService und der userNameService sind ja vermutlich private Fields in deiner Klasse.

Schritt 1:
* Es muss möglich sein, in Tests diese Felder mit Instanzen zu versehen, die nicht den Original-Instanzen entsprechen. Wie genau das am besten geht, hängt stark von der verwendeten Technologie (Spring Boot, CDI, Plain Java, etc.) ab. Die einfachste Möglichkeit ist, dass es einen Konstruktor gibt, der Instanzen dieser Klassen entgegen nimmt, Beispiel:

Java:
public class MyClassZuTesten {

public MyClassZuTesten(ContactService contactService, UserNameService userNameService) {
  this.contactService = contactService;
  this.userNameService = userNameService;
}

Dann kannst du einfach im Test mit Mockito arbeiten (hier mal alles in eine Test-Methode gepackt, in der Regel wird man den Setup-Code auslagern)

Java:
public void testMeineMethode() {
ContactService contactService = Mockito.mock(ContactService.class);
UserNameServer userNameService = Mockito.mock(UserNameService.class);

MyClassZuTesten meineKlassen = new MyClassZuTesten(contactService, userNameService);
Contact testContact = new Contact();
Mockito.when(contactService.findByName(Mockito.anyString(), Mockito.anyString()).thenReturn(testContact);
}
Und was ist wenn ich die Services meinen Exporter nicht als Konstruktor Parameter übergeben kann? Außerhalb der Klasse zu mocken und nicht zu übergeben ist nicht möglich? Die Services werden über Dependency Injection geholt, aber ich will die Datenbankzugriffe vermeiden.

Meine TestKlasse muss irgendwie die zwei Instanzen der Services kennen. Aber wie weiß ich noch nicht.
 
Das hängt von deinem Framework ab. Du musst im Test dafür sorgen, dass Dependency Injection Framework bei der Anfrage die zwei Testinstanzen liefert und nicht die normalen. Das ist Framework spezifisch.

Man kann für sowas aber oft auch einen protected oder package private Konstruktor vorsehen (neben dem Default-Konstruktor für das DI), der die übergeben bekommt. Hängt aber auch davon ab, wie man den zu testenden Service im Test instanziert.
 
Ich benutze hier Google Guice für DI und auch sozusagen mein Problem, da ich nicht genau weiß wie ich meine zwei Services der Testklasse reichen kann.

Macht man extra einen neuen Konstruktor nur für Tests?
 
Ja. Aber wie das mit deinem DI Framework zusammenarbeitet ist die Frage.

Du hast dann zwei Sachen, die per Reflection Felder setzen - Mockito und das DI Framework. Wer gewinnt, kann ich dir aus dem Bauch heraus nicht sagen.

Wir diskutieren hier auf einer sehr abstrakten Ebene und ich hab das Gefühl, du weißt noch gar nicht genau, was Mockito eigentlich macht.

Vielleicht würde ein konkretes Beispiel von dir mal helfen um das etwas konkreter zu machen. Den mir ist aktuell absolut unklar, wie du im Test dein Objekt konkret erzeugt. Per new kann es ja nicht sein, sonst würde das DI-Framework nicht greifen.
 
Ja. Aber wie das mit deinem DI Framework zusammenarbeitet ist die Frage.

Du hast dann zwei Sachen, die per Reflection Felder setzen - Mockito und das DI Framework. Wer gewinnt, kann ich dir aus dem Bauch heraus nicht sagen.

Wir diskutieren hier auf einer sehr abstrakten Ebene und ich hab das Gefühl, du weißt noch gar nicht genau, was Mockito eigentlich macht.

Vielleicht würde ein konkretes Beispiel von dir mal helfen um das etwas konkreter zu machen. Den mir ist aktuell absolut unklar, wie du im Test dein Objekt konkret erzeugt. Per new kann es ja nicht sein, sonst würde das DI-Framework nicht greifen.
🙂 Ja dein Gefühl hat dir das richtige gesagt, ich bin sehr neu in Mockito und versuche es anhand eines praktischen Test zu verstehen.

Ich schau mal ob ich das abgebildet bekomme, dann stelle ich den Code vor.

Danke.
 
Kleine Anmerkung am Rande, TDD ist das nicht. Bei Test Driven Development würdest du zuerst die Tests schreiben - und durch die Tests gibst du eine API vor, so dass die Klasse testbar ist. Wenn du schon Code hast und nachträglich Tests schreibst, ist es nicht mehr test-driven 🙂
 
Ja danke für die Begrifflichkeiten 🙂

Eine weitere Frage. Ich weiss nicht wie schön dass ist, aber an sich kann ich anstatt die Services im Konstruktor zu übergeben auch direkt als Feld initialisieren?

Code:
@Mock ContactListExporter underTest;

public void testMeineMethode() {
underTest.contactService = mock(IContactService.class);
underTest.usernameService = mock(IUsernameService.class);
underTest.export();
}

So lässt es auch machen, aber wäre diese Weg in Ordnung?
 
Naja, du müsstest dafür ein nicht-privates, veränderliches Feld haben. Fände sich sehr unschön, vor allem bietet es ja auch keinerlei Vorteile gegenüber einem vernünftigem Konstruktor, nur Nachteile.


Abgesehen davon willst du doch die Klasse unter Test eben nicht Mocken – Testen willst du doch die echte Implementation deiner Klasse.
 

Zurück
Oben