Java >> Java Tutorial >  >> Java

Saubere Tests schreiben – Teile und herrsche

Ein guter Komponententest sollte nur aus einem Grund fehlschlagen. Das bedeutet, dass ein richtiger Komponententest nur ein logisches Konzept testet.

Wenn wir saubere Tests schreiben wollen, müssen wir diese logischen Konzepte identifizieren und nur einen Testfall pro logischem Konzept schreiben.

Dieser Blogbeitrag beschreibt, wie wir die logischen Konzepte unserer Tests identifizieren und einen vorhandenen Einheitentest in mehrere Einheitentests aufteilen können.

Ziemlich sauber ist nicht gut genug

Beginnen wir mit einem Blick auf den Quellcode unseres Komponententests, der sicherstellt, dass die registerNewUserAccount(RegistrationForm userAccountData) Methode des RepositoryUserService Die Klasse funktioniert wie erwartet, wenn ein neues Benutzerkonto mit einer eindeutigen E-Mail-Adresse und einem Social-Sign-In-Anbieter erstellt wird.

Der Quellcode dieses Komponententests 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 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_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() 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);

		verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
		verify(repository, times(1)).save(createdUserAccount);
		verifyNoMoreInteractions(repository);
		verifyZeroInteractions(passwordEncoder);
	}
}

Dieser Komponententest ist ziemlich sauber. Schließlich haben unsere Testklasse, Testmethode und die innerhalb der Testmethode erstellten lokalen Variablen aussagekräftige Namen. Wir haben auch magische Zahlen durch Konstanten ersetzt und domänenspezifische Sprachen zum Erstellen neuer Objekte und zum Schreiben von Behauptungen erstellt.

Und doch können wir diesen Test noch besser machen .

Das Problem dieses Komponententests besteht darin, dass er aus mehr als einem Grund fehlschlagen kann. Es kann fehlschlagen, wenn

  1. Unsere Servicemethode prüft nicht, ob die im Registrierungsformular eingegebene E-Mail-Adresse nicht in unserer Datenbank gefunden wird.
  2. Die Informationen des dauerhaften Benutzers Objekt stimmt nicht mit den im Registrierungsformular eingegebenen Informationen überein.
  3. Die Informationen des zurückgegebenen Benutzers Objekt ist nicht korrekt.
  4. Unsere Servicemethode erstellt mithilfe des PasswordEncoder ein Passwort für den Benutzer Objekt.

Mit anderen Worten, dieser Komponententest testet vier verschiedene logische Konzepte, was die folgenden Probleme verursacht:

  • Wenn dieser Test fehlschlägt, wissen wir nicht unbedingt, warum er fehlgeschlagen ist. Das bedeutet, dass wir den Quellcode des Unit-Tests lesen müssen.
  • Der Komponententest ist etwas lang, was das Lesen etwas schwierig macht.
  • Es ist schwer, das erwartete Verhalten zu beschreiben. Das bedeutet, dass es sehr schwierig ist, große Namen für unsere Testmethoden zu finden.

Deshalb müssen wir diesen Test in vier Einheitentests aufteilen.

Ein Test, ein Fehlerpunkt

Unser nächster Schritt besteht darin, unseren Komponententest in vier neue Komponententests aufzuteilen und sicherzustellen, dass jeder von ihnen ein einziges logisches Konzept testet. Wir können dies tun, indem wir die folgenden Unit-Tests schreiben:

  1. Wir müssen sicherstellen, dass unsere Servicemethode überprüft, ob die vom Benutzer angegebene E-Mail-Adresse eindeutig ist.
  2. Wir müssen diese Informationen des dauerhaften Benutzers überprüfen Objekt ist korrekt.
  3. Wir müssen sicherstellen, dass die Informationen des zurückgegebenen Benutzers Objekt ist korrekt.
  4. Wir müssen sicherstellen, dass unsere Dienstmethode kein verschlüsseltes Passwort für einen Benutzer erstellt, der einen Social-Sign-in-Anbieter verwendet.

Nachdem wir diese Unit-Tests geschrieben haben, sieht der Quellcode unserer Testklasse 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 net.petrikainulainen.spring.social.signinmvc.user.model.UserAssert.assertThat;
import static org.mockito.Matchers.isA;
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_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);
    }
}

Der offensichtliche Vorteil des Schreibens von Unit-Tests, die nur ein logisches Konzept testen, besteht darin, dass man leicht erkennen kann, warum der Test fehlgeschlagen ist. Dieser Ansatz hat jedoch auch zwei weitere Vorteile:

  • Es ist einfach, das erwartete Verhalten anzugeben. Das bedeutet, dass es einfacher ist, gute Namen für unsere Testmethoden zu finden.
  • Da diese Unit-Tests erheblich kürzer sind als der ursprüngliche Unit-Test, ist es einfacher, die Anforderungen der getesteten Methode / Komponente herauszufinden. Dies hilft uns, unsere Tests in ausführbare Spezifikationen zu überführen.

Fahren wir fort und fassen zusammen, was wir aus diesem Blogbeitrag gelernt haben.

Zusammenfassung

Wir haben unseren Unit-Test jetzt erfolgreich in vier kleinere Unit-Tests aufgeteilt, die ein einziges logisches Konzept testen. Dieser Blogpost hat uns zwei Dinge gelehrt:

  • Wir haben gelernt, dass wir die logischen Konzepte identifizieren können, die von einem einzelnen Komponententest abgedeckt werden, indem wir die Situationen identifizieren, in denen dieser Test fehlschlägt.
  • Wir haben gelernt, dass das Schreiben von Unit-Tests, die nur ein logisches Konzept testen, uns hilft, unsere Testfälle in ausführbare Spezifikationen umzuwandeln, die die Anforderungen der getesteten Methode / Komponente identifizieren.

Java-Tag