Java >> Java Tutorial >  >> Java

Saubere Tests schreiben – Java 8 zur Rettung

Im vorherigen Teil dieses Tutorials haben wir einige häufige Probleme gelöst, die bei „sauberen“ Komponententests gefunden wurden, indem wir eine verschachtelte Konfiguration verwendet haben.

Ich war sehr zufrieden mit der abschließenden Testklasse, aber nach einer Weile merkte ich, dass mich etwas störte. Das einzige Problem war, dass ich nicht herausfinden konnte, was es war.

Ich ignorierte dieses Gefühl und schrieb weiter Einheitentests. Dann passierten zwei Dinge:

  1. AssertJ Core 3.0.0 für Java 8 wurde veröffentlicht.
  2. Ich habe einen Blogbeitrag mit dem Titel gelesen:Kompakteres Mockito mit Java 8, Lambda-Ausdrücken und Mockito-Java8-Add-ons.

Plötzlich war mir alles klar.

Die verborgenen Probleme aufdecken

Obwohl wir einige kleine Verbesserungen an unserer Testklasse vorgenommen haben, hat sie immer noch zwei Probleme.

Bevor wir uns diese Probleme genauer ansehen, frischen wir unser Gedächtnis auf und werfen einen Blick auf den Quellcode unserer Testklasse. Es sieht 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);
                }
            }
         
        }
    }
}

Wenn Sie bei unserem Testcode keine Probleme feststellen konnten, sollten Sie kein schlechtes Gewissen haben. Es ist extrem schwierig, diese Probleme zu bemerken, wenn Sie nicht wissen, wonach Sie suchen müssen.

Der Hauptgrund dafür ist, dass es vor der Veröffentlichung von Java 8 keine andere Möglichkeit gab, diese Tests zu schreiben. Nach der Veröffentlichung von Java 8 begannen Testwerkzeuge jedoch, seine Funktionen zu nutzen. Das bedeutet, dass wir unsere Tests etwas verbessern können.

Die zwei Probleme, die in unserer Testklasse gefunden wurden, sind:

Zuerst verwenden einige Testmethoden die Catch-Exception-Bibliothek zum Abfangen von Ausnahmen, die vom getesteten Code ausgelöst werden. Das Problem bei diesem Ansatz ist folgendes:

Wenn wir Zusicherungen für die vom getesteten Code ausgelöste Ausnahme schreiben wollen, müssen wir sie zuerst erfassen .

Der Code, der die ausgelöste Ausnahme erfasst und sicherstellt, dass es sich um eine Instanz der DuplicateEmailException handelt Klasse sieht wie folgt aus (der unnötige Schritt ist hervorgehoben):

catchException(registrationService).registerNewUserAccount(registration);
assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);

Offensichtlich sieht das nicht nach einem großen Problem aus, da unsere Testklasse nur eine Methode hat, die diesen Code verwendet.

Wenn wir jedoch Tests für eine reale Anwendung schreiben würden, müssten wir wahrscheinlich viele Tests schreiben, die vom getesteten Code ausgelöste Ausnahmen abfangen. Ich stimme zu, dass es immer noch kein großes Problem ist, aber wenn wir es besser machen könnten, wäre es dumm, es nicht zu tun.

Zweiter Da wir sicherstellen müssen, dass das erstellte Benutzerkonto die richtigen Informationen enthält, müssen einige Testmethoden den Methodenparameter erfassen, der an save() übergeben wird Methode des UserRepository spotten. Der Code, der den Methodenparameter erfasst und einen Verweis auf den persistenten Benutzer erhält Objekt sieht wie folgt aus:

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

Das Problem ist, dass wir jedes Mal denselben Code schreiben müssen, wenn wir auf den persistenten Benutzer zugreifen möchten Objekt. Obwohl unsere Testklasse beispielsweise relativ einfach ist, müssen wir diesen Code fünfmal schreiben. Können Sie erraten, wie oft wir das tun müssen, wenn wir Tests für eine reale Anwendung schreiben?

Genau . Deshalb ist dies ein großes Problem.

Probleme mit Java 8 beheben

Wir können diese Probleme beheben, indem wir die folgenden Bibliotheken verwenden:

  • AssertJ Core 3.2.0. Wir verwenden es, weil es eine Java 8-freundliche Möglichkeit bietet, Assertionen für die vom getesteten Code ausgelösten Ausnahmen zu schreiben, aber es hat auch viele andere coole Funktionen.
  • Mockito-Java8 macht Mocking kompakter, indem es Java 8 und Lambda-Ausdrücke nutzt.

Beginnen wir damit, die erforderlichen Abhängigkeiten abzurufen.

Erforderliche Abhängigkeiten erhalten

Bevor wir die in unserer Testklasse gefundenen Probleme beheben können, müssen wir die Bibliotheken AssertJ Core 3.1.0 und Mockito-Java8 0.3.0 abrufen.

Zuerst , wenn wir Gradle verwenden, müssen wir die folgenden Abhängigkeitserklärungen in unsere build.gradle einfügen Datei:

testCompile (
	'org.assertj:assertj-core:3.2.0',
	'info.solidsoft.mockito:mockito-java8:0.3.0'
)

Zweiter , wenn wir Maven verwenden, müssen wir die folgenden Abhängigkeitserklärungen in unsere pom.xml einfügen Datei:

<dependency>
	<groupId>org.assertj</groupId>
	<artifactId>assertj-core</artifactId>
	<version>3.2.0</version>
	<scope>test</scope>
</dependency>
<dependency>
    <groupId>info.solidsoft.mockito</groupId>
    <artifactId>mockito-java8</artifactId>
    <version>0.3.0</version>
    <scope>test</scope>
</dependency>

Lassen Sie uns herausfinden, wie wir Ausnahmen abfangen können, ohne Boilerplate-Code zu schreiben.

Ausnahmen abfangen, ohne Boilerplate-Code zu schreiben

Der vorhandene Code, der die von registerNewUserAccount() ausgelöste Ausnahme erfasst -Methode und stellt sicher, dass es sich um eine Instanz der DuplicateEmailException handelt Klasse sieht wie folgt aus:

catchException(registrationService).registerNewUserAccount(registration);
assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);

Wenn wir AssertJ 3.2.0 verwenden, können wir Ausnahmen mit einer dieser beiden Methoden abfangen:

Zuerst , können wir das statische catchThrowable() verwenden Methode der Assertions Klasse. Diese Methode gibt das Throwable zurück Objekt, das vom getesteten Code ausgelöst wird.

Der Code, der eine von registerNewUserAccount() ausgelöste Ausnahme erfasst Methode sieht wie folgt aus:

Throwable t = catchThrowable(() -> registrationService.registerNewUserAccount(registration));
assertThat(t).isExactlyInstanceOf(DuplicateEmailException.class);

Wie wir sehen können, löst dies unser Problem nicht wirklich. Wir haben einfach die Catch-Exception-Bibliothek durch AssertJ ersetzt. Obwohl es sinnvoll ist, die Catch-Exception-Bibliothek loszuwerden, wenn unsere Tests bereits AssertJ verwenden, können wir es besser machen.

Zweiter , können wir das statische assertThatThrownBy() verwenden Methode der Assertions Klasse. Diese Methode gibt ein AbstractThrowableAssert zurück -Objekt, mit dem wir Assertionen für die ausgelöste Ausnahme schreiben können.

Der Code, der eine von registerNewUserAccount() ausgelöste Ausnahme erfasst Methode sieht wie folgt aus:

assertThatThrownBy(() -> registrationService.registerNewUserAccount(registration))
		.isExactlyInstanceOf(DuplicateEmailException.class);

Wie wir sehen können, haben wir es geschafft, die Zeile zu entfernen, die verwendet wurde, um einen Verweis auf die vom getesteten Code ausgelöste Ausnahme zu erhalten. Es ist keine große Verbesserung, aber kleine Dinge summieren sich.

Lassen Sie uns herausfinden, wie wir Methodenargumente erfassen können, ohne Boilerplate-Code zu schreiben.

Methodenargumente erfassen, ohne Boilerplate-Code zu schreiben

Der vorhandene Code, der den dauerhaften Benutzer erfasst Objekt und stellt sicher, dass Vor- und Nachname korrekt sind, sieht folgendermaßen aus:

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)

Wir können Methodenargumente mit Mockito-Java8 erfassen, indem wir das statische assertArg() verwenden Methode des AssertionMatcher Klasse. Nachdem wir die erforderlichen Änderungen vorgenommen haben, sieht unser neuer Code wie folgt aus:

verify(repository, times(1)).save(assertArg(
	createdUserAccount -> assertThatUser(createdUserAccount)
			.hasFirstName(REGISTRATION_FIRST_NAME)
			.hasLastName(REGISTRATION_LAST_NAME)
));

Das sieht ziemlich toll aus. Wir haben zwei Zeilen mit unnötigem Code entfernt und eine stärkere Verbindung zwischen dem erwarteten Methodenaufruf und seinen Methodenparametern erstellt. Meiner Meinung nach sieht unser Code dadurch etwas „natürlicher“ und leichter lesbar aus.

Fahren wir fort und nehmen diese Änderungen an unserer Testklasse vor.

Was haben wir getan?

Als wir diese Änderungen an unserer Testklasse vorgenommen haben, haben wir 11 Zeilen unnötigen Codes entfernt. Der Quellcode unserer Testklasse sieht wie folgt aus (die geänderten Teile sind hervorgehoben):

import com.nitorcreations.junit.runners.NestedRunner
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 info.solidsoft.mockito.java8.AssertionMatcher.assertArg;  
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;  
import static org.assertj.core.api.Assertions.catchThrowable;
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 {
                    assertThatThrownBy(() -> registrationService.registerNewUserAccount(registration))
							.isExactlyInstanceOf(DuplicateEmailException.class);
                }
                 
                @Test
                public void shouldNotSaveNewUserAccount() throws DuplicateEmailException {  
                    catchThrowable(() -> 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);

  				  	verify(repository, times(1)).save(assertArg(
						createdUserAccount -> assertThatUser(createdUserAccount)
								.hasEmail(REGISTRATION_EMAIL_ADDRESS);
					));                           
                }
                 
                @Test
                public void shouldSetCorrectFirstAndLastName() throws DuplicateEmailException { 
                    registrationService.registerNewUserAccount(registration);
  
					verify(repository, times(1)).save(assertArg(
						createdUserAccount -> assertThatUser(createdUserAccount)
								.hasFirstName(REGISTRATION_FIRST_NAME)
								.hasLastName(REGISTRATION_LAST_NAME)
					));
                }
                 
                @Test
                public void shouldCreateRegisteredUser() throws DuplicateEmailException { 
                    registrationService.registerNewUserAccount(registration);
  
					verify(repository, times(1)).save(assertArg(
						createdUserAccount -> assertThatUser(createdUserAccount)
								.isRegisteredUser()
					));
                }
                 
                @Test
                public void shouldSetSignInProvider() throws DuplicateEmailException { 
                    registrationService.registerNewUserAccount(registration);
  
					verify(repository, times(1)).save(assertArg(
						createdUserAccount -> 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);
  
					verify(repository, times(1)).save(assertArg(
						createdUserAccount -> assertThat(returnedUserAccount)
							.isEqualTo(createdUserAccount);
					));
                }
            }
         
        }
    }
}

Fassen wir zusammen, was wir aus diesem Blogbeitrag gelernt haben.

Zusammenfassung

Dieser Blogpost hat uns zwei Dinge gelehrt:

  • Wir können Ausnahmen abfangen und Zusicherungen für sie schreiben, ohne einen Verweis auf die ausgelöste Ausnahme zu erhalten.
  • Wir können Methodenargumente erfassen und Zusicherungen für sie schreiben, indem wir Lambda-Ausdrücke verwenden.

Java-Tag