Java >> Java Tutorial >  >> Java

Saubere Tests schreiben – verifizieren oder nicht verifizieren

Wenn wir Einheitentests schreiben, die Scheinobjekte verwenden, gehen wir folgendermaßen vor:

  1. Konfigurieren Sie das Verhalten unserer Scheinobjekte.
  2. Rufen Sie die getestete Methode auf.
  3. Überprüfen Sie, ob die richtigen Methoden unserer Scheinobjekte aufgerufen wurden.

Die Beschreibung des dritten Schritts ist eigentlich etwas irreführend, weil wir am Ende oft verifizieren, dass die richtigen Methoden aufgerufen wurden UND dass die anderen Methoden unserer Mock-Objekte nicht aufgerufen wurden.

Und jeder weiß, dass wir, wenn wir fehlerfreie Software schreiben wollen, beides überprüfen müssen, sonst passieren schlimme Dinge.

Richtig?

Lassen Sie uns alles überprüfen

Beginnen wir mit einem Blick auf die Implementierung einer Dienstmethode, die zum Hinzufügen neuer Benutzerkonten zur Datenbank verwendet wird.

Die Anforderungen dieser Dienstmethode sind:

  • Wenn die E-Mail-Adresse des registrierten Benutzerkontos nicht eindeutig ist, muss unsere Servicemethode eine Ausnahme auslösen.
  • Wenn das registrierte Benutzerkonto eine eindeutige E-Mail-Adresse hat, muss unsere Servicemethode der Datenbank ein neues Benutzerkonto hinzufügen.
  • Wenn das registrierte Benutzerkonto eine eindeutige E-Mail-Adresse hat und es durch normale Anmeldung erstellt wird, muss unsere Servicemethode das Passwort des Benutzers verschlüsseln, bevor es in der Datenbank gespeichert wird.
  • Wenn das registrierte Benutzerkonto eine eindeutige E-Mail-Adresse hat und es durch die Verwendung von Social Sign-In erstellt wird, muss unsere Servicemethode den verwendeten Social-Sign-In-Anbieter speichern.
  • Ein Benutzerkonto, das mithilfe der sozialen Anmeldung erstellt wurde, darf kein Passwort haben.
  • Unsere Servicemethode muss die Informationen des erstellten Benutzerkontos zurückgeben.

Diese Dienstmethode wird folgendermaßen implementiert:

  1. Die Dienstmethode prüft, ob die vom Benutzer angegebene E-Mail-Adresse nicht in der Datenbank gefunden wird. Dazu wird findByEmail() aufgerufen Methode des UserRepository Schnittstelle.
  2. Wenn der Benutzer Objekt gefunden wird, löst die Dienstmethode Methode eine DuplicateEmailException aus .
  3. Es erstellt einen neuen Benutzer Objekt. Erfolgt die Registrierung über eine normale Anmeldung (der signInProvider Eigentum des RegistrationForm Klasse nicht festgelegt), verschlüsselt die Dienstmethode das vom Benutzer bereitgestellte Passwort und legt das verschlüsselte Passwort für den erstellten Benutzer fest Objekt.
  4. Die Dienstmethode speichert die Informationen des erstellten Benutzers Objekt in die Datenbank und gibt den gespeicherten Benutzer zurück Objekt.

Der Quellcode des RepositoryUserService Klasse sieht wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class RepositoryUserService implements UserService {

    private PasswordEncoder passwordEncoder;

    private UserRepository repository;

    @Autowired
    public RepositoryUserService(PasswordEncoder passwordEncoder, UserRepository repository) {
        this.passwordEncoder = passwordEncoder;
        this.repository = repository;
    }

    @Transactional
    @Override
    public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException {
        if (emailExist(userAccountData.getEmail())) {
            throw new DuplicateEmailException("The email address: " + userAccountData.getEmail() + " is already in use.");
        }

        String encodedPassword = encodePassword(userAccountData);

	    User registered = User.getBuilder()
				.email(userAccountData.getEmail())
				.firstName(userAccountData.getFirstName())
				.lastName(userAccountData.getLastName())
				.password(encodedPassword)
				.signInProvider(userAccountData.getSignInProvider())
				.build();

        return repository.save(registered);
    }

    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);

        if (user != null) {
            return true;
        }

        return false;
    }

    private String encodePassword(RegistrationForm dto) {
        String encodedPassword = null;

        if (dto.isNormalRegistration()) {
            encodedPassword = passwordEncoder.encode(dto.getPassword());
        }

        return encodedPassword;
    }
}

Wenn wir Unit-Tests schreiben wollen, die sicherstellen, dass unsere Dienstmethode korrekt funktioniert, wenn der Benutzer ein neues Benutzerkonto über die soziale Anmeldung registriert, UND wir jede Interaktion zwischen unserer Dienstmethode und unseren Scheinobjekten überprüfen möchten, müssen wir acht schreiben Unit-Tests dafür.

Wir müssen sicherstellen, dass:

  • Die Dienstmethoden prüfen, ob die E-Mail-Adresse eindeutig ist, wenn eine doppelte E-Mail-Adresse angegeben wird.
  • Die DuplicateEmailException wird ausgelöst, wenn eine doppelte E-Mail-Adresse angegeben wird.
  • Die Dienstmethode speichert kein neues Konto in der Datenbank, wenn eine doppelte E-Mail-Adresse angegeben wird.
  • Unsere Servicemethode verschlüsselt das Passwort des Benutzers nicht, wenn eine doppelte E-Mail-Adresse angegeben wird.
  • Unsere Servicemethode überprüft, ob die E-Mail-Adresse eindeutig ist, wenn eine eindeutige E-Mail-Adresse angegeben wird.
  • Wenn eine eindeutige E-Mail-Adresse angegeben wird, erstellt unsere Servicemethode einen neuen Benutzer Objekt, das die korrekten Informationen enthält und die Informationen des erstellten Benutzers speichert Objekt zur Datenbank.
  • Wenn eine eindeutige E-Mail-Adresse angegeben wird, gibt unsere Servicemethode die Informationen des erstellten Benutzerkontos zurück.
  • Wenn eine eindeutige E-Mail-Adresse angegeben und eine soziale Anmeldung verwendet wird, darf unsere Servicemethode das Passwort des erstellten Benutzerkontos nicht festlegen (oder verschlüsseln).

Der Quellcode unserer Testklasse sieht wie folgt aus:

import net.petrikainulainen.spring.social.signinmvc.user.dto.RegistrationForm;
import net.petrikainulainen.spring.social.signinmvc.user.dto.RegistrationFormBuilder;
import net.petrikainulainen.spring.social.signinmvc.user.model.SocialMediaService;
import net.petrikainulainen.spring.social.signinmvc.user.model.User;
import net.petrikainulainen.spring.social.signinmvc.user.repository.UserRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
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 com.googlecode.catchexception.CatchException.catchException;
import static com.googlecode.catchexception.CatchException.caughtException;
import static net.petrikainulainen.spring.social.signinmvc.user.model.UserAssert.assertThatUser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

    private static final String REGISTRATION_EMAIL_ADDRESS = "[email protected]";
    private static final String REGISTRATION_FIRST_NAME = "John";
    private static final String REGISTRATION_LAST_NAME = "Smith";
    private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;

    private RepositoryUserService registrationService;

    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private UserRepository repository;

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

    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldCheckThatEmailIsUnique() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());

        catchException(registrationService).registerNewUserAccount(registration);

        verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
    }

    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldThrowException() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());

        catchException(registrationService).registerNewUserAccount(registration);

        assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
    }

    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());

        catchException(registrationService).registerNewUserAccount(registration);

        verify(repository, never()).save(isA(User.class));
    }

    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());

        catchException(registrationService).registerNewUserAccount(registration);

        verifyZeroInteractions(passwordEncoder);
    }

    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCheckThatEmailIsUnique() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);

        registrationService.registerNewUserAccount(registration);

        verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
    }

    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);

        registrationService.registerNewUserAccount(registration);

        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
        verify(repository, times(1)).save(userAccountArgument.capture());

        User createdUserAccount = userAccountArgument.getValue();

        assertThatUser(createdUserAccount)
                .hasEmail(REGISTRATION_EMAIL_ADDRESS)
                .hasFirstName(REGISTRATION_FIRST_NAME)
                .hasLastName(REGISTRATION_LAST_NAME)
                .isRegisteredUser()
                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
    }


    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).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);

        assertThatUser(createdUserAccount)
                .hasEmail(REGISTRATION_EMAIL_ADDRESS)
                .hasFirstName(REGISTRATION_FIRST_NAME)
                .hasLastName(REGISTRATION_LAST_NAME)
                .isRegisteredUser()
                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
    }

    @Test
    public void registerNewUserAccount_SocialSignInAnUniqueEmail_ShouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);

        registrationService.registerNewUserAccount(registration);

        verifyZeroInteractions(passwordEncoder);
    }
}

Diese Klasse hat viele Unit-Tests. Sind wir sicher, dass jeder von ihnen wirklich notwendig ist?

Oder vielleicht nicht

Das offensichtliche Problem besteht darin, dass wir zwei Komponententests geschrieben haben, die beide überprüfen, ob unsere Servicemethode überprüft, ob die vom Benutzer angegebene E-Mail-Adresse eindeutig ist. Wir könnten dies beheben, indem wir diese Tests zu einem einzigen Komponententest kombinieren. Schließlich sollte uns ein Test davon überzeugen, dass unsere Servicemethode die Eindeutigkeit der vom Benutzer angegebenen E-Mail-Adresse überprüft, bevor ein neues Benutzerkonto erstellt wird.

Wenn wir dies tun, werden wir jedoch keine Antwort auf eine viel interessantere Frage finden. Diese Frage lautet:

Sollten wir wirklich jede Interaktion zwischen dem getesteten Code und unseren Scheinobjekten überprüfen?

Vor ein paar Monaten stieß ich auf einen Artikel mit dem Titel:Why Most Unit Testing is Waste von James Coplien. Dieser Artikel macht mehrere gute Punkte, aber einer davon passt sehr gut in diese Situation. James Coplien argumentierte, dass wir zu jedem Test in unserer Testsuite eine Frage stellen sollten:

Wenn dieser Test fehlschlägt, welche Geschäftsanforderung ist gefährdet?

Er erklärt auch, warum dies eine so wichtige Frage ist:

Meistens lautet die Antwort:"Ich weiß es nicht." Wenn Sie den Wert des Tests nicht kennen, könnte der Test theoretisch keinen geschäftlichen Wert haben. Der Test ist mit Kosten verbunden:Wartung, Rechenzeit, Administration und so weiter. Das bedeutet, dass der Test einen negativen Nettowert haben könnte. Das ist die vierte Kategorie von Tests, die entfernt werden müssen.

Lassen Sie uns anhand dieser Frage herausfinden, was passiert, wenn wir unsere Komponententests auswerten.

Aufklappen der Frage

Wenn Sie die Frage stellen:"Wenn dieser Test fehlschlägt, welche Geschäftsanforderung ist gefährdet?" zu jedem Unit-Test unserer Testklasse erhalten wir folgende Antworten:

  • Die Dienstmethode überprüft, ob die E-Mail-Adresse eindeutig ist, wenn eine doppelte E-Mail-Adresse angegeben wird.
    • Der Benutzer muss eine eindeutige E-Mail-Adresse haben.
  • Die DuplicateEmailException wird ausgelöst, wenn eine doppelte E-Mail-Adresse angegeben wird.
    • Der Benutzer muss eine eindeutige E-Mail-Adresse haben.
  • Die Dienstmethode speichert kein neues Konto in der Datenbank, wenn eine doppelte E-Mail-Adresse angegeben wird.
    • Der Benutzer muss eine eindeutige E-Mail-Adresse haben.
  • Unsere Servicemethode verschlüsselt das Passwort des Benutzers nicht, wenn eine doppelte E-Mail-Adresse angegeben wird.
    • -
  • Unsere Servicemethode prüft, ob die E-Mail-Adresse eindeutig ist, wenn eine eindeutige E-Mail-Adresse angegeben wird.
    • Der Benutzer muss eine eindeutige E-Mail-Adresse haben.
  • Wenn eine eindeutige E-Mail-Adresse angegeben wird, erstellt unsere Servicemethode ein neues Benutzerobjekt, das die korrekten Informationen enthält, und speichert die Informationen des erstellten Benutzerobjekts in der verwendeten Datenbank.
    • Wenn das registrierte Benutzerkonto eine eindeutige E-Mail-Adresse hat, muss diese in der Datenbank gespeichert werden.
    • Wird das registrierte Benutzerkonto über Social Sign-In erstellt, muss unsere Servicemethode den verwendeten Social-Sign-In-Anbieter speichern.
  • Wenn eine eindeutige E-Mail-Adresse angegeben wird, gibt unsere Servicemethode die Informationen des erstellten Benutzerkontos zurück.
    • Unsere Servicemethode muss die Informationen des erstellten Benutzerkontos zurückgeben.
  • Wenn eine eindeutige E-Mail-Adresse angegeben wird und eine soziale Anmeldung verwendet wird, darf unsere Servicemethode das Passwort des erstellten Benutzerkontos nicht festlegen (oder verschlüsseln).
    • Benutzerkonto, das mit Social Sign-in erstellt wurde, hat kein Passwort.

Zunächst sieht es so aus, als hätte unsere Testklasse nur einen Unit-Test, der keinen Geschäftswert hat (oder einen negativen Nettowert haben könnte). Dieser Einheitentest stellt sicher, dass es keine Interaktionen zwischen unserem Code und dem PasswordEncoder gibt Mock, wenn ein Benutzer versucht, ein neues Benutzerkonto mit einer doppelten E-Mail-Adresse zu erstellen.

Es ist klar, dass wir diesen Komponententest löschen müssen, aber dies ist nicht der einzige Komponententest, der gelöscht werden muss.

Der Kaninchenbau ist tiefer als erwartet

Vorhin haben wir festgestellt, dass unsere Testklasse zwei Komponententests enthält, die beide überprüfen, ob findByEmail() Methode des UserRepository Schnittstelle aufgerufen wird. Wenn wir uns die Implementierung der getesteten Servicemethode genauer ansehen, stellen wir Folgendes fest:

  • Unsere Dienstmethode löst eine DuplicateEmailException aus wenn findByEmail() Methode des UserRepository Schnittstelle gibt einen Benutzer zurück Objekt.
  • Unsere Dienstmethode erstellt ein neues Benutzerkonto, wenn findByEmail() Methode des UserRepository Schnittstelle gibt null zurück.

Der relevante Teil der getesteten Dienstmethode sieht wie folgt aus:

public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException {
	if (emailExist(userAccountData.getEmail())) {
		//If the PersonRepository returns a Person object, an exception is thrown.
		throw new DuplicateEmailException("The email address: " + userAccountData.getEmail() + " is already in use.");
	}

	//If the PersonRepository returns null, the execution of this method continues.
}

private boolean emailExist(String email) {
	User user = repository.findByEmail(email);

	if (user != null) {
		return true;
	}

	return false;
}

Ich argumentiere, dass wir diese beiden Komponententests aus zwei Gründen entfernen sollten:

  • Solange wir das PersonRepository konfiguriert haben Mock richtig, wir wissen, dass es findByEmail() ist Die Methode wurde mit dem richtigen Methodenparameter aufgerufen. Obwohl wir diese Testfälle mit einer Geschäftsanforderung verknüpfen können (die E-Mail-Adresse des Benutzers muss eindeutig sein), benötigen wir sie nicht, um zu überprüfen, ob diese Geschäftsanforderung nicht gefährdet ist.
  • Diese Einheitentests dokumentieren nicht die API unserer Servicemethode. Sie dokumentieren die Umsetzung. Tests wie dieser sind schädlich, weil sie unsere Testsuite mit irrelevanten Tests übersäten und das Refactoring erschweren.
Wenn wir unsere Mock-Objekte nicht konfigurieren, geben sie „schöne“ Werte zurück. In den häufig gestellten Fragen zu Mockito heißt es:

Um transparent und unauffällig zu sein, geben alle Mockito-Mocks standardmäßig "nette" Werte zurück. Zum Beispiel:Nullen, Falseys, leere Sammlungen oder Nullen. Siehe javadocs über Stubbing, um genau zu sehen, welche Werte standardmäßig zurückgegeben werden.

Deshalb sollten wir immer die relevanten Mock-Objekte konfigurieren! Andernfalls könnten unsere Tests nutzlos sein.

Lass uns weitermachen und dieses Chaos beseitigen.

Das Chaos aufräumen

Nachdem wir diese Komponententests aus unserer Testklasse entfernt haben, sieht ihr Quellcode wie folgt aus:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
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 com.googlecode.catchexception.CatchException.catchException;
import static com.googlecode.catchexception.CatchException.caughtException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

    private static final String REGISTRATION_EMAIL_ADDRESS = "[email protected]";
    private static final String REGISTRATION_FIRST_NAME = "John";
    private static final String REGISTRATION_LAST_NAME = "Smith";
    private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;

    private RepositoryUserService registrationService;

    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private UserRepository repository;

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

    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldThrowException() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());

        catchException(registrationService).registerNewUserAccount(registration);

        assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
    }

    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());

        catchException(registrationService).registerNewUserAccount(registration);

        verify(repository, never()).save(isA(User.class));
    }

    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);

        registrationService.registerNewUserAccount(registration);

        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
        verify(repository, times(1)).save(userAccountArgument.capture());

        User createdUserAccount = userAccountArgument.getValue();

        assertThatUser(createdUserAccount)
                .hasEmail(REGISTRATION_EMAIL_ADDRESS)
                .hasFirstName(REGISTRATION_FIRST_NAME)
                .hasLastName(REGISTRATION_LAST_NAME)
                .isRegisteredUser()
                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
    }


    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).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);

        assertThatUser(createdUserAccount)
                .hasEmail(REGISTRATION_EMAIL_ADDRESS)
                .hasFirstName(REGISTRATION_FIRST_NAME)
                .hasLastName(REGISTRATION_LAST_NAME)
                .isRegisteredUser()
                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
    }

    @Test
    public void registerNewUserAccount_SocialSignInAnUniqueEmail_ShouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);

        registrationService.registerNewUserAccount(registration);

        verifyZeroInteractions(passwordEncoder);
    }
}

Wir haben drei Komponententests aus unserer Testklasse entfernt und können daher die folgenden Vorteile genießen:

  • Unsere Testklasse hat weniger Einheitentests . Dies mag wie ein seltsamer Vorteil erscheinen, da uns oft geraten wird, so viele Unit-Tests wie möglich zu schreiben. Wenn wir jedoch darüber nachdenken, ist es sinnvoll, weniger Unit-Tests zu haben, da wir weniger Tests warten müssen. Dies und die Tatsache, dass jede Einheit nur eine Sache testet, macht unseren Code einfacher zu pflegen und umzugestalten.
  • Wir haben die Qualität unserer Dokumentation verbessert . Die entfernten Komponententests haben die öffentliche API der getesteten Dienstmethode nicht dokumentiert. Sie dokumentierten die Umsetzung. Da diese Tests entfernt wurden, ist es einfacher, die Anforderungen der getesteten Dienstmethode herauszufinden.

Zusammenfassung

Dieser Blogbeitrag hat uns drei Dinge gelehrt:

  • Wenn wir die Geschäftsanforderung nicht identifizieren können, die beeinträchtigt wird, wenn ein Einheitentest fehlschlägt, sollten wir diesen Test nicht schreiben.
  • Wir sollten keine Unit-Tests schreiben, die die öffentliche API der getesteten Methode nicht dokumentieren, da diese Tests unseren Code (und Tests) schwerer zu warten und umzugestalten machen.
  • Wenn wir bestehende Komponententests finden, die gegen diese beiden Regeln verstoßen, sollten wir sie löschen.

Wir haben in diesem Tutorial viel erreicht. Glauben Sie, dass es möglich ist, diese Unit-Tests noch besser zu machen?


Java-Tag