Java >> Java Tutorial >  >> Java

Schreiben von Tests für Datenzugriffscode - Komponententests sind Verschwendung

Vor einigen Jahren war ich einer jener Entwickler, die Unit-Tests für meinen Datenzugriffscode schreiben. Ich habe alles isoliert getestet und war ziemlich zufrieden mit mir. Ich dachte ehrlich, dass ich einen guten Job mache.

Oh Mann, habe ich mich geirrt!

Dieser Blogbeitrag beschreibt, warum wir keine Unit-Tests für unseren Datenzugriffscode schreiben sollten, und erklärt, warum wir Unit-Tests durch Integrationstests ersetzen sollten.

Fangen wir an.

Unit-Tests-Antworten auf die falsche Frage

Wir schreiben Tests für unseren Datenzugriffscode, weil wir wissen wollen, ob er wie erwartet funktioniert. Mit anderen Worten, wir wollen die Antworten auf diese Fragen finden:

  1. Werden die richtigen Daten in der verwendeten Datenbank gespeichert?
  2. Ergibt unsere Datenbankabfrage die richtigen Daten?

Können Unit-Tests uns dabei helfen, die gesuchten Antworten zu finden?

Nun, eine der grundlegendsten Regeln für Unit-Tests ist, dass Unit-Tests keine externen Systeme wie eine Datenbank verwenden sollten . Diese Regel ist für die vorliegende Situation nicht geeignet, da die Verantwortung für die Speicherung korrekter Informationen und die Rückgabe korrekter Abfrageergebnisse zwischen unserem Datenzugriffscode und der verwendeten Datenbank aufgeteilt ist.

Wenn unsere Anwendung beispielsweise eine einzelne Datenbankabfrage ausführt, ist die Verantwortung wie folgt aufgeteilt:

  • Der Datenzugriffscode, der für die Erstellung der ausgeführten Datenbankabfrage verantwortlich ist.
  • Die Datenbank ist für die Ausführung der Datenbankabfrage und die Rückgabe der Abfrageergebnisse an den Datenzugriffscode verantwortlich.

Die Sache ist die, dass wir, wenn wir unseren Datenzugriffscode von der Datenbank isolieren, testen können, ob unser Datenzugriffscode die "richtige" Abfrage erstellt, aber wir können nicht sicherstellen, dass die erstellte Abfrage die richtigen Abfrageergebnisse zurückgibt.

Aus diesem Grund können uns Unit-Tests nicht dabei helfen, die gesuchten Antworten zu finden .

Eine warnende Geschichte:Spott ist Teil des Problems

Es gab eine Zeit, in der ich Komponententests für meinen Datenzugriffscode geschrieben habe. Damals hatte ich zwei Regeln:

  1. Jeder Teil des Codes muss isoliert getestet werden.
  2. Lassen Sie uns Mocks verwenden.

Ich habe in einem Projekt gearbeitet, in dem Spring Data JPA verwendet wurde, und dynamische Abfragen wurden mithilfe von JPA-Kriterienabfragen erstellt.

Wie auch immer, ich habe eine Spezifikationserstellungsklasse erstellt, die Specification erstellt Objekte. Nachdem ich eine Spezifikation erstellt hatte -Objekt habe ich es an mein Spring Data JPA-Repository weitergeleitet, das die Abfrage ausgeführt und die Abfrageergebnisse zurückgegeben hat.

Der Quellcode der Spezifikationserstellungsklasse sieht wie folgt aus:

import org.springframework.data.jpa.domain.Specification;
  
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
  
public class PersonSpecifications {
 
    public static Specification<Person> lastNameIsLike(final String searchTerm) {
          
        return new Specification<Person>() {
            @Override
            public Predicate toPredicate(Root<Person> personRoot, 
									CriteriaQuery<?> query, 
									CriteriaBuilder cb) {
                String likePattern = getLikePattern(searchTerm);              
                return cb.like(cb.lower(personRoot.<String>get(Person_.lastName)), likePattern);
            }
              
            private String getLikePattern(final String searchTerm) {
                return searchTerm.toLowerCase() + "%";
            }
        };
    }
}

Werfen wir einen Blick auf den Testcode, der „verifiziert“, dass die Spezifikationserstellungsklasse „die richtige“ Abfrage erstellt. Denken Sie daran, dass ich diese Testklasse nach meinen eigenen Regeln geschrieben habe, was bedeutet, dass das Ergebnis großartig sein sollte.

Der Quellcode des PersonSpecificationsTest Klasse sieht wie folgt aus:

import org.junit.Before;
import org.junit.Test;
import org.springframework.data.jpa.domain.Specification;
 
import javax.persistence.criteria.*;
 
import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.*;
 
public class PersonSpecificationsTest {
     
    private static final String SEARCH_TERM = "Foo";
    private static final String SEARCH_TERM_LIKE_PATTERN = "foo%";
     
    private CriteriaBuilder criteriaBuilderMock;
     
    private CriteriaQuery criteriaQueryMock;
     
    private Root<Person> personRootMock;
 
    @Before
    public void setUp() {
        criteriaBuilderMock = mock(CriteriaBuilder.class);
        criteriaQueryMock = mock(CriteriaQuery.class);
        personRootMock = mock(Root.class);
    }
 
    @Test
    public void lastNameIsLike() {
        Path lastNamePathMock = mock(Path.class);       
        when(personRootMock.get(Person_.lastName)).thenReturn(lastNamePathMock);
         
        Expression lastNameToLowerExpressionMock = mock(Expression.class);
        when(criteriaBuilderMock.lower(lastNamePathMock)).thenReturn(lastNameToLowerExpressionMock);
         
        Predicate lastNameIsLikePredicateMock = mock(Predicate.class);
        when(criteriaBuilderMock.like(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN)).thenReturn(lastNameIsLikePredicateMock);
 
        Specification<Person> actual = PersonSpecifications.lastNameIsLike(SEARCH_TERM);
        Predicate actualPredicate = actual.toPredicate(personRootMock, criteriaQueryMock, criteriaBuilderMock);
         
        verify(personRootMock, times(1)).get(Person_.lastName);
        verifyNoMoreInteractions(personRootMock);
         
        verify(criteriaBuilderMock, times(1)).lower(lastNamePathMock);
        verify(criteriaBuilderMock, times(1)).like(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN);
        verifyNoMoreInteractions(criteriaBuilderMock);
 
        verifyZeroInteractions(criteriaQueryMock, lastNamePathMock, lastNameIsLikePredicateMock);
 
        assertEquals(lastNameIsLikePredicateMock, actualPredicate);
    }
}

Ist das sinnvoll?

NEIN!

Ich muss zugeben, dass dieser Test ein Stück Scheiße ist, das für niemanden einen Wert hat und so schnell wie möglich gelöscht werden sollte. Dieser Test hat drei Hauptprobleme:

  • Es hilft uns nicht sicherzustellen, dass die Datenbankabfrage die richtigen Ergebnisse zurückgibt.
  • Es ist schwer zu lesen und um die Sache noch schlimmer zu machen, es beschreibt, wie die Abfrage aufgebaut ist, aber es beschreibt nicht, was sie zurückgeben soll.
  • Solche Tests sind schwer zu schreiben und zu pflegen.

Die Wahrheit ist, dass dieser Komponententest ein Lehrbuchbeispiel für einen Test ist, der niemals hätte geschrieben werden sollen. Es hat keinen Wert für uns, aber wir müssen es trotzdem pflegen. Somit ist es Verschwendung!

Und doch passiert genau das, wenn wir Unit-Tests für unseren Datenzugriffscode schreiben. Am Ende haben wir eine Testsuite, die nicht die richtigen Dinge testet.

Datenzugriffstests richtig gemacht

Ich bin ein großer Fan von Komponententests, aber es gibt Situationen, in denen es nicht das beste Werkzeug für den Job ist. Dies ist eine dieser Situationen.

Der Datenzugriffscode hat eine sehr starke Beziehung zum verwendeten Datenspeicher. Diese Beziehung ist so stark, dass der Datenzugriffscode selbst ohne die Datenspeicherung nicht nützlich ist. Deshalb macht es keinen Sinn, unseren Datenzugriffscode von der verwendeten Datenspeicherung zu isolieren.

Die Lösung für dieses Problem ist einfach.

Wenn wir umfassende Tests für unseren Datenzugriffscode schreiben wollen, müssen wir unseren Datenzugriffscode zusammen mit dem verwendeten Datenspeicher testen. Das bedeutet, dass wir Unit-Tests vergessen und mit dem Schreiben von Integrationstests beginnen müssen .

Wir müssen verstehen, dass nur Integrationstests dies verifizieren können

  • Unser Datenzugriffscode erstellt die richtigen Datenbankabfragen.
  • Unsere Datenbank gibt die korrekten Abfrageergebnisse zurück.

Zusammenfassung

Dieser Blogpost hat uns zwei Dinge gelehrt:

  • Wir haben festgestellt, dass Einheitentests uns nicht helfen können, zu überprüfen, ob unser Datenzugriffscode ordnungsgemäß funktioniert, da wir nicht sicherstellen können, dass die richtigen Daten in unseren Datenspeicher eingefügt werden oder dass unsere Abfragen die richtigen Ergebnisse zurückgeben.
  • Wir haben gelernt, dass wir unseren Datenzugriffscode durch Integrationstests testen sollten, weil die Beziehung zwischen unserem Datenzugriffscode und dem verwendeten Datenspeicher so eng ist, dass es keinen Sinn macht, sie zu trennen.

Bleibt nur noch eine Frage:

Schreiben Sie immer noch Komponententests für Ihren Datenzugriffscode?


Java-Tag