Java >> Java Tutorial >  >> Java

Saubere Tests schreiben - Klein ist schön

Wir haben gelernt, dass „saubere“ Komponententests möglicherweise nicht so sauber sind, wie wir denken.

Wir haben unser Bestes getan, um unsere Komponententests so sauber wie möglich zu gestalten. Unsere Tests sind richtig formatiert, verwenden eine domänenspezifische Sprache und vermeiden übermäßiges Spotten.

Trotzdem sind unsere Unit-Tests nicht sauber, weil:

  • Wenn wir Änderungen am getesteten Code vornehmen, werden die meisten unserer vorhandenen Einheitentests nicht kompiliert oder schlagen fehl, wenn wir sie ausführen. Das Korrigieren dieser Einheitentests ist langsam und frustrierend.
  • Wenn wir der getesteten Klasse neue Methoden hinzufügen, stellen wir fest, dass das Schreiben neuer Komponententests viel langsamer ist, als es sein sollte.

Wenn dies der Fall ist, leiden unsere Unit-Tests sehr wahrscheinlich unter diesen allgemeinen Problemen:

  • Die Methodennamen unserer Testmethoden sind viel zu lang. Wenn ein Test fehlschlägt, beschreibt der Methodenname nicht unbedingt, was schief gelaufen ist. Außerdem ist es schwierig, sich einen kurzen Überblick über die Situationen zu verschaffen, die von unseren Tests abgedeckt werden. Dies bedeutet, dass wir möglicherweise dieselbe Situation mehr als einmal testen.
  • Unsere Testmethoden enthalten doppelten Code, der Scheinobjekte konfiguriert und andere Objekte erstellt, die in unseren Tests verwendet werden. Das bedeutet, dass unsere Tests schwer zu lesen, zu schreiben und zu warten sind.
  • Da es keinen sauberen Weg gibt, eine gemeinsame Konfiguration mit nur wenigen Testmethoden zu teilen, müssen wir alle Konstanten an den Anfang der Testklasse stellen. Einige von Ihnen mögen behaupten, dass dies ein geringfügiges Problem ist, und Sie haben Recht, aber es macht unsere Testklassen immer noch chaotischer, als sie sein sollten.

Lassen Sie uns herausfinden, wie wir all diese Probleme lösen können.

Verschachtelte Konfiguration zur Rettung

Wenn wir die bei unseren Unit-Tests gefundenen Probleme beheben wollen, müssen wir das tun

  • Beschreiben Sie die getestete Methode und den Testzustand auf eine Weise, die keine langen Methodennamen erfordert.
  • Finden Sie einen Weg, um die allgemeine Konfiguration von Testmethoden zu Einrichtungsmethoden zu verschieben.
  • Erstellen Sie einen gemeinsamen Kontext für Testmethoden und machen Sie Einrichtungsmethoden und -konstanten nur für die Testmethoden sichtbar, die zum erstellten Kontext gehören.

Es gibt einen JUnit-Runner, der uns helfen kann, diese Ziele zu erreichen. Es heißt NestedRunner und ermöglicht es uns, Testmethoden auszuführen, die in verschachtelten inneren Klassen platziert sind.

Bevor wir unsere Probleme mithilfe von NestedRunner lösen können, müssen wir die NestedRunner-Abhängigkeit zu unserem Maven-Build hinzufügen und sicherstellen, dass unsere Testmethoden von NestedRunner aufgerufen werden Klasse.

Zuerst , müssen wir die folgende Abhängigkeitserklärung zu unserer pom.xml hinzufügen Datei:

<dependency>
	<groupId>com.nitorcreations</groupId>
	<artifactId>junit-runners</artifactId>
	<version>1.2</version>
	<scope>test</scope>
</dependency>

Zweiter , müssen wir die folgenden Änderungen am RepositoryUserServiceTest vornehmen Klasse:

  1. Stellen Sie sicher, dass die Testmethoden aus RepositoryUserServiceTest gefunden werden -Klasse werden vom NestedRunner aufgerufen Klasse.
  2. Entfernen Sie @Mock Anmerkungen vom passwordEncoder und Repository Felder.
  3. Erstellen Sie die erforderlichen Mock-Objekte, indem Sie das statische mock() aufrufen Methode des Mockito Klasse und fügen Sie sie in den passwordEncoder ein und Repository Felder.

Der Quellcode des RepositoryUserServiceTest Klasse sieht wie folgt aus:

import com.nitorcreations.junit.runners.NestedRunner
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.security.crypto.password.PasswordEncoder;
 
import static org.mockito.Mockito.mock;
 
@RunWith(NestedRunner.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;
 
    private PasswordEncoder passwordEncoder;
 
    private UserRepository repository;
 
    @Before
    public void setUp() {
		passwordEncoder = mock(PasswordEncoder.class);
		repository = mock(UserRepository.class);
	
		registrationService = new RepositoryUserService(passwordEncoder, repository);
    }
}

Wir haben NestedRunner jetzt konfiguriert und können mit der Lösung der Probleme beginnen, die bei unseren Komponententests gefunden wurden. Beginnen wir damit, lange Methodennamen durch eine verschachtelte Klassenhierarchie zu ersetzen.

Ersetzen langer Testmethodennamen durch eine verschachtelte Klassenhierarchie

Bevor wir die langen Testmethodennamen durch eine verschachtelte Klassenhierarchie ersetzen können, müssen wir herausfinden, welche Situationen von unseren Komponententests abgedeckt werden. Wenn wir einen Blick auf unsere Testklasse werfen, stellen wir fest, dass die Unit-Tests aus dem RepositoryUserServiceTest gefunden wurden Klasse sicherstellen, dass:

  • Wenn es bereits ein Benutzerkonto mit derselben E-Mail-Adresse gibt, sollte unser Code
    • Eine Ausnahme auslösen.
    • kein neues Benutzerkonto speichern.
  • Wenn es kein Benutzerkonto mit derselben E-Mail-Adresse gibt, sollte unser Code
    • Speichern Sie ein neues Benutzerkonto.
    • Geben Sie die richtige E-Mail-Adresse ein.
    • Geben Sie den richtigen Vor- und Nachnamen ein.
    • Sollte einen registrierten Benutzer erstellen.
    • Stellen Sie den richtigen Anmeldeanbieter ein.
    • Kein verschlüsseltes Passwort für den Benutzer erstellen.
    • Geben Sie das erstellte Benutzerkonto zurück.

Wir können jetzt die langen Testmethodennamen eliminieren, indem wir unsere Testmethoden durch eine Klassenhierarchie im BDD-Stil ersetzen. Die Idee ist, dass wir:

  1. Erstellen Sie eine innere Klasse pro getesteter Methode. Diese Klasse kann eine Setup-Methode, Testmethoden und andere innere Klassen enthalten. In unserem Fall lautet der Name dieser inneren Klasse RegisterNewUserAccount .
  2. Erstellen Sie die Klassenhierarchie, die den zu testenden Zustand beschreibt. Wir können dies tun, indem wir dem RegisterNewUserAccount innere Klassen hinzufügen Klasse (und zu ihren inneren Klassen). Wir können diese inneren Klassen benennen, indem wir die folgende Syntax verwenden:When[StateUnderTest] . Wir können diese Klassenhierarchie unserer Testklasse hinzufügen, indem wir die folgenden Schritte ausführen:
    1. Da der Benutzer ein Benutzerkonto über die soziale Anmeldung registriert, müssen wir WhenUserUsesSocialSignIn hinzufügen Klasse zum RegisterNewUserAccount Klasse.
    2. Da wir zwei verschiedene Situationen abdecken müssen, müssen wir zwei innere Klassen hinzufügen (WhenUserAccountIsFoundWithEmailAddress und WhenEmailAddressIsUnique ) zu WhenUserUsesSocialSignIn Klasse.
  3. Fügen Sie die eigentlichen Testmethoden den richtigen inneren Klassen hinzu. Da die Klassenhierarchie die getestete Methode und den Testzustand beschreibt, darf der Name jedes Komponententests nur das erwartete Verhalten beschreiben. Eine Möglichkeit, dies zu tun, besteht darin, jede Testmethode mit dem Präfix zu benennen:sollte .

Nachdem wir die Klassenhierarchie erstellt haben, sieht der Quellcode unserer Testklasse wie folgt aus:

import com.nitorcreations.junit.runners.NestedRunner
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.mock;
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(NestedRunner.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;
 
    private PasswordEncoder passwordEncoder;
 
    private UserRepository repository;
 
    @Before
    public void setUp() {
		passwordEncoder = mock(PasswordEncoder.class);
		repository = mock(UserRepository.class);
	
		registrationService = new RepositoryUserService(passwordEncoder, repository);
    }
	
	public class RegisterNewUserAccount {
	
		public class WhenUserUsesSocialSignIn {
			
			public class WhenUserAccountIsFoundWithEmailAddress {
			    
				@Test
			    public void 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 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));
			    }
			}
			
			public class WhenEmailAddressIsUnique {
			    
				@Test
			    public void shouldSaveNewUserAccount() 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(isA(User.class));
			    }
				
			    @Test
			    public void shouldSetCorrectEmailAddress() 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);
			    }
				
			    @Test
			    public void shouldSetCorrectFirstAndLastName() 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)
			                .hasFirstName(REGISTRATION_FIRST_NAME)
			                .hasLastName(REGISTRATION_LAST_NAME)
			    }
				
			    @Test
			    public void shouldCreateRegisteredUser() 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)
			                .isRegisteredUser()
			    }
				
			    @Test
			    public void shouldSetSignInProvider() 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)
			                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
			    }
				
			    @Test
			    public void 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);
			    }
				
			    @Test
			    public void 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 returnedUserAccount = registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(userAccountArgument.capture());
 
			        User createdUserAccount = userAccountArgument.getValue();
 
			        assertThat(returnedUserAccount)
			                .isEqualTo(createdUserAccount);
			    }
			}
		
		}
	}
}

Wir haben jetzt die langen Testmethodennamen durch eine verschachtelte Klassenhierarchie ersetzt, aber der Nachteil dieser Lösung ist, dass wir viel doppelten Code hinzugefügt haben. Lassen Sie uns diesen Code loswerden.

Doppelten Code entfernen

Wir können den gesamten doppelten Code aus unserer Testklasse entfernen, indem wir ihn in die Setup-Methoden verschieben, die in den „richtigen“ inneren Klassen platziert sind. Bevor wir die „richtigen“ inneren Klassen identifizieren können, müssen wir die Ausführungsreihenfolge von Setup- und Testmethoden verstehen. Der beste Weg, dies zu verstehen, ist ein einfaches Beispiel:

@RunWith(NestedRunner.class)
public class TestClass {

	/**
	 * This setup method is invoked before the test and setup methods
	 * found from the inner classes of this class. 
	 
	 * This is a good place for configuration that is shared by all 
	 * test methods found from this test class.
	 */
	@Before
	public void setUpTestClass() {}
	
	public class MethodA {
	
		/**
		 * This setup method is invoked before the test methods found from
		 * this class and before the test and setup methods found from the
		 * inner classes of this class.
		 *
		 * This is a good place for configuration that is shared by all test
		 * methods which ensure that the methodA() is working correctly.
		 */
		@Before
		public void setUpMethodA() {}
		
		@Test
		public void shouldFooBar() {}
		
		public class WhenFoo {
		
			/**
			 * This setup method is invoked before the test methods found from
			 * this class and before the test and setup methods found from the
			 * inner classes of this class.
			 *
			 * This is a good place for configuration which ensures that the methodA()
			 * working correctly when foo is 'true'.
			 */
			@Before
			public void setUpWhenFoo() {}
			
			@Test
			public void shouldBar() {}
		}
		
		public class WhenBar {
		
			@Test
			public shouldFoo() {}
		}
	}
}

Mit anderen Worten, bevor eine Testmethode aufgerufen wird, ruft NestedRunner die Setup-Methoden auf, indem es von der Stammklasse der Klassenhierarchie zur Testmethode navigiert und alle Setup-Methoden aufruft. Gehen wir die in unserem Beispiel gefundenen Testmethoden durch:

  • Vor der Testmethode sollteFooBar() aufgerufen wird, ruft NestedRunner die setUpTestClass() auf und setUpMethodA() Methoden.
  • Vor der Testmethode shouldBar() aufgerufen wird, ruft NestedRunner die setUpTestClass() auf , setUpMethodA() und setUpWhenFoo() Methoden.
  • Vor der Testmethode shouldFoo() aufgerufen wird, ruft NestedRunner die setUpTestClass() auf und setUpMethodA() Methoden.

Wir können jetzt die notwendigen Änderungen am RepositoryUserServiceTest vornehmen Klasse, indem Sie diesen Schritten folgen:

  1. Fügen Sie ein setUp() hinzu -Methode zu WhenUserUsesSocialSignIn Klasse und implementieren Sie sie, indem Sie ein neues RegistrationForm erstellen Objekt. Dies ist der richtige Ort dafür, da alle Einheitentests ein RegistrationForm enthalten Objekt als Eingabe für die getestete Methode.
  2. Fügen Sie ein setUp() hinzu -Methode zu WhenUserAccountIsFoundWithEmailAddress Klasse und konfigurieren Sie unseren Repository-Mock, um einen Benutzer zurückzugeben Objekt, wenn es findByEmail() ist Methode wird aufgerufen, indem die E-Mail-Adresse verwendet wird, die in das Registrierungsformular eingegeben wurde. Dies ist der richtige Ort für diesen Code, da jeder Einheitentest, der von WhenUserAccountIsFoundWithEmailAddress gefunden wird Die Klasse geht davon aus, dass die bei der Registrierung angegebene E-Mail-Adresse nicht eindeutig ist.
  3. Fügen Sie ein setUp() hinzu -Methode zu WhenEmailAddressIsUnique Klasse und konfigurieren Sie unseren Repository-Mock so, dass er 1) null zurückgibt wenn es findByEmail() ist Methode wird aufgerufen, indem die E-Mail-Adresse verwendet wird, die in das Registrierungsformular eingegeben wurde, und 2) den Benutzer zurückgibt Objekt, das als Methodenparameter angegeben wird, wenn es save() ist Methode aufgerufen wird. Dies ist der richtige Ort für diesen Code, da jeder Unit-Test, der aus WhenEmailAddressIsUnique gefunden wird class geht davon aus, dass die bei der Registrierung angegebene E-Mail-Adresse eindeutig ist und die Informationen des erstellten Benutzerkontos zurückgegeben werden.

Nachdem wir diese Änderungen vorgenommen haben, sieht der Quellcode unserer Testklasse wie folgt aus:

import com.nitorcreations.junit.runners.NestedRunner
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.BDDMockito.given;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
 
@RunWith(NestedRunner.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;
 
    private PasswordEncoder passwordEncoder;
 
    private UserRepository repository;
 
    @Before
    public void setUp() {
		passwordEncoder = mock(PasswordEncoder.class);
		repository = mock(UserRepository.class);
	
		registrationService = new RepositoryUserService(passwordEncoder, repository);
    }
	
	public class RegisterNewUserAccount {
	
		public class WhenUserUsesSocialSignIn {
		
			private RegistrationForm registration;
			
			@Before
			public void setUp() {
		        RegistrationForm registration = new RegistrationFormBuilder()
		                .email(REGISTRATION_EMAIL_ADDRESS)
		                .firstName(REGISTRATION_FIRST_NAME)
		                .lastName(REGISTRATION_LAST_NAME)
		                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
		                .build();
			}
			
			public class WhenUserAccountIsFoundWithEmailAddress {
			    
				@Before
				public void setUp() {
					given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(new User());
				}
				
				@Test
			    public void shouldThrowException() throws DuplicateEmailException {
			        catchException(registrationService).registerNewUserAccount(registration);
			        assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
			    }
				
			    @Test
			    public void shouldNotSaveNewUserAccount() throws DuplicateEmailException {  
			        catchException(registrationService).registerNewUserAccount(registration);
			        verify(repository, never()).save(isA(User.class));
			    }
			}
			
			public class WhenEmailAddressIsUnique {
			
				@Before
				public void setUp() {
					given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(null);
					
			        given(repository.save(isA(User.class))).willAnswer(new Answer<User>() {
			          	@Override
			            public User answer(InvocationOnMock invocation) throws Throwable {
			                Object[] arguments = invocation.getArguments();
			                return (User) arguments[0];
			            }
			        });
				}
			    
				@Test
			    public void shouldSaveNewUserAccount() throws DuplicateEmailException {  
			        registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(isA(User.class));
			    }
				
			    @Test
			    public void shouldSetCorrectEmailAddress() throws DuplicateEmailException { 
			        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);
			    }
				
			    @Test
			    public void shouldSetCorrectFirstAndLastName() throws DuplicateEmailException { 
			        registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(userAccountArgument.capture());
			        User createdUserAccount = userAccountArgument.getValue();
 
			        assertThatUser(createdUserAccount)
			                .hasFirstName(REGISTRATION_FIRST_NAME)
			                .hasLastName(REGISTRATION_LAST_NAME)
			    }
				
			    @Test
			    public void shouldCreateRegisteredUser() throws DuplicateEmailException { 
			        registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(userAccountArgument.capture());
			        User createdUserAccount = userAccountArgument.getValue();
 
			        assertThatUser(createdUserAccount)
			                .isRegisteredUser()
			    }
				
			    @Test
			    public void shouldSetSignInProvider() throws DuplicateEmailException { 
			        registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(userAccountArgument.capture());
			        User createdUserAccount = userAccountArgument.getValue();
 
			        assertThatUser(createdUserAccount)
			                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
			    }
				
			    @Test
			    public void shouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException { 
			        registrationService.registerNewUserAccount(registration);
 
			        verifyZeroInteractions(passwordEncoder);
			    }
				
			    @Test
			    public void shouldReturnCreatedUserAccount() throws DuplicateEmailException {
			        User returnedUserAccount = registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(userAccountArgument.capture());
			        User createdUserAccount = userAccountArgument.getValue();
 
			        assertThat(returnedUserAccount)
			                .isEqualTo(createdUserAccount);
			    }
			}
		
		}
	}
}

Unsere Testklasse sieht ziemlich sauber aus, aber wir können sie noch ein wenig sauberer machen. Lassen Sie uns herausfinden, wie wir das tun können.

Verknüpfung von Konstanten mit den Testmethoden

Ein Problem, auf das wir stoßen, wenn wir magische Zahlen durch Konstanten ersetzen, besteht darin, dass wir diese Konstanten am Anfang unserer Testklasse hinzufügen müssen. Das bedeutet, dass es schwierig ist, diese Konstanten mit den Testfällen zu verknüpfen, die sie verwenden.

Wenn wir uns unsere Unit-Test-Klasse ansehen, stellen wir fest, dass wir Konstanten verwenden, wenn wir ein neues RegistrationForm erstellen Objekt. Denn das geschieht im setUp() Methode des RegisterNewUserAccount Klasse können wir unser Problem lösen, indem wir die Konstanten vom Anfang des RepositoryUserServiceTest verschieben Klasse bis zum Anfang des RegisterNewUserAccount Klasse.

Nachdem wir dies getan haben, sieht unsere Testklasse wie folgt aus:

import com.nitorcreations.junit.runners.NestedRunner
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.BDDMockito.given;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
 
@RunWith(NestedRunner.class)
public class RepositoryUserServiceTest {
 
    private RepositoryUserService registrationService;
 
    private PasswordEncoder passwordEncoder;
 
    private UserRepository repository;
 
    @Before
    public void setUp() {
		passwordEncoder = mock(PasswordEncoder.class);
		repository = mock(UserRepository.class);
	
		registrationService = new RepositoryUserService(passwordEncoder, repository);
    }
	
	public class RegisterNewUserAccount {
	
    	private final String REGISTRATION_EMAIL_ADDRESS = "[email protected]";
    	private final String REGISTRATION_FIRST_NAME = "John";
    	private final String REGISTRATION_LAST_NAME = "Smith";
    	private final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
	
		public class WhenUserUsesSocialSignIn {
		
			private RegistrationForm registration;
			
			@Before
			public void setUp() {
		        RegistrationForm registration = new RegistrationFormBuilder()
		                .email(REGISTRATION_EMAIL_ADDRESS)
		                .firstName(REGISTRATION_FIRST_NAME)
		                .lastName(REGISTRATION_LAST_NAME)
		                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
		                .build();
			}
			
			public class WhenUserAccountIsFoundWithEmailAddress {
			    
				@Before
				public void setUp() {
					given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(new User());
				}
				
				@Test
			    public void shouldThrowException() throws DuplicateEmailException {
			        catchException(registrationService).registerNewUserAccount(registration);
			        assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
			    }
				
			    @Test
			    public void shouldNotSaveNewUserAccount() throws DuplicateEmailException {  
			        catchException(registrationService).registerNewUserAccount(registration);
			        verify(repository, never()).save(isA(User.class));
			    }
			}
			
			public class WhenEmailAddressIsUnique {
			
				@Before
				public void setUp() {
					given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(null);
					
			        given(repository.save(isA(User.class))).willAnswer(new Answer<User>() {
			          	@Override
			            public User answer(InvocationOnMock invocation) throws Throwable {
			                Object[] arguments = invocation.getArguments();
			                return (User) arguments[0];
			            }
			        });
				}
			    
				@Test
			    public void shouldSaveNewUserAccount() throws DuplicateEmailException {  
			        registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(isA(User.class));
			    }
				
			    @Test
			    public void shouldSetCorrectEmailAddress() throws DuplicateEmailException { 
			        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);
			    }
				
			    @Test
			    public void shouldSetCorrectFirstAndLastName() throws DuplicateEmailException { 
			        registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(userAccountArgument.capture());
			        User createdUserAccount = userAccountArgument.getValue();
 
			        assertThatUser(createdUserAccount)
			                .hasFirstName(REGISTRATION_FIRST_NAME)
			                .hasLastName(REGISTRATION_LAST_NAME)
			    }
				
			    @Test
			    public void shouldCreateRegisteredUser() throws DuplicateEmailException { 
			        registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(userAccountArgument.capture());
			        User createdUserAccount = userAccountArgument.getValue();
 
			        assertThatUser(createdUserAccount)
			                .isRegisteredUser()
			    }
				
			    @Test
			    public void shouldSetSignInProvider() throws DuplicateEmailException { 
			        registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(userAccountArgument.capture());
			        User createdUserAccount = userAccountArgument.getValue();
 
			        assertThatUser(createdUserAccount)
			                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
			    }
				
			    @Test
			    public void shouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException { 
			        registrationService.registerNewUserAccount(registration);
 
			        verifyZeroInteractions(passwordEncoder);
			    }
				
			    @Test
			    public void shouldReturnCreatedUserAccount() throws DuplicateEmailException {
			        User returnedUserAccount = registrationService.registerNewUserAccount(registration);
 
			        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
			        verify(repository, times(1)).save(userAccountArgument.capture());
			        User createdUserAccount = userAccountArgument.getValue();
 
			        assertThat(returnedUserAccount)
			                .isEqualTo(createdUserAccount);
			    }
			}
		
		}
	}
}

Es ist nun klar, dass diese Konstanten für die Unit-Tests relevant sind, die aus dem RegisterNewUserAccount gefunden werden innere Klasse und von ihren inneren Klassen. Dies mag wie eine kleine Änderung erscheinen, aber ich habe festgestellt, dass kleine Dinge einen großen Unterschied machen können.

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

Zusammenfassung

Das hat uns dieser Blogbeitrag gelehrt

  • Wir können lange Methodennamen durch eine Klassenhierarchie im BDD-Stil ersetzen.
  • Wir können doppelten Code entfernen, indem wir diesen Code in Setup-Methoden verschieben und diese Methoden in die richtigen inneren Klassen einfügen.
  • Wir können die Konstanten mit Testfällen verknüpfen, die sie verwenden, indem wir die Konstanten in der richtigen inneren Klasse deklarieren.

Aktualisierung: Einige Redakteure argumentieren, dass diese Lösung nicht sauberer ist als die alte. Ich stimme zu, dass die neuen Unit-Tests ganz anders aussehen als "normale" JUnit-Tests, und es kann (zunächst) schwierig sein, sie zu lesen.

Wenn Sie jedoch IntelliJ IDEA verwenden, wird die Arbeit mit Unit-Tests zum Vergnügen. Werfen wir einen Blick auf einige Screenshots (wenn Sie das Bild in voller Größe sehen möchten, klicken Sie auf das Miniaturbild):

Wir können die inneren Klassen, die uns nicht interessieren, "schließen" und uns auf die interessanten Testfälle konzentrieren:

Wenn wir unsere Komponententests ausführen, können wir in der Testklassenhierarchie navigieren, indem wir die Registerkarte „Test Runner“ von IDEA verwenden:

Mit anderen Worten, wenn wir IDEA und NestedRunner verwenden, ist es sehr einfach, die Anforderungen der getesteten Methode herauszufinden. Ich denke, dass dies eine enorme Verbesserung gegenüber dem "traditionellen" Weg ist (eine lange Liste von Testmethoden mit langen und ungenauen Methodennamen).

P.S. Ich empfehle Ihnen, einen Blogbeitrag mit dem Titel „Three Steps to Code Quality via TDD“ zu lesen. Es ist ein ausgezeichneter Blogbeitrag und Sie können seine Lektionen nutzen, auch wenn Sie TDD nicht verwenden.


Java-Tag