Java >> Java Tutorial >  >> Tag >> Spring

Hinzufügen von Social Sign-In zu einer Spring MVC-Webanwendung:Unit Testing

Spring Social 1.0 verfügt über ein Spring-Social-Test-Modul, das das Testen von Connect-Implementierungen und API-Bindungen unterstützt. Dieses Modul wurde aus Spring Social 1.1.0 entfernt und durch das Spring MVC Test Framework ersetzt.

Das Problem ist, dass es praktisch keine Informationen zum Schreiben von Komponententests für eine Anwendung gibt, die Spring Social 1.1.0 verwendet.

Dieser Blogbeitrag behebt dieses Problem .

In diesem Blogbeitrag erfahren wir, wie wir Unit-Tests für die Registrierungsfunktion unserer Beispielanwendung schreiben können, die wir in den vorherigen Teilen dieses Spring Social-Tutorials erstellt haben.

Beginnen wir damit, herauszufinden, wie wir mit Maven die erforderlichen Testanständigkeiten erreichen können.

Erforderliche Abhängigkeiten mit Maven erhalten

Wir können die erforderlichen Testabhängigkeiten erhalten, indem wir die folgenden Abhängigkeiten in unserer POM-Datei deklarieren:

  • AssertJ (Version 1.6.0). AssertJ ist eine Bibliothek, die eine fließende Schnittstelle zum Schreiben von Behauptungen bietet.
  • hamcrest-all (Version 1.4). Wir verwenden Hamcrest-Matcher zum Schreiben von Behauptungen in unseren Einheitentests.
  • JUnit (Version 4.11). Wir müssen auch den hamcrest-core ausschließen weil wir bereits den hamcrest-all hinzugefügt haben Abhängigkeit.
  • mockito-all (Version 1.9.5). Wir verwenden Mockito als unsere spöttische Bibliothek.
  • Catch-Exception (Version 1.2.0). Die Catch-Exception-Bibliothek hilft uns, Ausnahmen abzufangen, ohne die Ausführung unserer Testmethoden zu beenden, und stellt die abgefangenen Ausnahmen für weitere Analysen zur Verfügung. Wir müssen den mockito-core ausschließen Abhängigkeit, weil wir bereits mockito-all hinzugefügt haben Abhängigkeit.
  • Frühjahrstest (Version 3.2.4.RELEASE). Das Spring Test Framework ist ein Framework, das es ermöglicht, Tests für Spring-betriebene Anwendungen zu schreiben.

Der relevante Teil der pom.xml Datei sieht wie folgt aus:

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>1.6.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <artifactId>hamcrest-core</artifactId>
            <groupId>org.hamcrest</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.9.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.googlecode.catch-exception</groupId>
    <artifactId>catch-exception</artifactId>
    <version>1.2.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>3.2.4.RELEASE</version>
    <scope>test</scope>
</dependency>

Lassen Sie uns umziehen und einen kurzen Blick unter die Haube von Spring Social werfen.

Blick unter die Haube von Spring Social

Wie wir uns vielleicht noch aus dem zweiten Teil dieses Tutorials erinnern, ist der RegistrationController Klasse ist verantwortlich für die Erstellung des Registrierungsformulars und die Verarbeitung der Formularübermittlungen des Registrierungsformulars. Es verwendet die ProviderSignInUtils Klasse für zwei Zwecke:

  1. Wenn das Registrierungsformular gerendert wird, der RegistrationController -Klasse füllt die Formularfelder vorab aus, wenn der Benutzer ein neues Benutzerkonto erstellt, indem er sich bei sozialen Netzwerken anmeldet. Das Formularobjekt wird vorab ausgefüllt, indem die vom verwendeten SaaS-API-Anbieter bereitgestellten Informationen verwendet werden. Diese Informationen werden in einer Verbindung gespeichert Objekt. Die Controller-Klasse erhält die Verbindung Objekt durch Aufrufen des statischen getConnection() Methode der ProviderSignInUtils Klasse.
  2. Nachdem ein neues Benutzerkonto erstellt wurde, wird die RegistrationConnection Klasse behält die Verbindung bei -Objekt in die Datenbank ein, wenn das Benutzerkonto mithilfe der sozialen Anmeldung erstellt wurde. Die Controller-Klasse tut dies, indem sie handlePostSignUp() aufruft Methode der ProviderSignInUtils Klasse.

Wenn wir die Rolle der ProviderSignInUtils verstehen wollen Klasse, werfen wir einen Blick auf ihren Quellcode. Der Quellcode der ProviderSignInUtils Klasse sieht wie folgt aus:

package org.springframework.social.connect.web;

import org.springframework.social.connect.Connection;
import org.springframework.web.context.request.RequestAttributes;

public class ProviderSignInUtils {
	
	public static Connection<?> getConnection(RequestAttributes request) {
		ProviderSignInAttempt signInAttempt = getProviderUserSignInAttempt(request);
		return signInAttempt != null ? signInAttempt.getConnection() : null;
	}

	public static void handlePostSignUp(String userId, RequestAttributes request) {
		ProviderSignInAttempt signInAttempt = getProviderUserSignInAttempt(request);
		if (signInAttempt != null) {
			signInAttempt.addConnection(userId);
			request.removeAttribute(ProviderSignInAttempt.SESSION_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);
		}		
	}
	
	private static ProviderSignInAttempt getProviderUserSignInAttempt(RequestAttributes request) {
		return (ProviderSignInAttempt) request.getAttribute(ProviderSignInAttempt.SESSION_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);
	}
}

Aus dem Quellcode der ProviderSignInUtils können wir zwei Dinge erkennen Klasse:

  1. Die getConnection() -Methode erhält einen ProviderSignInAttempt Objekt aus der Sitzung. Wenn das erhaltene Objekt null ist, gibt es null zurück. Andernfalls wird getConnection() aufgerufen Methode des ProviderSignInAttempt Klasse und gibt die Verbindung zurück Objekt.
  2. Der handlePostSignUp() -Methode erhält einen ProviderSignInAttempt Objekt aus der Sitzung. Wenn das Objekt gefunden wird, ruft es addConnection() auf Methode des ProviderSignInAttempt Klasse und entfernt den gefundenen ProviderSignInAttempt Objekt aus der Sitzung.

Es ist klar, dass, um Einheitentests für den RegistrationController zu schreiben Klasse müssen wir einen Weg finden, ProviderSignInAttempt zu erstellen Objekte und setzen Sie die erstellten Objekte auf Sitzung.

Lassen Sie uns herausfinden, wie das gemacht wird.

Testdoppel erstellen

Wie wir herausgefunden haben, wenn wir Unit-Tests für den RegistrationController schreiben wollen Klasse müssen wir einen Weg finden, ProviderSignInAttempt zu erstellen Objekte. Dieser Abschnitt beschreibt, wie wir dieses Ziel erreichen können, indem wir Testdoubles verwenden.

Fahren wir fort und finden heraus, wie wir ProviderSignInAttempt erstellen können Objekte in unseren Komponententests.

Erstellen von ProviderSignInAttempt-Objekten

Wenn wir verstehen wollen, wie wir ProviderSignInAttempt erstellen können Objekten, müssen wir uns den Quellcode genauer ansehen. Der Quellcode des ProviderSignInAttempt Klasse sieht wie folgt aus:

package org.springframework.social.connect.web;

import java.io.Serializable;

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.DuplicateConnectionException;
import org.springframework.social.connect.UsersConnectionRepository;

@SuppressWarnings("serial")
public class ProviderSignInAttempt implements Serializable {

	public static final String SESSION_ATTRIBUTE = ProviderSignInAttempt.class.getName();

	private final ConnectionData connectionData;
	
	private final ConnectionFactoryLocator connectionFactoryLocator;
	
	private final UsersConnectionRepository connectionRepository;
		
	public ProviderSignInAttempt(Connection<?> connection, ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository connectionRepository) {
		this.connectionData = connection.createData();
		this.connectionFactoryLocator = connectionFactoryLocator;
		this.connectionRepository = connectionRepository;		
	}
		
	public Connection<?> getConnection() {
		return connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId()).createConnection(connectionData);
	}

	void addConnection(String userId) {
		connectionRepository.createConnectionRepository(userId).addConnection(getConnection());
	}
}

Wie wir sehen können, ist der ProviderSignInAttempt Klasse hat drei Abhängigkeiten, die im Folgenden beschrieben werden:

  • Die Verbindung Schnittstelle stellt die Verbindung zum verwendeten SaaS-API-Anbieter dar.
  • Der ConnectionFactoryLocator Schnittstelle gibt die Methoden an, die zum Auffinden von ConnectionFactory erforderlich sind Objekte.
  • Das UsersConnectionRepository interface deklariert die Methoden, die verwendet werden, um die Verbindungen zwischen einem Benutzer und einem SaaS-API-Anbieter zu verwalten.

Das erste, was mir in den Sinn kommt, ist, diese Abhängigkeiten zu verspotten. Obwohl dies wie eine gute Idee erscheinen mag, hat dieser Ansatz zwei Probleme:

  1. Wir müssten das Verhalten unserer Scheinobjekte in jedem Test, den wir schreiben, konfigurieren. Das bedeutet, dass unsere Tests schwerer zu verstehen wären.
  2. Wir lassen die Implementierungsdetails von Spring Social in unsere Tests durchsickern. Dies würde die Wartung unserer Tests erschweren, da unsere Tests fehlerhaft sein könnten, wenn sich die Implementierung von Spring Social ändert.

Es ist klar, dass Spott nicht die beste Lösung für dieses Problem ist. Wir dürfen nicht vergessen, dass Mocking zwar ein wertvolles und praktisches Testwerkzeug ist, wir es aber nicht überbeanspruchen sollten.

Dadurch wird eine neue Frage erstellt:

Wenn Spott nicht in Frage kommt, was ist das richtige Werkzeug für den Job?

Die Antwort auf diese Frage findet sich in einem Artikel von Martin Fowler. In diesem Artikel spezifiziert Martin Fowler ein Test-Double namens Stub wie folgt:

Stubs bieten vorgefertigte Antworten auf Anrufe, die während des Tests getätigt wurden, und reagieren normalerweise überhaupt nicht auf irgendetwas außerhalb dessen, was für den Test programmiert wurde. Stubs können auch Informationen über Anrufe aufzeichnen, wie z. B. ein E-Mail-Gateway-Stub, der sich an die Nachrichten erinnert, die er „gesendet“ hat, oder vielleicht nur, wie viele Nachrichten er „gesendet“ hat.

Die Verwendung eines Stubs ist absolut sinnvoll, da wir an zwei Dingen interessiert sind:

  1. Wir müssen in der Lage sein, die Verbindung zu konfigurieren Objekt, das von unserem Stub zurückgegeben wird.
  2. Wir müssen überprüfen, ob die Verbindung zur Datenbank bestehen blieb, nachdem ein neues Benutzerkonto erstellt wurde.

Wir können einen Stub erstellen, der diese Ziele erfüllt, indem wir diesen Schritten folgen:

  1. Erstellen Sie einen TestProviderSignInAttempt Klasse, die den ProviderSignInAttempt erweitert Klasse.
  2. Fügen Sie eine private Verbindung hinzu Feld zur Klasse und setzen Sie den Typ des hinzugefügten Feldes auf Connection . Dieses Feld enthält einen Verweis auf die Verbindung zwischen dem Benutzer und dem SaaS-API-Anbieter.
  3. Fügen Sie private Verbindungen hinzu -Feld in die Klasse ein und legen Sie den Typ des hinzugefügten Felds auf Set fest . Dieses Feld enthält die Benutzer-IDs der dauerhaften Verbindungen.
  4. Fügen Sie einen Konstruktor hinzu, der eine Verbindung annimmt Objekt als Konstruktorargument für die erstellte Klasse. Implementieren Sie den Konstruktor, indem Sie die folgenden Schritte ausführen:
    1. Rufen Sie den Konstruktor von ProviderSignInAttempt auf Klasse und übergeben Sie die Verbindung Objekt als Konstruktorargument. Setzen Sie die Werte anderer Konstruktorargumente auf null .
    2. Legen Sie die Verbindung fest Objekt, das als Konstruktorargument an die Verbindung übergeben wird Feld.
  5. Überschreiben Sie getConnection() Methode des ProviderSignInAttempt Klasse und implementieren Sie sie, indem Sie das gespeicherte Objekt an die Verbindung zurückgeben Feld.
  6. Überschreiben Sie die addConnection(String userId) Methode des ProviderSignInAttempt Klasse und implementieren Sie sie, indem Sie die als Methodenparameter angegebene Benutzer-ID zu den Verbindungen hinzufügen eingestellt.
  7. Fügen Sie ein öffentliches getConnections() hinzu -Methode an die erstellte Klasse und implementieren Sie sie, indem Sie die Verbindungen zurückgeben eingestellt.

Der Quellcode des TestProviderSignInAttempt sieht wie folgt aus:

package org.springframework.social.connect.web;

import org.springframework.social.connect.Connection;

import java.util.HashSet;
import java.util.Set;

public class TestProviderSignInAttempt extends ProviderSignInAttempt {

    private Connection<?> connection;

    private Set<String> connections = new HashSet<>();

    public TestProviderSignInAttempt(Connection<?> connection) {
        super(connection, null, null);
        this.connection = connection;
    }

    @Override
    public Connection<?> getConnection() {
        return connection;
    }

    @Override
    void addConnection(String userId) {
        connections.add(userId);
    }

    public Set<String> getConnections() {
        return connections;
    }
}

Lassen Sie uns weitermachen und herausfinden, wie wir die Verbindung erstellen können Klasse, die in unseren Komponententests verwendet wird.

Erstellen der Verbindungsklasse

Die erstellte Verbindungsklasse ist eine Stub-Klasse, die das Verhalten "echter" Verbindungsklassen simuliert, aber keine Logik implementiert, die mit OAuth1- und OAuth2-Verbindungen verbunden ist. Außerdem muss diese Klasse die Verbindung implementieren Schnittstelle.

Wir können diese Stub-Klasse erstellen, indem wir diesen Schritten folgen:

  1. Erstellen Sie eine Testverbindung Klasse, die die AbstractConnection erweitert Klasse. Die AbstractConnection Klasse ist eine Basisklasse, die den Zustand und das Verhalten definiert, die von allen Verbindungsimplementierungen geteilt werden.
  2. Fügen Sie Verbindungsdaten hinzu Feld zur erstellten Klasse. Legen Sie den Feldtyp auf ConnectionData fest . Die Verbindungsdaten ist ein Datenübertragungsobjekt, das den internen Zustand der Verbindung zum verwendeten SaaS-API-Anbieter enthält.
  3. Fügen Sie ein Benutzerprofil hinzu Feld zur erstellten Klasse. Legen Sie den Feldtyp auf UserProfile fest . Diese Klasse stellt das Benutzerprofil des verwendeten SaaS-API-Anbieters dar und enthält die Informationen, die zwischen verschiedenen Dienstanbietern geteilt werden.
  4. Erstellen Sie einen Konstruktor, der ConnectionData übernimmt und Benutzerprofil Objekte als Konstruktorargumente und implementieren Sie es, indem Sie diesen Schritten folgen:
    1. Rufen Sie den Konstruktor der AbstractConnection auf Klasse und übergeben Sie die ConnectionData object als erstes Konstruktorargument. Setzen Sie das zweite Konstruktorargument auf null .
    2. Setzen Sie den Wert der connectionData Feld.
    3. Legen Sie den Wert von userProfile fest Feld.
  5. Überschreiben Sie das fetchUserProfile() Methode der AbstractConnection -Klasse und implementieren Sie sie, indem Sie das gespeicherte Objekt an das userProfile zurückgeben Feld.
  6. Überschreiben Sie die getAPI() Methode der AbstractConnection Klasse, und implementieren Sie sie, indem Sie null zurückgeben .
  7. Überschreiben Sie createData() Methode von AbstractConnection Klasse, und implementieren Sie sie, indem Sie das in connectionData gespeicherte Objekt zurückgeben Feld.

Der Quellcode der TestConnection Klasse sieht wie folgt aus:

package org.springframework.social.connect.support;

import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.UserProfile;

public class TestConnection extends AbstractConnection {

    private ConnectionData connectionData;

    private UserProfile userProfile;

    public TestConnection(ConnectionData connectionData, UserProfile userProfile) {
        super(connectionData, null);
        this.connectionData = connectionData;
        this.userProfile = userProfile;
    }

    @Override
    public UserProfile fetchUserProfile() {
        return userProfile;
    }

    @Override
    public Object getApi() {
        return null;
    }

    @Override
    public ConnectionData createData() {
        return connectionData;
    }
}

Lassen Sie uns weitermachen und herausfinden, wie wir diese Testdoubles in unseren Unit-Tests erstellen können.

Erstellen der Builder-Klasse

Wir haben jetzt die Stub-Klassen für unsere Unit-Tests erstellt. Unser letzter Schritt besteht darin, herauszufinden, wie wir TestProviderSignInAttempt erstellen können Objekte mithilfe dieser Klassen.

An diesem Punkt wissen wir das

  1. Der Konstruktor von TestProviderSignInAttempt Klasse nimmt eine Verbindung auf Objekt als Konstruktorargument.
  2. Der Konstruktor der TestConnection Klasse nimmt ConnectionData und Benutzerprofil Objekte als Konstruktorargumente.

Das bedeutet, dass wir neue TestProviderSignInAttempt erstellen können Objekte, indem Sie diesen Schritten folgen:

  1. Erstellen Sie neue Verbindungsdaten Objekt. Die Verbindungsdaten Die Klasse hat einen einzigen Konstruktor, der die erforderlichen Felder als Konstruktorargumente entgegennimmt.
  2. Erstellen Sie ein neues Benutzerprofil Objekt. Wir können ein neues Benutzerprofil erstellen Objekte mithilfe des UserProfileBuilder Klasse.
  3. Erstellen Sie eine neue Testverbindung -Objekt und übergeben Sie die erstellten ConnectionData und Benutzerprofil Objekte als Konstruktorargumente.
  4. Erstellen Sie einen neuen TestProviderSignInAttempt -Objekt und übergeben Sie die erstellte TestConnectionConnection Objekt als Konstruktorargument.

Der Quellcode, der einen neuen TestProviderSignInAttempt erstellt Objekt sieht wie folgt aus:

 ConnectionData connectionData = new ConnectionData("providerId",
                 "providerUserId",
                 "displayName",
                 "profileUrl",
                 "imageUrl",
                 "accessToken",
                 "secret",
                 "refreshToken",
                 1000L);
 
 UserProfile userProfile = userProfileBuilder
                .setEmail("email")
                .setFirstName("firstName")
                .setLastName("lastName")
                .build();
				
TestConnection connection = new TestConnection(connectionData, userProfile);
TestProviderSignInAttempt signIn = new TestProviderSignInAttempt(connection);

Die gute Nachricht ist, dass wir jetzt wissen, wie wir TestProviderSignInAttempt erstellen können Objekte in unseren Tests. Die schlechte Nachricht ist, dass wir diesen Code in unseren Tests nicht verwenden können.

Wir dürfen nicht vergessen, dass wir keine Komponententests schreiben, nur um sicherzustellen, dass unser Code wie erwartet funktioniert. Jeder Testfall sollte auch zeigen, wie sich unser Code in einer bestimmten Situation verhält. Wenn wir TestProviderSignInAttempt erstellen Indem wir diesen Code jedem Testfall hinzufügen, legen wir zu viel Wert auf die Erstellung der Objekte, die für unsere Testfälle erforderlich sind. Dies bedeutet, dass der Testfall schwerer zu lesen ist und die "Essenz" des Testfalls verloren geht.

Stattdessen erstellen wir eine Test-Data-Builder-Klasse, die eine fließende API zum Erstellen von TestProviderSignInAttempt bereitstellt Objekte. Wir können diese Klasse erstellen, indem wir diesen Schritten folgen:

  1. Erstellen Sie eine Klasse namens TestProviderSignInAttemptBuilder .
  2. Fügen Sie alle erforderlichen Felder hinzu, um neue Verbindungsdaten zu erstellen und Benutzerprofil Objekte zum TestProviderSignInAttemptBuilder Klasse.
  3. Methoden hinzufügen, die zum Setzen von Feldwerten der hinzugefügten Felder verwendet werden. Implementieren Sie jede Methode, indem Sie die folgenden Schritte ausführen:
    1. Setzen Sie den als Methodenparameter angegebenen Wert auf das richtige Feld.
    2. Gib einen Verweis auf den TestProviderSignInAttemptBuilder zurück Objekt.
  4. Fügen Sie Verbindungsdaten() hinzu und userProfile() Methoden zum TestProviderSignInAttemptBuilder Klasse. Diese Methoden geben einfach einen Verweis auf den TestProviderSignInAttemptBuilder zurück Objekt, und ihr Zweck ist es, unsere API lesbarer zu machen.
  5. Fügen Sie build() hinzu -Methode an die Test Data Builder-Klasse. Dadurch wird der TestProviderSignInAttempt erstellt Objekt, indem Sie den zuvor beschriebenen Schritten folgen und das erstellte Objekt zurückgeben.

Der Quellcode des TestProviderSignInAttemptBuilder Klasse sieht wie folgt aus:

package org.springframework.social.connect.support;

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.UserProfile;
import org.springframework.social.connect.UserProfileBuilder;
import org.springframework.social.connect.web.TestProviderSignInAttempt;

public class TestProviderSignInAttemptBuilder {

    private String accessToken;

    private String displayName;

    private String email;

    private Long expireTime;

    private String firstName;

    private String imageUrl;

    private String lastName;

    private String profileUrl;

    private String providerId;

    private String providerUserId;

    private String refreshToken;

    private String secret;

    public TestProviderSignInAttemptBuilder() {

    }

    public TestProviderSignInAttemptBuilder accessToken(String accessToken) {
        this.accessToken = accessToken;
        return this;
    }

    public TestProviderSignInAttemptBuilder connectionData() {
        return this;
    }

    public TestProviderSignInAttemptBuilder displayName(String displayName) {
        this.displayName = displayName;
        return this;
    }

    public TestProviderSignInAttemptBuilder email(String email) {
        this.email = email;
        return this;
    }

    public TestProviderSignInAttemptBuilder expireTime(Long expireTime) {
        this.expireTime = expireTime;
        return this;
    }

    public TestProviderSignInAttemptBuilder firstName(String firstName) {
        this.firstName = firstName;
        return this;
    }

    public TestProviderSignInAttemptBuilder imageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
        return this;
    }

    public TestProviderSignInAttemptBuilder lastName(String lastName) {
        this.lastName = lastName;
        return this;
    }

    public TestProviderSignInAttemptBuilder profileUrl(String profileUrl) {
        this.profileUrl = profileUrl;
        return this;
    }

    public TestProviderSignInAttemptBuilder providerId(String providerId) {
        this.providerId = providerId;
        return this;
    }

    public TestProviderSignInAttemptBuilder providerUserId(String providerUserId) {
        this.providerUserId = providerUserId;
        return this;
    }

    public TestProviderSignInAttemptBuilder refreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
        return this;
    }

    public TestProviderSignInAttemptBuilder secret(String secret) {
        this.secret = secret;
        return this;
    }

    public TestProviderSignInAttemptBuilder userProfile() {
        return this;
    }

    public TestProviderSignInAttempt build() {
        ConnectionData connectionData = new ConnectionData(providerId,
                providerUserId,
                displayName,
                profileUrl,
                imageUrl,
                accessToken,
                secret,
                refreshToken,
                expireTime);

        UserProfile userProfile = new UserProfileBuilder()
                .setEmail(email)
                .setFirstName(firstName)
                .setLastName(lastName)
                .build();

        Connection connection = new TestConnection(connectionData, userProfile);

        return new TestProviderSignInAttempt(connection);
    }
}

Der Code, der den neuen TestProviderSignInAttempt erstellt Objekt ist jetzt viel sauberer und besser lesbar:

TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("email")
                    .firstName("firstName")
                    .lastName("lastName")
                .build();

Lassen Sie uns weitermachen und herausfinden, wie wir unsere Komponententests mit benutzerdefiniertem AssertJ bereinigen können.

Benutzerdefinierte Behauptungen erstellen

Wir können unsere Komponententests bereinigen, indem wir die standardmäßigen JUnit-Assertionen durch benutzerdefinierte AssertJ-Assertionen ersetzen. Wir müssen vier benutzerdefinierte Assertion-Klassen erstellen, die im Folgenden beschrieben werden:

  • Die erste Assertion-Klasse wird verwendet, um Assertionen für ExampleUserDetails zu schreiben Objekte. Die ExampleUserDetails Klasse enthält die Informationen eines angemeldeten Benutzers, die im SecurityContext gespeichert werden unserer Bewerbung. Mit anderen Worten, die von dieser Klasse bereitgestellten Zusicherungen werden verwendet, um zu überprüfen, ob die Informationen des angemeldeten Benutzers korrekt sind.
  • Die zweite Assertion-Klasse wird verwendet, um Assertionen für SecurityContext zu schreiben Objekte. Diese Klasse wird zum Schreiben von Zusicherungen für den Benutzer verwendet, dessen Informationen im SecurityContext gespeichert sind .
  • Die dritte Assertionsklasse wird verwendet, um Assertionen für TestProviderSignInAttempt zu schreiben Objekte. Diese Assertionsklasse wird verwendet, um zu überprüfen, ob eine Verbindung zu einem SaaS-API-Anbieter mithilfe von TestProviderSignInAttempt erstellt wurde Objekt.
  • Die vierte Zusicherungsklasse wird verwendet, um Zusicherungen für RegistrationForm zu schreiben Objekte. Diese Klasse wird verwendet, um zu überprüfen, ob das RegistrationForm Objekt, das an unsere Servicemethode übergeben wird, enthält die richtigen Informationen.

Weiter geht's.

Erstellen der ExampleUserDetailsAssert-Klasse

Wir können die erste benutzerdefinierte Assertion-Klasse implementieren, indem wir diesen Schritten folgen:

  1. Erstellen Sie ein ExampleUserDetailsAssert Klasse, die AbstractAssert erweitert Klasse. Geben Sie die folgenden Typparameter an:
    1. Der erste Typparameter ist der Typ der benutzerdefinierten Behauptung. Legen Sie den Wert dieses Typparameters auf ExampleUserDetailsAssert fest .
    2. Der zweite Typparameter ist der Typ des eigentlichen Wertobjekts. Legen Sie den Wert dieses Typparameters auf ExampleUserDetails. fest
  2. Fügen Sie der erstellten Klasse einen privaten Konstruktor hinzu. Dieser Konstruktor akzeptiert ExampleUserDetails Objekt als Konstruktorargument. Implementieren Sie den Controller, indem Sie den Konstruktor der Oberklasse aufrufen und die folgenden Objekte als Konstruktorargumente übergeben:
    1. Das erste Konstruktorargument ist das eigentliche Wertobjekt. Übergeben Sie das als Konstruktorargument angegebene Objekt an den Konstruktor der Oberklasse.
    2. Das zweite Konstruktorargument ist eine Klasse -Objekt, das den Typ der benutzerdefinierten Assertion-Klasse angibt. Legen Sie den Wert dieses Konstruktorarguments auf ExampleUserDetailsAssert.class fest .
  3. Fügen Sie ein öffentliches statisches assertThat() hinzu -Methode auf die erstellte Klasse. Diese Methode akzeptiert ExampleUserDetails Objekt als Methodenparameter. Implementieren Sie diese Methode, indem Sie ein neues ExampleUserDetailsAssert erstellen Objekt.
  4. Fügen Sie einen öffentlichen hasFirstName() hinzu -Methode zum ExampleUserDetailsAssert Klasse. Diese Methode nimmt den erwarteten Vornamen als Methodenparameter und gibt ein ExampleUserDetailsAssert zurück Objekt.
  5. Fügen Sie eine öffentliche hasId() hinzu -Methode zum ExampleUserDetailsAssert Klasse. Diese Methode nimmt die erwartete ID als Methodenparameter und gibt ein ExampleUserDetailsAssert zurück Objekt.
  6. Fügen Sie einen öffentlichen hasLastName() hinzu -Methode zum ExampleUserDetailsAssert Klasse. Diese Methode nimmt den erwarteten Nachnamen als Methodenparameter und gibt ein ExampleUserDetailsAssert zurück Objekt.
  7. Fügen Sie ein öffentliches hasPassword() hinzu -Methode zum ExampleUserDetailsAssert Klasse. Diese Methode nimmt das erwartete Passwort als Methodenparameter und gibt ein ExampleUserDetailsAssert zurück Objekt.
  8. Fügen Sie einen öffentlichen hasUsername() hinzu -Methode zum ExampleUserDetailsAssert Klasse. Diese Methode nimmt den erwarteten Benutzernamen als Methodenparameter und gibt ein ExampleUserDetailsAssert zurück Objekt.
  9. Fügen Sie ein öffentliches isActive() hinzu -Methode zum ExampleUserDetailsAssert Klasse. Diese Methode akzeptiert keine Methodenparameter und gibt ein ExampleUserDetailsAssert zurück Objekt. Diese Methode überprüft, ob das Benutzerkonto abgelaufen oder gesperrt ist. Es stellt auch sicher, dass die Anmeldeinformationen des Benutzers nicht abgelaufen sind und das Benutzerkonto aktiviert ist.
  10. Fügen Sie einen öffentlichen isRegisteredUser() hinzu -Methode zum ExampleUserDetailsAssert Klasse. Diese Methode akzeptiert keine Methodenparameter und gibt ein ExampleUserDetailsAssert zurück Objekt. Diese Methode überprüft, ob der Benutzer nur eine Rolle hat und diese Rolle Role.ROLE_USER ist .
  11. Fügen Sie ein öffentliches isRegisteredByUsingFormRegistration() hinzu -Methode zum ExampleUserDetailsAssert Klasse. Diese Methode gibt ein ExampleUserDetailsAssert zurück Objekt.
  12. Fügen Sie ein öffentliches isSignedInByUsingSocialSignInProvider() hinzu -Methode zum ExampleUserDetailsAssert Klasse. Diese Methode nimmt einen SocialMediaService enum (der erwartete Anmeldeanbieter) als Methodenparameter und gibt ein ExampleUserDetailsAssert zurück Objekt.

Der Quellcode von ExampleUserDetailsAssert Klasse sieht wie folgt aus:

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class ExampleUserDetailsAssert extends AbstractAssert<ExampleUserDetailsAssert, ExampleUserDetails> {

    private ExampleUserDetailsAssert(ExampleUserDetails actual) {
        super(actual, ExampleUserDetailsAssert.class);
    }

    public static ExampleUserDetailsAssert assertThat(ExampleUserDetails actual) {
        return new ExampleUserDetailsAssert(actual);
    }

    public ExampleUserDetailsAssert hasFirstName(String firstName) {
        isNotNull();

        Assertions.assertThat(actual.getFirstName())
                .overridingErrorMessage("Expected first name to be <%s> but was <%s>",
                        firstName,
                        actual.getFirstName()
                )
                .isEqualTo(firstName);

        return this;
    }

    public ExampleUserDetailsAssert hasId(Long id) {
        isNotNull();

        Assertions.assertThat(actual.getId())
                .overridingErrorMessage( "Expected id to be <%d> but was <%d>",
                        id,
                        actual.getId()
                )
                .isEqualTo(id);

        return this;
    }

    public ExampleUserDetailsAssert hasLastName(String lastName) {
        isNotNull();

        Assertions.assertThat(actual.getLastName())
                .overridingErrorMessage("Expected last name to be <%s> but was <%s>",
                        lastName,
                        actual.getLastName()
                )
                .isEqualTo(lastName);

        return this;
    }

    public ExampleUserDetailsAssert hasPassword(String password) {
        isNotNull();

        Assertions.assertThat(actual.getPassword())
                .overridingErrorMessage("Expected password to be <%s> but was <%s>",
                        password,
                        actual.getPassword()
                )
                .isEqualTo(password);

        return this;
    }

    public ExampleUserDetailsAssert hasUsername(String username) {
        isNotNull();

        Assertions.assertThat(actual.getUsername())
                .overridingErrorMessage("Expected username to be <%s> but was <%s>",
                        username,
                        actual.getUsername()
                )
                .isEqualTo(username);

        return this;
    }

    public ExampleUserDetailsAssert isActive() {
        isNotNull();

        Assertions.assertThat(actual.isAccountNonExpired())
                .overridingErrorMessage("Expected account to be non expired but it was expired")
                .isTrue();

        Assertions.assertThat(actual.isAccountNonLocked())
                .overridingErrorMessage("Expected account to be non locked but it was locked")
                .isTrue();

        Assertions.assertThat(actual.isCredentialsNonExpired())
                .overridingErrorMessage("Expected credentials to be non expired but they were expired")
                .isTrue();

        Assertions.assertThat(actual.isEnabled())
                .overridingErrorMessage("Expected account to be enabled but it was not")
                .isTrue();

        return this;
    }

    public ExampleUserDetailsAssert isRegisteredUser() {
        isNotNull();

        Assertions.assertThat(actual.getRole())
                .overridingErrorMessage( "Expected role to be <ROLE_USER> but was <%s>",
                        actual.getRole()
                )
                .isEqualTo(Role.ROLE_USER);

        Collection<? extends GrantedAuthority> authorities = actual.getAuthorities();

        Assertions.assertThat(authorities.size())
                .overridingErrorMessage( "Expected <1> granted authority but found <%d>",
                        authorities.size()
                )
                .isEqualTo(1);

        GrantedAuthority authority = authorities.iterator().next();

        Assertions.assertThat(authority.getAuthority())
                .overridingErrorMessage( "Expected authority to be <ROLE_USER> but was <%s>",
                        authority.getAuthority()
                )
                .isEqualTo(Role.ROLE_USER.name());

        return this;
    }

    public ExampleUserDetailsAssert isRegisteredByUsingFormRegistration() {
        isNotNull();

        Assertions.assertThat(actual.getSocialSignInProvider())
                .overridingErrorMessage( "Expected socialSignInProvider to be <null> but was <%s>",
                        actual.getSocialSignInProvider()
                )
                .isNull();

        return this;
    }

    public ExampleUserDetailsAssert isSignedInByUsingSocialSignInProvider(SocialMediaService socialSignInProvider) {
        isNotNull();

        Assertions.assertThat(actual.getSocialSignInProvider())
                .overridingErrorMessage( "Expected socialSignInProvider to be <%s> but was <%s>",
                        socialSignInProvider,
                        actual.getSocialSignInProvider()
                )
                .isEqualTo(socialSignInProvider);

        return this;
    }
}

Erstellen der SecurityContextAssert-Klasse

Wir können die zweite Kundenassertionsklasse erstellen, indem wir diesen Schritten folgen:

  1. Erstellen Sie ein SecurityContextAssert Klasse, die AbstractAssert erweitert Klasse. Geben Sie die folgenden Typparameter an:
    1. Der erste Typparameter ist der Typ der benutzerdefinierten Behauptung. Legen Sie den Wert dieses Typparameters auf SecurityContextAssert fest .
    2. Der zweite Typparameter ist der Typ des eigentlichen Wertobjekts. Legen Sie den Wert dieses Typparameters auf SecurityContext fest .
  2. Fügen Sie der erstellten Klasse einen privaten Konstruktor hinzu. Dieser Konstruktor übernimmt einen SecurityContext Objekt als Konstruktorargument. Implementieren Sie den Controller, indem Sie den Konstruktor der Oberklasse aufrufen und die folgenden Objekte als Konstruktorargumente übergeben:
    1. Das erste Konstruktorargument ist das eigentliche Wertobjekt. Übergeben Sie das als Konstruktorargument angegebene Objekt an den Konstruktor der Oberklasse.
    2. Das zweite Konstruktorargument ist eine Klasse -Objekt, das den Typ der benutzerdefinierten Assertion-Klasse angibt. Legen Sie den Wert dieses Konstruktorarguments auf SecurityContextAssert.class fest .
  3. Fügen Sie ein öffentliches statisches assertThat() hinzu -Methode auf die erstellte Klasse. Diese Methode akzeptiert einen SecurityContext Objekt als Methodenparameter. Implementieren Sie diese Methode, indem Sie ein neues SecurityContextAssert erstellen Objekt.
  4. Fügen Sie ein öffentliches userIsAnonymous() hinzu -Methode zum SecurityContextAssert Klasse, und implementieren Sie sie, indem Sie die folgenden Schritte ausführen:
    1. Stellen Sie sicher, dass der tatsächliche SecurityContext Objekte ist nicht null, indem isNotNull() aufgerufen wird Methode des AbstractAssert Klasse.
    2. Holen Sie sich die Authentifizierung Objekt aus dem SecurityContext und stellen Sie sicher, dass es null ist .
    3. Gib einen Verweis auf SecurityContextAssert zurück Objekt.
  5. Fügen Sie ein öffentliches loggedInUserIs() hinzu -Methode zum SecurityContextAssert Klasse. Diese Methode benötigt einen Benutzer Objekt (der erwartete angemeldete Benutzer) als Methodenparameter und gibt ein SecurityContextAssert zurück Objekt. Wir können diese Methode implementieren, indem wir die folgenden Schritte ausführen:
    1. Stellen Sie sicher, dass der tatsächliche SecurityContext Objekt ist nicht null durch Aufrufen von isNotNull() Methode des AbstractAssert Klasse.
    2. Holen Sie sich die ExampleUserDetails Objekt aus dem SecurityContext und stellen Sie sicher, dass es nicht null ist.
    3. Stellen Sie sicher, dass die Informationen der ExampleUserDetails Objekt ist gleich den Informationen des Benutzers Objekt.
    4. Gib einen Verweis auf SecurityContextAssert zurück Objekt.
  6. Fügen Sie ein öffentliches loggedInUserHasPassword() hinzu -Methode zum SecurityContextAssert Klasse. Diese Methode nimmt das erwartete Passwort als Methodenparameter und gibt ein SecurityContextAssert zurück Objekt. Wir können diese Methode implementieren, indem wir die folgenden Schritte ausführen:
    1. Stellen Sie sicher, dass der tatsächliche SecurityContext Objekt ist nicht null durch Aufrufen von isNotNull() Methode des AbstractAssert Klasse.
    2. Holen Sie das ExampleUserDetails-Objekt aus dem SecurityContext und stellen Sie sicher, dass es nicht null ist.
    3. Stellen Sie sicher, dass die ExampleUserDetails Das Passwortfeld des Objekts ist gleich dem Passwort, das als Methodenparameter angegeben wurde.
    4. Gib einen Verweis auf SecurityContextAssert zurück Objekt.
  7. Fügen Sie ein öffentliches loggedInUserIsRegisteredByUsingNormalRegistration() hinzu -Methode zum SecurityContextAssert Klasse und implementieren Sie sie, indem Sie die folgenden Schritte ausführen:
    1. Stellen Sie sicher, dass der tatsächliche SecurityContext Objekt ist nicht null durch Aufrufen von isNotNull() Methode des AbstractAssert Klasse.
    2. Holen Sie sich die ExampleUserDetails Objekt aus dem SecurityContext und stellen Sie sicher, dass es nicht null ist.
    3. Stellen Sie sicher, dass das Benutzerkonto durch die normale Registrierung erstellt wird.
    4. Gib einen Verweis auf SecurityContextAssert zurück Objekt.
  8. Fügen Sie ein öffentliches loggedInUserIsSignedInByUsingSocialProvider() hinzu -Methode zum SecurityContextAssert Klasse. Diese Methode nimmt einen SocialMediaService enum (der erwartete Anbieter für soziale Anmeldungen) als Methodenparameter und gibt ein SecurityContextAssert zurück Objekt. Wir können diese Methode implementieren, indem wir die folgenden Schritte ausführen:
    1. Stellen Sie sicher, dass der tatsächliche SecurityContext Objekt ist nicht null durch Aufrufen von isNotNull() Methode des AbstractAssert Klasse.
    2. Holen Sie sich die ExampleUserDetails Objekt aus dem SecurityContext und stellen Sie sicher, dass es nicht null ist.
    3. Stellen Sie sicher, dass das Benutzerkonto mithilfe des SociaMediaService erstellt wird als Methodenparameter angegeben.
    4. Gib einen Verweis auf SecurityContextAssert zurück Objekt.

Der Quellcode von SecurityContextAssert Klasse sieht wie folgt aus:

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;

public class SecurityContextAssert extends AbstractAssert<SecurityContextAssert, SecurityContext> {

    private SecurityContextAssert(SecurityContext actual) {
        super(actual, SecurityContextAssert.class);
    }

    public static SecurityContextAssert assertThat(SecurityContext actual) {
        return new SecurityContextAssert(actual);
    }

    public SecurityContextAssert userIsAnonymous() {
        isNotNull();

        Authentication authentication = actual.getAuthentication();

        Assertions.assertThat(authentication)
                .overridingErrorMessage("Expected authentication to be <null> but was <%s>.",
                        authentication
                )
                .isNull();

        return this;
    }

    public SecurityContextAssert loggedInUserIs(User user) {
        isNotNull();

        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();

        Assertions.assertThat(loggedIn)
                .overridingErrorMessage("Expected logged in user to be <%s> but was <null>",
                        user
                )
                .isNotNull();

        ExampleUserDetailsAssert.assertThat(loggedIn)
                .hasFirstName(user.getFirstName())
                .hasId(user.getId())
                .hasLastName(user.getLastName())
                .hasUsername(user.getEmail())
                .isActive()
                .isRegisteredUser();

        return this;
    }

    public SecurityContextAssert loggedInUserHasPassword(String password) {
        isNotNull();

        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();

        Assertions.assertThat(loggedIn)
                .overridingErrorMessage("Expected logged in user to be <not null> but was <null>")
                .isNotNull();

        ExampleUserDetailsAssert.assertThat(loggedIn)
                .hasPassword(password);

        return this;
    }

    public SecurityContextAssert loggedInUserIsRegisteredByUsingNormalRegistration() {
        isNotNull();

        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();

        Assertions.assertThat(loggedIn)
                .overridingErrorMessage("Expected logged in user to be <not null> but was <null>")
                .isNotNull();

        ExampleUserDetailsAssert.assertThat(loggedIn)
                .isRegisteredByUsingFormRegistration();

        return this;
    }

    public SecurityContextAssert loggedInUserIsSignedInByUsingSocialProvider(SocialMediaService signInProvider) {
        isNotNull();

        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();

        Assertions.assertThat(loggedIn)
                .overridingErrorMessage("Expected logged in user to be <not null> but was <null>")
                .isNotNull();

        ExampleUserDetailsAssert.assertThat(loggedIn)
                .hasPassword("SocialUser")
                .isSignedInByUsingSocialSignInProvider(signInProvider);

        return this;
    }
}

Erstellen der TestProviderSignInAttemptAssert-Klasse

Wir können die dritte benutzerdefinierte Assertion-Klasse erstellen, indem wir diesen Schritten folgen:

  1. Erstellen Sie ein TestProviderSignInAttemptAssert Klasse, die AbstractAssert erweitert Klasse. Geben Sie die folgenden Typparameter an:
    1. Der erste Typparameter ist der Typ der benutzerdefinierten Behauptung. Legen Sie den Wert dieses Typparameters auf TestProviderSignInAttemptAssert fest .
    2. Der zweite Typparameter ist der Typ des eigentlichen Wertobjekts. Legen Sie den Wert dieses Typparameters auf TestProviderSignInAttempt fest .
  2. Fügen Sie der erstellten Klasse einen privaten Konstruktor hinzu. Dieser Konstruktor akzeptiert einen TestProviderSignInAttempt Objekt als Konstruktorargument. Implementieren Sie den Controller, indem Sie den Konstruktor der Oberklasse aufrufen und die folgenden Objekte als Konstruktorargumente übergeben:
    1. Das erste Konstruktorargument ist das eigentliche Wertobjekt. Übergeben Sie das als Konstruktorargument angegebene Objekt an den Konstruktor der Oberklasse.
    2. Das zweite Konstruktorargument ist eine Klasse -Objekt, das den Typ der benutzerdefinierten Assertion-Klasse angibt. Legen Sie den Wert dieses Konstruktorarguments auf TestProviderSignInAttemptAssert.class fest .
  3. Fügen Sie ein öffentliches statisches assertThatSignIn() hinzu -Methode auf die erstellte Klasse. Diese Methode akzeptiert einen TestProviderSignInAttempt Objekt als Methodenparameter. Implementieren Sie diese Methode, indem Sie ein neues TestProviderSignInAttemptAssert erstellen Objekt.
  4. Fügen Sie ein öffentliches createdNoConnections() hinzu -Methode auf die erstellte Klasse. Diese Methode akzeptiert keine Methodenparameter und gibt eine Referenz auf TestProviderSignInAttemptAssert zurück Objekt. Wir können diese Methode implementieren, indem wir die folgenden Schritte ausführen:
    1. Stellen Sie sicher, dass der tatsächliche TestProviderSignInAttempt Objekt ist nicht null durch Aufrufen von isNotNull() Methode des AbstractAssert Klasse.
    2. Stellen Sie sicher, dass der tatsächliche TestProviderSignInAttempt Objekt hat keine Verbindungen erstellt.
    3. Gib einen Verweis auf TestProviderSignInAttemptAssert zurück Objekt.
  5. Fügen Sie eine öffentliche createdConnectionForUserId() hinzu -Methode auf die erstellte Klasse. Diese Methode nimmt die erwartete Benutzer-ID als Methodenparameter und gibt eine Referenz auf TestProviderSignInAttempt zurück Objekt. Wir können diese Methode implementieren, indem wir die folgenden Schritte ausführen:
    1. Stellen Sie sicher, dass der tatsächliche TestProviderSignInAttempt Objekt ist nicht null durch Aufrufen von isNotNull() Methode des AbstractAssert Klasse.
    2. Stellen Sie sicher, dass eine Verbindung für den Benutzer erstellt wurde, dessen Benutzer-ID als Methodenparameter angegeben wurde.
    3. Gib einen Verweis auf TestProviderSignInAttemptAssert zurück Objekt.

Der Quellcode von TestProviderSignInAttemptAssert Klasse sieht wie folgt aus:

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.springframework.social.connect.web.TestProviderSignInAttempt;

public class TestProviderSignInAttemptAssert extends AbstractAssert<TestProviderSignInAttemptAssert, TestProviderSignInAttempt> {

    private TestProviderSignInAttemptAssert(TestProviderSignInAttempt actual) {
        super(actual, TestProviderSignInAttemptAssert.class);
    }

    public static TestProviderSignInAttemptAssert assertThatSignIn(TestProviderSignInAttempt actual) {
        return new TestProviderSignInAttemptAssert(actual);
    }

    public TestProviderSignInAttemptAssert createdNoConnections() {
        isNotNull();

        Assertions.assertThat(actual.getConnections())
                .overridingErrorMessage( "Expected that no connections were created but found <%d> connection",
                        actual.getConnections().size()
                )
                .isEmpty();

        return this;
    }

    public TestProviderSignInAttemptAssert createdConnectionForUserId(String userId) {
        isNotNull();

        Assertions.assertThat(actual.getConnections())
                .overridingErrorMessage("Expected that connection was created for user id <%s> but found none.",
                        userId
                )
                .contains(userId);

        return this;
    }
}

Erstellen der RegistrationFormAssert-Klasse

Wir können die vierte benutzerdefinierte Assertion-Klasse erstellen, indem wir diesen Schritten folgen:

  1. Erstellen Sie ein RegistrationFormAssert Klasse, die AbstractAssert erweitert Klasse. Geben Sie die folgenden Typparameter an:
    1. Der erste Typparameter ist der Typ der benutzerdefinierten Behauptung. Legen Sie den Wert dieses Typparameters auf RegistrationFormAssert fest .
    2. Der zweite Typparameter ist der Typ des eigentlichen Wertobjekts. Setzen Sie den Wert dieses Typparameters auf RegistrationForm .
  2. Fügen Sie der erstellten Klasse einen privaten Konstruktor hinzu. Dieser Konstruktor übernimmt ein RegistrationForm Objekt als Konstruktorargument. Implementieren Sie den Controller, indem Sie den Konstruktor der Oberklasse aufrufen und die folgenden Objekte als Konstruktorargumente übergeben:
    1. Das erste Konstruktorargument ist das eigentliche Wertobjekt. Übergeben Sie das als Konstruktorargument angegebene Objekt an den Konstruktor der Oberklasse.
    2. Das zweite Konstruktorargument ist eine Klasse -Objekt, das den Typ der benutzerdefinierten Assertion-Klasse angibt. Legen Sie den Wert dieses Konstruktorarguments auf RegistrationFormAssert.class fest .
  3. Fügen Sie ein öffentliches statisches assertThatRegistrationForm() hinzu -Methode auf die erstellte Klasse. Diese Methode benötigt ein RegistrationForm Objekt als Methodenparameter. Implementieren Sie diese Methode, indem Sie ein neues RegistrationFormAssert zurückgeben Objekt.
  4. Fügen Sie eine öffentliche hasEmail() hinzu -Methode auf die erstellte Klasse. Diese Methode nimmt die erwartete E-Mail als Methodenparameter und gibt ein RegistrationFormAssert zurück Objekt.
  5. Fügen Sie einen öffentlichen hasFirstName() hinzu -Methode auf die erstellte Klasse. Diese Methode nimmt den erwarteten Vornamen als Methodenparameter und gibt ein RegistrationFormAssert zurück Objekt.
  6. Fügen Sie einen öffentlichen hasLastName() hinzu -Methode auf die erstellte Klasse. Diese Methode nimmt den erwarteten Nachnamen als Methodenparameter und gibt ein RegistrationFormAssert zurück Objekt.
  7. Fügen Sie ein öffentliches hasNoPassword() hinzu -Methode auf die erstellte Klasse. Diese Methode hat keine Methodenparameter und gibt ein RegistrationFormAssert zurück Objekt.
  8. Fügen Sie eine öffentliche hasNoPasswordVerification() hinzu -Methode auf die erstellte Klasse. Diese Methode akzeptiert keine Methodenparameter und gibt ein RegistrationFormAssert zurück Objekt.
  9. Fügen Sie ein öffentliches hasPassword() hinzu -Methode auf die erstellte Klasse. Diese Methode nimmt das erwartete Passwort als Methodenparameter und gibt ein RegistrationFormAssert zurück Objekt.
  10. Fügen Sie eine öffentliche hasPasswordVerification hinzu -Methode auf die erstellte Klasse. Diese Methode nimmt die erwartete Passwortüberprüfung als Methodenparameter und gibt ein RegistrationFormAssert zurück Objekt.
  11. Fügen Sie eine öffentliche isNormalRegistration() hinzu -Methode auf die erstellte Klasse. Diese Methode akzeptiert keine Methodenparameter und gibt ein RegistrationFormAssert zurück Objekt.
  12. Fügen Sie ein öffentliches isSocialSignInWithSignInProvider() hinzu -Methode auf die erstellte Klasse. Diese Methode nimmt einen SocialMediaService enum als Methodenparameter (der erwartete Anmeldeanbieter) und gibt ein RegistrationFormAssert zurück Objekt.

Der Quellcode des RegistrationFormAssert Klasse sieht wie folgt aus:

import org.assertj.core.api.AbstractAssert;

import static org.assertj.core.api.Assertions.assertThat;

public class RegistrationFormAssert extends AbstractAssert<RegistrationFormAssert, RegistrationForm> {

    private RegistrationFormAssert(RegistrationForm actual) {
        super(actual, RegistrationFormAssert.class);
    }

    public static RegistrationFormAssert assertThatRegistrationForm(RegistrationForm actual) {
        return new RegistrationFormAssert(actual);
    }

    public RegistrationFormAssert hasEmail(String email) {
        isNotNull();

        assertThat(actual.getEmail())
                .overridingErrorMessage("Expected email to be <%s> but was <%s>",
                        email,
                        actual.getEmail()
                )
                .isEqualTo(email);

        return this;
    }

    public RegistrationFormAssert hasFirstName(String firstName) {
        isNotNull();

        assertThat(actual.getFirstName())
                .overridingErrorMessage("Expected first name to be <%s> but was <%s>",
                        firstName,
                        actual.getFirstName()
                )
                .isEqualTo(firstName);

        return this;
    }

    public RegistrationFormAssert hasLastName(String lastName) {
        isNotNull();

        assertThat(actual.getLastName())
                .overridingErrorMessage("Expected last name to be <%s> but was <%s>",
                        lastName,
                        actual.getLastName())
                .isEqualTo(lastName);

        return this;
    }

    public RegistrationFormAssert hasNoPassword() {
        isNotNull();

        assertThat(actual.getPassword())
                .overridingErrorMessage("Expected password to be <null> but was <%s>",
                        actual.getPassword()
                )
                .isNull();

        return this;
    }

    public RegistrationFormAssert hasNoPasswordVerification() {
        isNotNull();

        assertThat(actual.getPasswordVerification())
                .overridingErrorMessage("Expected password verification to be <null> but was <%s>",
                        actual.getPasswordVerification()
                )
                .isNull();

        return this;
    }

    public RegistrationFormAssert hasPassword(String password) {
        isNotNull();

        assertThat(actual.getPassword())
                .overridingErrorMessage("Expected password to be <%s> but was <%s>",
                        password,
                        actual.getPassword()
                )
                .isEqualTo(password);

        return this;
    }

    public RegistrationFormAssert hasPasswordVerification(String passwordVerification) {
        isNotNull();

        assertThat(actual.getPasswordVerification())
                .overridingErrorMessage("Expected password verification to be <%s> but was <%s>",
                        passwordVerification,
                        actual.getPasswordVerification()
                )
                .isEqualTo(passwordVerification);

        return this;
    }

    public RegistrationFormAssert isNormalRegistration() {
        isNotNull();

        assertThat(actual.getSignInProvider())
                .overridingErrorMessage("Expected sign in provider to be <null> but was <%s>",
                        actual.getSignInProvider()
                )
                .isNull();

        return this;
    }

    public RegistrationFormAssert isSocialSignInWithSignInProvider(SocialMediaService signInProvider) {
        isNotNull();

        assertThat(actual.getSignInProvider())
                .overridingErrorMessage("Expected sign in provider to be <%s> but was <%s>",
                        signInProvider,
                        actual.getSignInProvider()
                )
                .isEqualTo(signInProvider);

        return this;
    }
}

Lassen Sie uns weitermachen und anfangen, einen Unit-Test für den RegistrationController zu schreiben Klasse.

Einheitentests schreiben

Wir haben jetzt unsere Vorbereitungen abgeschlossen und sind bereit, Unit-Tests für die Registrierungsfunktion zu schreiben. Wir müssen Komponententests für die folgenden Controller-Methoden schreiben:

  • Die erste Controller-Methode rendert die Registrierungsseite.
  • Die zweite Controller-Methode verarbeitet die Übermittlungen des Registrierungsformulars.

Bevor wir mit dem Schreiben unserer Unit-Tests beginnen können, müssen wir sie konfigurieren. Lassen Sie uns herausfinden, wie das gemacht wird.

Konfigurieren unserer Komponententests

Die Anwendungskontextkonfiguration unserer Beispielanwendung ist so gestaltet, dass es einfach ist, Unit-Tests für die Webschicht zu schreiben. Diese Gestaltungsprinzipien werden im Folgenden beschrieben:

  • Die Anwendungskontextkonfiguration ist in mehrere Konfigurationsklassen unterteilt und jede Klasse hat einen bestimmten Teil unserer Anwendung konfiguriert (Web, Sicherheit, Soziales und Persistenz).
  • Unsere Anwendungskontextkonfiguration hat eine "Haupt"-Konfigurationsklasse, die einige "allgemeine" Beans konfiguriert und die anderen Konfigurationsklassen importiert. Diese Konfigurationsklasse konfiguriert auch den Komponentenscan für die Serviceschicht.

Wenn wir den Anwendungskontext nach diesen Prinzipien konfigurieren, ist es einfach, die Anwendungskontextkonfiguration für unsere Komponententests zu erstellen. Wir können dies tun, indem wir die Anwendungskontext-Konfigurationsklasse wiederverwenden, die die Webschicht unserer Beispielanwendung konfiguriert, und eine neue Anwendungskontext-Konfigurationsklasse für unsere Komponententests erstellen.

Wir können die Anwendungskontext-Konfigurationsklasse für unsere Komponententests erstellen, indem wir diesen Schritten folgen:

  1. Erstellen Sie eine Klasse namens UnitTestContext .
  2. Kommentieren Sie die erstellte Klasse mit @Configuration Anmerkung.
  3. Fügen Sie eine messageSource() hinzu Methode zur erstellten Klasse und kommentieren Sie die Methode mit @Bean Anmerkung. Konfigurieren Sie die MessageSource bean by following these steps:
    1. Create a new ResourceBundleMessageSource Objekt.
    2. Set the base name of the message files and ensure that if a message is not found, its code is returned.
    3. Return the created object.
  4. Add a userService() method to the created class and annotate the method with the @Bean Anmerkung. Configure the UserService mock object by following these steps:
    1. Call the static mock() method of the Mockito class, and pass UserService.class as a method parameter.
    2. Return the created object.

The source code of the UnitTestContext Klasse sieht wie folgt aus:

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;

import static org.mockito.Mockito.mock;

@Configuration
public class UnitTestContext {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();

        messageSource.setBasename("i18n/messages");
        messageSource.setUseCodeAsDefaultMessage(true);

        return messageSource;
    }

    @Bean
    public UserService userService() {
        return mock(UserService.class);
    }
}

The next thing that we have to do is to configure our unit tests. Wir können dies tun, indem wir diesen Schritten folgen:

  1. Annotate the test class with the @RunWith annotation and ensure that our tests are executed by using the SpringUnit4ClassRunner .
  2. Annotate the class with the @ContextConfiguration annotation, and ensure that the correct configuration classes are used. In our case, the correct configuration classes are:WebAppContext and UnitTestContext .
  3. Annotate the class with the @WebAppConfiguration Anmerkung. This annotation ensures that the loaded application context is a WebApplicationContext .
  4. Fügen Sie einen MockMvc hinzu field to the test class.
  5. Add a WebApplicationContext field to the class and annotate it with the @Autowired Anmerkung.
  6. Add a UserService field to the test class and annotate it with the @Autowired Anmerkung.
  7. Add a setUp() method to the test class and annotate the method with the @Before Anmerkung. This ensures that the method is called before each test method. Implement this method by following these steps:
    1. Reset the UserService mock by calling the static reset() method of the Mockito class and passing the reseted mock as a method parameter.
    2. Create a new MockMvc object by using the MockMvcBuilders Klasse.
    3. Ensure that no Authentication object is found from the SecurityContext when our tests are run. We can do this by following these steps:
      1. Obtain a reference to the SecurityContext object by calling the static getContext() method of the SecurityContextHolder Klasse.
      2. Clear the authentication by calling the setAuthentication() method of the SecurityContext Klasse. Pass null as a method parameter.

The source code of our unit test class looks as follows:

import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest2 {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    @Before
    public void setUp() {
        Mockito.reset(userServiceMock);

        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
                .build();
				
		SecurityContextHolder.getContext().setAuthentication(null);
    }
}

Let's move on and write unit tests for a controller method which renders the registration form.

Rendering the Registration Form

The controller method which renders the registration form has one important responsibility:

If the user is using social sign in, the fields of the registration are pre-populated by using the information which is used provided by the used SaaS API provider.

Let's refresh our memory and take a look at the source code of the RegistrationController Klasse:

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionKey;
import org.springframework.social.connect.UserProfile;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.request.WebRequest;

@Controller
@SessionAttributes("user")
public class RegistrationController {

    @RequestMapping(value = "/user/register", method = RequestMethod.GET)
    public String showRegistrationForm(WebRequest request, Model model) {
        Connection<?> connection = ProviderSignInUtils.getConnection(request);

        RegistrationForm registration = createRegistrationDTO(connection);
        model.addAttribute("user", registration);

        return "user/registrationForm";
    }

    private RegistrationForm createRegistrationDTO(Connection<?> connection) {
        RegistrationForm dto = new RegistrationForm();

        if (connection != null) {
            UserProfile socialMediaProfile = connection.fetchUserProfile();
            dto.setEmail(socialMediaProfile.getEmail());
            dto.setFirstName(socialMediaProfile.getFirstName());
            dto.setLastName(socialMediaProfile.getLastName());

            ConnectionKey providerKey = connection.getKey();
            dto.setSignInProvider(SocialMediaService.valueOf(providerKey.getProviderId().toUpperCase()));
        }

        return dto;
    }
}

It is clear that we have to write two unit tests for this controller method:

  1. We have to write a test which ensures that the controller method is working properly when the user is using "normal" registration.
  2. We have to write a test which ensures that the controller method is working properly when the user is using social sign in.

Let's move and write these unit tests.

Test 1:Rendering a Normal Registration Form

We can write the first unit test by following these steps:

  1. Führen Sie ein GET aus request to url '/user/register'.
  2. Ensure that the HTTP status code 200 is returned.
  3. Verify that the name of the rendered view is 'user/registrationForm'.
  4. Verify that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  5. Ensure that all fields of the model attribute called 'user' are either null or empty.
  6. Verify that no methods of the UserService mock were called.

Der Quellcode unseres Unit-Tests sieht wie folgt aus:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity

    @Test
    public void showRegistrationForm_NormalRegistration_ShouldRenderRegistrationPageWithEmptyForm() throws Exception {
        mockMvc.perform(get("/user/register"))
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", isEmptyOrNullString()),
                        hasProperty("firstName", isEmptyOrNullString()),
                        hasProperty("lastName", isEmptyOrNullString()),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", isEmptyOrNullString())
                )));

        verifyZeroInteractions(userServiceMock);
    }
}

Test 2:Rendering the Registration Form by Using Social Sign In

We can write the second unit test by following these steps:

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder Klasse. Set the provider id, first name, last name and email address.
  2. Execute a GET request to url '/user/register' and set the created TestProviderSignInAttempt object to the HTTP session.
  3. Ensure that the HTTP status code 200 is returned.
  4. Verify that the name of the rendered view is 'user/registrationForm'.
  5. Ensure that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  6. Verify that the fields of the model object called 'user' are pre-populated by using the information contained by the TestProviderSignInAttempt Objekt. We can do this by following these steps:
    1. Ensure that the value of the email field is '[email protected]'.
    2. Ensure that the value of the firstName field is 'John'.
    3. Ensure that the value of the lastName field is 'Smith'.
    4. Ensure that the value of the password field is empty or null String.
    5. Ensure that the value of the passwordVerification field is empty or null String.
    6. Ensure that the value of the signInProvider field is 'twitter'.
  7. Verify that the methods of the UserService interface were not called.

Der Quellcode unseres Unit-Tests sieht wie folgt aus:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity

    @Test
    public void showRegistrationForm_SocialSignInWithAllValues_ShouldRenderRegistrationPageWithAllValuesSet() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("[email protected]")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        mockMvc.perform(get("/user/register")
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
        )
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", is("[email protected]")),
                        hasProperty("firstName", is("John")),
                        hasProperty("lastName", is("Smith")),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", is("twitter"))
                )));

        verifyZeroInteractions(userServiceMock);
    }
}

Submitting The Registration Form

The controller method which processes the submissions of the registration form has the following responsibilities:

  1. It validates the information entered to the registration form. If the information is not valid, it renders the registration form and shows validation error messages to user.
  2. If the email address given by the user is not unique, it renders the registration form and shows an error message to the user.
  3. It creates a new user account by using the UserService interface and logs the created user in.
  4. It persists the connection to a SaaS API provider if user was using social sign in
  5. It redirects user to the front page.

The relevant part of the RegistrationController Klasse sieht wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.request.WebRequest;

import javax.validation.Valid;

@Controller
@SessionAttributes("user")
public class RegistrationController {

    private UserService service;

    @Autowired
    public RegistrationController(UserService service) {
        this.service = service;
    }

    @RequestMapping(value ="/user/register", method = RequestMethod.POST)
    public String registerUserAccount(@Valid @ModelAttribute("user") RegistrationForm userAccountData,
                                      BindingResult result,
                                      WebRequest request) throws DuplicateEmailException {
        if (result.hasErrors()) {
            return "user/registrationForm";
        }

        User registered = createUserAccount(userAccountData, result);

        if (registered == null) {
            return "user/registrationForm";
        }
        SecurityUtil.logInUser(registered);
        ProviderSignInUtils.handlePostSignUp(registered.getEmail(), request);

        return "redirect:/";
    }

    private User createUserAccount(RegistrationForm userAccountData, BindingResult result) {
        User registered = null;

        try {
            registered = service.registerNewUserAccount(userAccountData);
        }
        catch (DuplicateEmailException ex) {
            addFieldError(
                    "user",
                    "email",
                    userAccountData.getEmail(),
                    "NotExist.user.email",
                    result);
        }

        return registered;
    }

    private void addFieldError(String objectName, String fieldName, String fieldValue,  String errorCode, BindingResult result) {
        FieldError error = new FieldError(
                objectName,
                fieldName,
                fieldValue,
                false,
                new String[]{errorCode},
                new Object[]{},
                errorCode
        );

        result.addError(error);
    }
}

We will write three unit tests for this controller method:

  1. We write a unit test which ensures that the controller method is working properly when validation fails.
  2. We write a unit test which ensures the the controller method is working when the email address isn't unique.
  3. We write a unit test which ensures that the controller method is working properly when the registration is successful.

Let's find out how we can write these unit tests.

Test 1:Validierung schlägt fehl

We can write the first unit test by following these steps:

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder Klasse. Set the provider id, first name, last name and email address.
  2. Führen Sie einen POST aus request to url '/user/register' by following these steps:
    1. Set the content type of the request to 'application/x-www-form-urlencoded'.
    2. Send the value of the signInProvider form field as a request parameter.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set a new RegistrationForm object to the HTTP session. This is required because our controller class is annotated with the @SessionAttributes Anmerkung.
  3. Vergewissern Sie sich, dass der HTTP-Statuscode 200 zurückgegeben wird.
  4. Ensure that the name of the rendered view is 'user/registrationForm'.
  5. Ensure that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  6. Verify that field values of the model object called 'user' are correct by following these steps:
    1. Verify that the value of the email field is empty or null String.
    2. Verify that the value of the firstName field is empty or null String.
    3. Verify that the value of the lastName field is empty or null String.
    4. Verify that the value of the password field is empty or null String.
    5. Verify that the value of the passwordVerification field is empty or null String.
    6. Verify that the value of the signInProvider field is SocialMediaService.TWITTER .
  7. Ensure that the model attribute called 'user' has field errors in email , firstName , and lastName Felder.
  8. Verify that the current user is not logged in.
  9. Ensure that no connections were created by using the TestProviderSignInAttempt Objekt.
  10. Verify that the methods of the UserService mock were not called.

Der Quellcode unseres Unit-Tests sieht wie folgt aus:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity

    @Test
    public void registerUserAccount_SocialSignInAndEmptyForm_ShouldRenderRegistrationFormWithValidationErrors() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("[email protected]")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("signInProvider", SocialMediaService.TWITTER.name())
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", new RegistrationForm())
        )
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", isEmptyOrNullString()),
                        hasProperty("firstName", isEmptyOrNullString()),
                        hasProperty("lastName", isEmptyOrNullString()),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", is(SocialMediaService.TWITTER))
                )))
                .andExpect(model().attributeHasFieldErrors("user", 
						"email", 
						"firstName", 
						"lastName"
				));

        assertThat(SecurityContextHolder.getContext()).userIsAnonymous();
        assertThatSignIn(socialSignIn).createdNoConnections();
        verifyZeroInteractions(userServiceMock);
    }
}

Test 2:Email Address Is Found From the Database

We can write the second unit test by following these steps:

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder Klasse. Set the provider id, first name, last name and email address.
  2. Configure the UserService mock to throw a DuplicateEmailException when its registerNewUserAccount() method is called and a RegistrationForm object is given as a method parameter.
  3. Führen Sie einen POST aus request to url '/user/register' by following these steps:
    1. Set the content type of the request to 'application/x-www-form-urlencoded'.
    2. Send the values of the email , firstName , Nachname , and signInProvider form fields as request parameters.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set a new RegistrationForm object to the HTTP session. This is required because our controller class is annotated with the @SessionAttributes Anmerkung.
  4. Vergewissern Sie sich, dass der HTTP-Statuscode 200 zurückgegeben wird.
  5. Ensure that the name of the rendered view is 'user/registrationForm'.
  6. Ensure that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  7. Verify that field values of the model object called 'user' are correct by following these steps:
    1. Ensure that the value of the email field is '[email protected]'.
    2. Ensure that the value of the firstName field is 'John'.
    3. Ensure that the value of the lastName field is 'Smith'.
    4. Ensure that the value of the password field is empty or null String.
    5. Ensure that the value of the passwordVerification field is empty or null String.
    6. Ensure that the value of the signInProvider field is SocialMediaService.TWITTER .
  8. Ensure that the model attribute called 'user' has field error in email Feld.
  9. Verify that the current user is not logged in.
  10. Ensure that no connections were created by using the TestProviderSignInAttempt Objekt.
  11. Verify that the registerNewUserAccount() method of the UserService mock was called once and that a RegistrationForm object was given as a method parameter. Capture the method argument by using an ArgumentCaptor .
  12. Verify that the other methods of the UserService interface weren’t invoked during the test.
  13. Verify that the information of the RegistrationForm object, which was passed to our service method, is correct.

Der Quellcode unseres Unit-Tests sieht wie folgt aus:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity.

    @Test
    public void registerUserAccount_SocialSignInAndEmailExist_ShouldRenderRegistrationFormWithFieldError() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("[email protected]")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        when(userServiceMock.registerNewUserAccount(isA(RegistrationForm.class))).thenThrow(new DuplicateEmailException(""));

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
 			   	.param("email", "[email protected]")
                .param("firstName", "John")
                .param("lastName", "Smith")
                .param("signInProvider", SocialMediaService.TWITTER.name())
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", new RegistrationForm())
        )
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", is("[email protected]")),
                        hasProperty("firstName", is("John")),
                        hasProperty("lastName", is("Smith")),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", is(SocialMediaService.TWITTER))
                )))
                .andExpect(model().attributeHasFieldErrors("user", "email"));

        assertThat(SecurityContextHolder.getContext()).userIsAnonymous();
        assertThatSignIn(socialSignIn).createdNoConnections();

		ArgumentCaptor<RegistrationForm> registrationFormArgument = ArgumentCaptor.forClass(RegistrationForm.class);
		verify(userServiceMock, times(1)).registerNewUserAccount(registrationFormArgument.capture());
		verifyNoMoreInteractions(userServiceMock);

		RegistrationForm formObject = registrationFormArgument.getValue();
		assertThatRegistrationForm(formObject)
				.isSocialSignInWithSignInProvider(SocialMediaService.TWITTER)
				.hasEmail("[email protected]")
				.hasFirstName("John")
				.hasLastName("Smith")
				.hasNoPassword()
				.hasNoPasswordVerification();
    }
}

Test 3:Registration Is Successful

We can write the third unit test by following these steps:

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder Klasse. Set the provider id, first name, last name and email address.
  2. Create a new User object by using the UserBuilder Klasse. Set the values of the id , email , firstName , Nachname , and signInProvider Felder.
  3. Configure the UserService mock object to return the created User object when its registerNewUserAccount() method is called and a RegistrationForm object is given as a method parameter.
  4. Führen Sie einen POST aus request to url '/user/register' by following these steps:
    1. Set the content type of the request to 'application/x-www-form-urlencoded'.
    2. Send the values of the email , firstName , Nachname , and signInProvider form fields as request parameters.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set a new RegistrationForm object to the HTTP session. This is required because our controller class is annotated with the @SessionAttributes Anmerkung.
  5. Verify that the HTTP status code 302 is returned.
  6. Ensure that the request is redirected to url '/'.
  7. Verify that the created user is logged in by using Twitter.
  8. Verify that the TestProviderSignInAttempt object was used to created a connection for a user with email address '[email protected]'.
  9. Verify that the registerNewUserAccount() method of the UserService mock was called once and that a RegistrationForm object was given as a method parameter. Capture the method argument by using an ArgumentCaptor .
  10. Verify that the other methods of the UserService interface weren’t invoked during the test.
  11. Verify that the information of the RegistrationForm object, which was passed to our service method, is correct.

Der Quellcode unseres Unit-Tests sieht wie folgt aus:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity.

    @Test
    public void registerUserAccount_SocialSignIn_ShouldCreateNewUserAccountAndRenderHomePage() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("[email protected]")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        User registered = new UserBuilder()
                .id(1L)
                .email("[email protected]")
                .firstName("John")
                .lastName("Smith")
                .signInProvider(SocialMediaService.TWITTER)
                .build();

        when(userServiceMock.registerNewUserAccount(isA(RegistrationForm))).thenReturn(registered);

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
 			   	.param("email", "[email protected]")
                .param("firstName", "John")
                .param("lastName", "Smith")
                .param("signInProvider", SocialMediaService.TWITTER.name())
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", new RegistrationForm())
        )
                .andExpect(status().isMovedTemporarily())
                .andExpect(redirectedUrl("/"));

        assertThat(SecurityContextHolder.getContext())
                .loggedInUserIs(registered)
                .loggedInUserIsSignedInByUsingSocialProvider(SocialMediaService.TWITTER);
        assertThatSignIn(socialSignIn).createdConnectionForUserId("[email protected]");

		ArgumentCaptor<RegistrationForm> registrationFormArgument = ArgumentCaptor.forClass(RegistrationForm.class);
		verify(userServiceMock, times(1)).registerNewUserAccount(registrationFormArgument.capture());
		verifyNoMoreInteractions(userServiceMock);

		RegistrationForm formObject = registrationFormArgument.getValue();
		assertThatRegistrationForm(formObject)
				.isSocialSignInWithSignInProvider(SocialMediaService.TWITTER)
				.hasEmail("[email protected]")
				.hasFirstName("John")
				.hasLastName("Smith")
				.hasNoPassword()
				.hasNoPasswordVerification();
    }
}

Zusammenfassung

We have now written some unit tests for the registration function of our example application. Dieser Blogbeitrag hat uns vier Dinge gelehrt:

  1. We learned how we can create the test doubles required by our unit tests.
  2. We learned to emulate social sign in by using the created test double classes.
  3. We learned how we can verify that the connection to the used SaaS API provider is persisted after a new user account has been created for a user who used social sign in.
  4. We learned how we can verify that the user is logged in after a new user account has been created.

The example application of this blog post has many tests which were not covered in this blog post. If you are interested to see them, you can get the example application from Github.

P.S. This blog post describes one possible approach for writing unit tests to a registration controller which uses Spring Social 1.1.0. If you have any improvement ideas, questions, or feedback about my approach, feel free to leave a comment to this blog post.


Java-Tag