Java >> Java Tutorial >  >> Java

Saubere Tests schreiben – es kommt auf die Benennung an

Wenn wir automatisierte Tests für unsere Anwendung schreiben, müssen wir unsere Testklassen, unsere Testmethoden, Felder unserer Testklassen und die von unseren Testmethoden gefundenen lokalen Variablen benennen.

Wenn wir Tests schreiben wollen, die gut lesbar sind, müssen wir aufhören, auf Autopilot zu programmieren, und auf die Benennung achten.

Das ist leichter gesagt als getan .

Aus diesem Grund habe ich mich entschlossen, einen Blogbeitrag zu schreiben, der die Probleme identifiziert, die durch schlechte Namensgebung verursacht werden, und Lösungen für diese Probleme bietet.

Der Teufel steckt im Detail

Es ist relativ einfach, Tests zu schreiben, die sauber erscheinen. Wenn wir jedoch noch einen Schritt weiter gehen und unsere Tests in eine ausführbare Spezifikation umwandeln wollen, müssen wir besonders auf die Benennung von Testklassen, Testmethoden, Testklassenfeldern und lokalen Variablen achten.

Lassen Sie uns herausfinden, was das bedeutet.

Testklassen benennen

Wenn wir an die verschiedenen Testklassen denken, die wir in einem typischen Projekt erstellen, stellen wir fest, dass diese Klassen in zwei Gruppen unterteilt werden können:

  • Die erste Gruppe enthält Tests, die die Methoden einer einzelnen Klasse testen. Diese Tests können entweder Einheitentests oder Integrationstests sein, die für unsere Repositories geschrieben wurden.
  • Die zweite Gruppe enthält Integrationstests, die sicherstellen, dass eine einzelne Funktion ordnungsgemäß funktioniert.

Ein guter Name identifiziert die getestete Klasse oder Funktion. Mit anderen Worten, wir sollten unsere Testklassen nach diesen Regeln benennen:

  1. Wenn die Testklasse zur ersten Gruppe gehört, sollten wir sie mit dieser Formel benennen:[Der Name der getesteten Klasse]Test . Zum Beispiel, wenn wir Tests für den RepositoryUserService schreiben Klasse, sollte der Name unserer Testklasse sein:RepositoryUserServiceTest . Der Vorteil dieses Ansatzes besteht darin, dass uns diese Regel hilft, herauszufinden, welche Klasse fehlerhaft ist, ohne den Testcode zu lesen, wenn ein Test fehlschlägt.
  2. Wenn die Klasse zur zweiten Gruppe gehört, sollten wir sie mit dieser Formel benennen:[Der Name des getesteten Features]Test . Wenn wir beispielsweise Tests für die Registrierungsfunktion schreiben würden, sollte der Name unserer Testklasse RegistrationTest lauten . Die Idee hinter dieser Regel ist, dass, wenn ein Test fehlschlägt, uns die Verwendung dieser Namenskonvention hilft, herauszufinden, welche Funktion defekt ist, ohne den Testcode zu lesen.

Testmethoden benennen

Ich bin ein großer Fan der von Roy Osherove eingeführten Namenskonvention. Seine Idee ist es, die getestete Methode (oder das getestete Feature), die erwartete Eingabe oder den erwarteten Zustand und das erwartete Verhalten im Namen einer Testmethode zu beschreiben.

Mit anderen Worten, wenn wir dieser Namenskonvention folgen, sollten wir unsere Testmethoden wie folgt benennen:

  1. Wenn wir Tests für eine einzelne Klasse schreiben, sollten wir unsere Testmethoden nach dieser Formel benennen:[Name der getesteten Methode]_[erwartete Eingabe / getesteter Zustand]_[erwartetes Verhalten] . Zum Beispiel, wenn wir einen Einheitentest für ein registerNewUserAccount() schreiben -Methode, die eine Ausnahme auslöst, wenn die angegebene E-Mail-Adresse bereits mit einem bestehenden Benutzerkonto verknüpft ist, sollten wir unsere Testmethode wie folgt benennen:registerNewUserAccount_ExistingEmailAddressGiven_ShouldThrowException() .
  2. Wenn wir Tests für eine einzelne Funktion schreiben, sollten wir unsere Testmethoden anhand dieser Formel benennen:[Name der getesteten Funktion]_[erwartete Eingabe/getesteter Zustand]_[erwartetes Verhalten] . Wenn wir beispielsweise einen Integrationstest schreiben, der testet, dass eine Fehlermeldung angezeigt wird, wenn ein Benutzer versucht, ein neues Benutzerkonto zu erstellen, indem er eine E-Mail-Adresse verwendet, die bereits mit einem bestehenden Benutzerkonto verknüpft ist, sollten wir die Testmethode wie folgt benennen :registerNewUserAccount_ExistingEmailAddressGiven_ShouldShowErrorMessage() .

Diese Namenskonvention stellt Folgendes sicher:

  • Der Name einer Testmethode beschreibt eine bestimmte geschäftliche oder technische Anforderung.
  • Der Name einer Testmethode beschreibt die erwartete Eingabe (oder den Zustand) und das erwartete Ergebnis für diese Eingabe (den Zustand).

Mit anderen Worten, wenn wir dieser Namenskonvention folgen, können wir die folgenden Fragen beantworten, ohne den Code unserer Testmethoden zu lesen:

  • Was sind die Funktionen unserer Anwendung?
  • Was ist das erwartete Verhalten einer Funktion oder Methode, wenn sie eine Eingabe X empfängt?

Wenn ein Test fehlschlägt, haben wir außerdem eine ziemlich gute Vorstellung davon, was falsch ist, bevor wir den Quellcode des fehlgeschlagenen Tests lesen.

Ziemlich cool, oder?

Felder der Testklasse benennen

Eine Testklasse kann die folgenden Felder haben:

  • Felder, die Test enthalten, verdoppeln solche Mocks oder Stubs.
  • Ein Feld, das einen Verweis auf das getestete Objekt enthält.
  • Felder, die die anderen Objekte (Testwerkzeuge) enthalten, die in unseren Testfällen verwendet werden.

Wir sollten diese Felder benennen, indem wir dieselben Regeln verwenden, die wir verwenden, wenn wir die Felder benennen, die aus dem Anwendungscode gefunden werden. Mit anderen Worten, der Name jedes Felds sollte den "Zweck" des Objekts beschreiben, das in diesem Feld gespeichert ist.

Diese Regel klingt ziemlich "einfach" (Benennen ist immer schwierig), und es war für mich einfach, dieser Regel zu folgen, wenn ich die getestete Klasse und die anderen Klassen benenne, die für meine Tests verwendet werden. Zum Beispiel, wenn ich einen TodoCrudService hinzufügen muss Feld zu meiner Testklasse verwende ich den Namen crudService .

Wenn ich meiner Testklasse Felder hinzugefügt habe, die Testdoubles enthalten, habe ich normalerweise den Typ des Testdoubles am Ende des Feldnamens hinzugefügt. Zum Beispiel, wenn ich einen TodoCrudService hinzugefügt habe Mock für meine Testklasse habe ich den Namen crudServiceMock verwendet .

Es klingt wie eine gute Idee, aber ich bin zu dem Schluss gekommen, dass es ein Fehler ist. Es ist kein großes Problem, aber die Sache ist, dass ein Feldname den "Zweck" des Feldes beschreiben sollte, nicht seinen Typ. Daher sollten wir den Typ des Testdoubles nicht zum Feldnamen hinzufügen.

Lokale Variablen benennen

Wenn wir die in unseren Testmethoden verwendeten lokalen Variablen benennen, sollten wir die gleichen Prinzipien befolgen, die bei der Benennung der Variablen aus unserem Anwendungscode verwendet werden.

Die wichtigsten Regeln sind meiner Meinung nach:

  • Beschreiben Sie die Bedeutung der Variablen. Eine gute Faustregel ist, dass der Variablenname den Inhalt der Variablen beschreiben muss.
  • Verwenden Sie keine abgekürzten Namen, die für niemanden offensichtlich sind. Verkürzte Namen verringern die Lesbarkeit und oft bringt Ihnen die Verwendung nichts.
  • Verwenden Sie keine generischen Namen wie dto , modelObject , oder Daten .
  • Sei konsequent. Beachten Sie die Namenskonventionen der verwendeten Programmiersprache. Wenn Ihr Projekt eigene Namenskonventionen hat, sollten Sie diese ebenfalls respektieren.

Genug der Theorie. Lassen Sie uns diese Lektionen in die Praxis umsetzen.

Theorie in die Praxis umsetzen

Werfen wir einen Blick auf einen modifizierten Komponententest (ich habe ihn noch schlimmer gemacht), der aus der Beispielanwendung meines Spring Social-Tutorials stammt.

Dieser Komponententest wurde geschrieben, um registerNewUserAccount() zu testen Methode des RepositoryUserService Klasse und überprüft, ob diese Methode ordnungsgemäß funktioniert, wenn ein neues Benutzerkonto erstellt wird, indem ein Anbieter für soziale Zeichen und eine eindeutige E-Mail-Adresse verwendet wird.

Der Quellcode unserer Testklasse sieht wie folgt aus:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

    private RepositoryUserService service;

    @Mock
    private PasswordEncoder passwordEncoderMock;

    @Mock
    private UserRepository repositoryMock;

    @Before
    public void setUp() {
        service = new RepositoryUserService(passwordEncoderMock, repositoryMock);
    }


    @Test
    public void registerNewUserAccountByUsingSocialSignIn() throws DuplicateEmailException {
        RegistrationForm form = new RegistrationForm();
        form.setEmail("[email protected]");
        form.setFirstName("John");
        form.setLastName("Smith");
        form.setSignInProvider(SocialMediaService.TWITTER);

        when(repositoryMock.findByEmail("[email protected]")).thenReturn(null);
        
        when(repositoryMock.save(isA(User.class))).thenAnswer(new Answer<User>() {
            @Override
            public User answer(InvocationOnMock invocation) throws Throwable {
                Object[] arguments = invocation.getArguments();
                return (User) arguments[0];
            }
        });

        User modelObject = service.registerNewUserAccount(form);

        assertEquals("[email protected]", modelObject.getEmail());
        assertEquals("John", modelObject.getFirstName());
        assertEquals("Smith", modelObject.getLastName());
        assertEquals(SocialMediaService.TWITTER, modelObject.getSignInProvider());
        assertEquals(Role.ROLE_USER, modelObject.getRole());
        assertNull(modelObject.getPassword());

        verify(repositoryMock, times(1)).findByEmail("[email protected]");
        verify(repositoryMock, times(1)).save(modelObject);
        verifyNoMoreInteractions(repositoryMock);
        verifyZeroInteractions(passwordEncoderMock);
    }
}

Dieser Komponententest hat ziemlich viele Probleme:

  • Die Feldnamen sind ziemlich generisch und beschreiben die Typen der Testdoubles.
  • Der Name der Testmethode ist „ziemlich gut“, aber er beschreibt weder die gegebene Eingabe noch das erwartete Verhalten.
  • Die in der Testmethode verwendeten Variablennamen sind schrecklich.

Wir können die Lesbarkeit dieses Komponententests verbessern, indem wir die folgenden Änderungen daran vornehmen:

  1. Ändern Sie den Namen des RepositoryUserService Feld an registrationService (Der Name der Serviceklasse ist ein bisschen schlecht, aber ignorieren wir das).
  2. Entfernen Sie das Wort "mock" aus Feldnamen des PasswordEncoder und UserRepository Felder.
  3. Ändern Sie den Namen der Testmethode zu:registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() .
  4. Ändern Sie den Namen des Formulars Variable zur Registrierung .
  5. Ändern Sie den Namen des modelObject Variable zu createdUserAccount .

Der Quellcode unseres "modifizierten" Unit-Tests sieht wie folgt aus:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;


@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

    private RepositoryUserService registrationService;

    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private UserRepository repository;

    @Before
    public void setUp() {
        registrationService = new RepositoryUserService(passwordEncoder, repository);
    }


    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationForm();
        registration.setEmail("[email protected]");
        registration.setFirstName("John");
        registration.setLastName("Smith");
        registration.setSignInProvider(SocialMediaService.TWITTER);

        when(repository.findByEmail("[email protected]")).thenReturn(null);

        when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
            @Override
            public User answer(InvocationOnMock invocation) throws Throwable {
                Object[] arguments = invocation.getArguments();
                return (User) arguments[0];
            }
        });

        User createdUserAccount = registrationService.registerNewUserAccount(registration);

        assertEquals("[email protected]", createdUserAccount.getEmail());
        assertEquals("John", createdUserAccount.getFirstName());
        assertEquals("Smith", createdUserAccount.getLastName());
        assertEquals(SocialMediaService.TWITTER, createdUserAccount.getSignInProvider());
        assertEquals(Role.ROLE_USER, createdUserAccount.getRole());
        assertNull(createdUserAccount.getPassword());

        verify(repository, times(1)).findByEmail("[email protected]");
        verify(repository, times(1)).save(createdUserAccount);
        verifyNoMoreInteractions(repository);
        verifyZeroInteractions(passwordEncoder);
    }
}

Es ist klar, dass dieser Testfall noch einige Probleme hat, aber ich denke, dass unsere Änderungen seine Lesbarkeit verbessert haben. Ich denke, dass die dramatischsten Verbesserungen sind:

  1. Der Name der Testmethode beschreibt das erwartete Verhalten der getesteten Methode, wenn ein neues Benutzerkonto erstellt wird, indem ein Social-Sign-in-Anbieter und eine eindeutige E-Mail-Adresse verwendet werden. Die einzige Möglichkeit, diese Informationen aus dem „alten“ Testfall zu erhalten, bestand darin, den Quellcode der Testmethode zu lesen. Dies ist offensichtlich viel langsamer, als nur den Methodennamen zu lesen. Mit anderen Worten, Testmethoden gute Namen zu geben, spart Zeit und hilft uns, einen schnellen Überblick über die Anforderungen der getesteten Methode oder Funktion zu bekommen.
  2. Die anderen Änderungen verwandelten einen generischen CRUD-Test in einen "Anwendungsfall". Das "neue" Testverfahren beschreibt eindeutig
    1. Welche Schritte hat dieser Anwendungsfall.
    2. Was das registerNewUserAccount() -Methode zurück, wenn sie eine Registrierung erhält, die über einen Social-Sign-In-Anbieter erfolgt und eine eindeutige E-Mail-Adresse hat.

    Das hat der "alte" Testfall meiner Meinung nach nicht geschafft.

Zusammenfassung

Wir haben nun gelernt, dass sich die Benennung enorm positiv auf die Lesbarkeit unserer Testfälle auswirken kann. Außerdem haben wir ein paar Grundregeln gelernt, die uns helfen, unsere Testfälle in ausführbare Spezifikationen umzuwandeln.

Unser Testfall weist jedoch noch einige Probleme auf. Diese Probleme sind:

  • Der Testfall verwendet magische Zahlen. Wir können es verbessern, indem wir diese magischen Zahlen durch Konstanten ersetzen.
  • Der Code, der ein neues RegistrationForm erstellt objects setzt einfach die Eigenschaftswerte des erstellten Objekts. Wir können diesen Code verbessern, indem wir Test Data Builder verwenden.
  • Die standardmäßigen JUnit-Assertionen, die überprüfen, ob die Informationen des zurückgegebenen Benutzers Objekt korrekt ist, sind nicht sehr gut lesbar. Ein weiteres Problem besteht darin, dass sie nur die Eigenschaftswerte des zurückgegebenen Benutzers überprüfen Objekt sind richtig. Wir können diesen Code verbessern, indem wir Behauptungen in eine domänenspezifische Sprache umwandeln.

Ich werde diese Techniken in Zukunft beschreiben.

In der Zwischenzeit würde ich gerne hören, welche Art von Namenskonventionen Sie verwenden.


Java-Tag