DAO Unit Tests mit JUnit und Mockito

If it’s not tested, it’s broken. – Bruce Eckel

Stabile software ist mir sehr wichtig. Als Teil der Softwareindustrie möchte ich nur software herstellen, die auch möglichst zuverlässig funktioniert. Sollte eine Software abstürzen (oder noch schlimmer: Daten verlieren), erzeugt das beim Anwender ein so großes Misstrauen, dass er die Software nicht mehr verwendet. Wenn man sich selbst die Frage stellt: Würde ich eine Software einsetzen wollen, die abstürzt? Ich nicht. Daher ist die Qualitätssicherung und insbesondere der Softwaretest, um Fehler vor dem Endnutzer zu finden, der wichtigste Schritt im Softwareentwicklungsprozess.

Nun stellt sich die Frage: Wie testet man Software? Wie immer gibt es hier auf Wikipedia detaillierte Beschreibungen zu verschiedenen Arten von Tests. Auch gibt es diverse Blogs, Tutorials und Ähnliches überall im Internet. Es wird also schnell deutlich, dass es verschiedenste Arten gibt, die Qualität der Software sicherzustellen. In diesem Artikel beziehen wir uns auf den Teil der Softwareentwicklung, die Hauptsächlich durch Entwickler durchgeführt wird: Unit Tests. Wie der Name suggeriert, wird dabei eine Unit, also eine Einheit der Software geprüft. Wie genau eine Einheit definiert ist, kann von Anwendungsfall, Art der Software und sogar der Programmiersprache abhängig sein. In diesem Beispiel verwenden wir Java Klassen und ihre Methodem als Einheit. Wir starten also mit folgender trivialen Klasse, dem SomeDao:

package de.egore911.test;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.Id;

public class SomeDao {
    public static class Some {
        @Id public Integer id;
    }

    @Inject private EntityManager em;

    protected EntityManager getEntityManager() { return em; }

    public Some comeGetSome(Integer id) {
        return getEntityManager().find(Some.class, id);
    }
}

Wir haben ein DAO (Data Access Object) das eine Entität des Typs *Some anhand ihrer ID aus der Datenbank laden kann. Die Klasse selbst verwendet CDI um eine EntityManager Instanz zu beziehen. Dieses Muster ist typisch für diverse Webanwendungen und es wird in verschiedsten Projekten so verwendet. Aber wie testet am so eine Einheit? Auf den ersten Blick brauchen wir also ein Injection Framework, das wiederum eine Implementierung der EntityManager-JPA-Spezifiktion benötigt und diese wiederum benötig eine Datenbank, gefüllt mit Testdaten. Diese Abhängigkeiten könnten selbst Fehler erzeugen (z.B. wenn die Datenbank nicht verfügbar ist), und dürfen daher nicht Teil eines Unit-Tests sein. Eine Einheit muss abgeschlossen sein und externe Abhängigkeiten gemockt. Ein Mock ist dabei eine Blackbox, die zu definierten Eingaben immer die gleichen Ausgaben liefert. Wir benötigen also einen Mock des EntityManager, der selbst nicht durch ein Framework injeziert wird und selbst gar nicht auf eine Datenbank zugreifen muss. Dafür gibt es Frameworks wie Mockito um einen solchen zu erstellen.

import javax.persistence.EntityManager;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class SomeDaoTest {

    @Test
    public void testComeGetSome() {
        // Gegeben: Ein Mock des EntityManager der unseren Dummy zurückgibt
        Some dummy = new Some();
        EntityManager em = Mockito.mock(EntityManager.class);
        Mockito.when(em.find(Some.class, 1234)).thenReturn(dummy);

        // Ebenfalls gegeben: Unser gemocktes SomeDao mit dem gemockten
        // EntityManager, dass nur bestimmte Aufrufe auf comeGetSome beantwortet
        // allowing only the call to comeGetSome
        SomeDao someDao = Mockito.mock(SomeDao.class);
        Mockito.when(someDao.comeGetSome(1234)).thenCallRealMethod();
        Mockito.when(someDao.getEntityManager()).thenReturn(em);

        // Der eigentliche Test-Code
        Assertions.assertSame(dummy, someDao.comeGetSome(1234));
        Assertions.assertNull(someDao.comeGetSome(4321));
    }
}

Das Beispiel ist bewusst extrem einfach gehalten und der eigentliche Test-Code ergibt daher wenig Sinn: wir testen nur unseren Mock (weil unser DAO selbst keine Logik besitzt). Sobald das DAO allerdings Logik benötigt (z.B. das Validieren von Parametern oder die Verwendung der Criteria API), kann diese Art des Tests einen Mehrwert bringen. Zuerst mocken wir den EntityManager, der das Dummy-Objekt zurückgibt, wenn seine EntityManager.find()-Method aufgerufen wird. Anschließend sorgen wir dafür, dass der gemockte EntityManager von SomeDao.getEntityManager() zurückgegeben wird, was im Fall von der echten SomeDao.comeGetSome()-Methode der Fall ist is invoked. Selbstverständlich könnten wir das alles mit Reflection selbst machen, aber Mockito übernimmt hier die harte Arbeit für uns.

Das obige Bespiel kan sogar noch vereinfacht werden, wenn man Mockito die Dependency Injection überlässt. Das folgende Beispiel zeigt eine vergleichbare Implementation auf Basis der InjectMocks-Annotation.

import javax.persistence.EntityManager;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class SomeDaoTest {

    @Mock
    private EntityManager em;

    @InjectMocks
    SomeDao someDao;

    @Test
    public void testComeGetSome() {
        // Given: Tell the mocked entitymanager to return our dummy element
        Some dummy = new Some();
        Mockito.when(em.find(Some.class, 1234)).thenReturn(dummy);
        Mockito.when(em.find(Some.class, Mockito.any(Integer.class))).thenReturn(null);

        // Perform the actual test
        Assertions.assertSame(dummy, someDao.comeGetSome(1234));
        Assertions.assertNull(someDao.comeGetSome(4321));
    }
}
Copyright © christophbrill.de, 2002-2020.