Статьи

Добавление входа в социальные сети в веб-приложение Spring MVC: модульное тестирование

Spring Social 1.0 имеет модуль spring-social-test, который обеспечивает поддержку для тестирования реализаций Connect и привязок API. Этот модуль был удален из Spring Social 1.1.0 и заменен на среду Spring MVC Test.

Проблема в том, что практически нет информации о написании модульных тестов для приложения, которое использует Spring Social 1.1.0.

Эта запись блога исправляет эту проблему .

В этом посте мы узнаем, как написать модульные тесты для функции регистрации нашего примера приложения, которое мы создали в предыдущих частях этого учебника Spring Social.

Примечание. Если вы не читали предыдущие части моего учебного пособия по Spring Social, я рекомендую прочитать их перед прочтением этого сообщения в блоге. Предварительные условия этого сообщения в блоге описаны ниже:

Давайте начнем с выяснения, как мы можем получить необходимые тестовые приличия с Maven.

Получение необходимых зависимостей с Maven

Мы можем получить необходимые тестовые зависимости, объявив следующие зависимости в нашем файле POM:

  • FEST-Assert (версия 1.4). FEST-Assert — это библиотека, которая обеспечивает свободный интерфейс для написания утверждений.
  • Hamcrest-All (версия 1.4). Мы используем средства сравнения Hamcrest для написания утверждений в наших модульных тестах.
  • JUnit (версия 4.11). Нам также нужно исключить ядро hamcrest, потому что мы уже добавили зависимость hamcrest-all .
  • mockito-all (версия 1.9.5). Мы используем Mockito в качестве нашей библиотеки для насмешек.
  • Catch-Exception (версия 1.2.0). Библиотека перехвата исключений помогает нам перехватывать исключения, не прерывая выполнение наших тестовых методов, и делает перехваченные исключения доступными для дальнейшего анализа. Нам нужно исключить зависимость mockito-core, потому что мы уже добавили зависимость mockito-all .
  • Spring Test (версия 3.2.4.RELEASE). Spring Test Framework — это фреймворк, который позволяет писать тесты для приложений на базе Spring.

Соответствующая часть файла pom.xml выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<dependency>
    <groupId>org.easytesting</groupId>
    <artifactId>fest-assert</artifactId>
    <version>1.4</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>

Давайте переместимся и взглянем под капот Spring Social.

Глядя под капот Весеннего Социального

Как мы помним из второй части этого руководства , класс RegistrationController отвечает за визуализацию регистрационной формы и обработку отправленных форм регистрационной формы. Он использует класс ProviderSignInUtils для двух целей:

  1. Когда форма регистрации отображается, класс RegistrationController предварительно заполняет поля формы, если пользователь создает новую учетную запись пользователя, используя социальный вход. Объект формы предварительно заполняется с использованием информации, предоставленной используемым поставщиком API SaaS. Эта информация хранится в объекте Connection . Класс контроллера получает объект Connection , вызывая статический метод getConnection () класса ProviderSignInUtils .
  2. После создания новой учетной записи пользователя класс RegistrationConnection сохраняет объект Connection в базе данных, если учетная запись пользователя была создана с помощью социального входа. Класс контроллера делает это путем вызова метода handlePostSignUp () класса ProviderSignInUtils .

Если мы хотим понять роль класса ProviderSignInUtils , мы взглянем на его исходный код. Исходный код класса ProviderSignInUtils выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
    }
}

Мы можем видеть две вещи из исходного кода класса ProviderSignInUtils :

  1. Метод getConnection () получает объект ProviderSignInAttempt из сеанса. Если полученный объект является нулем, он возвращает ноль. В противном случае он вызывает метод getConnection () класса ProviderSignInAttempt и возвращает объект Connection .
  2. Метод handlePostSignUp () получает объект ProviderSignInAttempt из сеанса. Если объект найден, он вызывает метод addConnection () класса ProviderSignInAttempt и удаляет найденный объект ProviderSignInAttempt из сеанса.

Понятно, что для написания модульных тестов для класса RegistrationController нам нужно найти способ создания объектов ProviderSignInAttempt и установить сессионные созданные объекты.

Давайте выясним, как это делается.

Создание тестовых пар

Как мы выяснили, если мы хотим написать модульные тесты для класса RegistrationController , мы должны найти способ создания объектов ProviderSignInAttempt . В этом разделе описывается, как мы можем достичь этой цели, используя удвоение теста.

Давайте продолжим и выясним, как мы можем создавать объекты ProviderSignInAttempt в наших модульных тестах.

Создание объектов ProviderSignInAttempt

Если мы хотим понять, как мы можем создавать объекты ProviderSignInAttempt , мы должны более внимательно посмотреть на его исходный код. Исходный код класса ProviderSignInAttempt выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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());
    }
}

Как мы видим, у класса ProviderSignInAttempt есть три зависимости, которые описаны ниже:

  • Интерфейс подключения представляет собой соединение с используемым поставщиком SaaS API.
  • Интерфейс ConnectionFactoryLocator указывает методы, необходимые для поиска объектов ConnectionFactory .
  • Интерфейс UsersConnectionRepository объявляет методы, которые используются для управления соединениями между пользователем и поставщиком API SaaS.

Первое, что приходит на ум, — это издеваться над этими зависимостями. Хотя это может показаться хорошей идеей, у этого подхода есть две проблемы:

  1. Мы должны были бы настроить поведение наших фиктивных объектов в каждом тесте, который мы пишем. Это означает, что наши тесты будет сложнее понять.
  2. Мы внедряем детали реализации Spring Social в наши тесты. Это усложнит обслуживание наших тестов, потому что если реализация Spring Social изменится, наши тесты могут быть нарушены.

Понятно, что издевательство — не лучшее решение этой проблемы. Мы должны помнить, что, хотя насмешка является ценным и удобным инструментом тестирования, мы не должны злоупотреблять им .

Это создает новый вопрос:

Если о насмешках не может быть и речи, какой инструмент лучше всего подойдет для этой работы?

Ответ на этот вопрос можно найти в статье, написанной Мартином Фаулером . В этой статье Мартин Фаулер определяет двойной тест, называемый заглушкой, следующим образом:

Заглушки обеспечивают постоянные ответы на вызовы, сделанные во время теста, обычно вообще не реагируя ни на что, кроме того, что запрограммировано для теста. Заглушки могут также записывать информацию о вызовах, такую ​​как заглушка шлюза электронной почты, которая запоминает сообщения, которые она «отправила», или, возможно, только сколько сообщений она «отправила».

Использование заглушки имеет смысл, потому что нас интересуют две вещи:

  1. Нам нужно настроить объект Connection <?>, Возвращаемый нашей заглушкой.
  2. Нам необходимо убедиться, что соединение с базой данных было сохранено после создания новой учетной записи пользователя.

Мы можем создать заглушку, которая выполняет эти цели, выполнив следующие шаги:

  1. Создайте класс TestProviderSignInAttempt, который расширяет класс ProviderSignInAttempt .
  2. Добавьте частное поле подключения к классу и установите тип добавленного поля Connection <?> . Это поле содержит ссылку на соединение между пользователем и поставщиком SaaS API.
  3. Добавьте частное поле соединений в класс и установите тип добавляемого поля в Set <String> . Это поле содержит идентификаторы пользователей постоянных соединений.
  4. Добавьте конструктор, который принимает объект Connection <?> В качестве аргумента конструктора, к созданному классу. Реализуйте конструктор, выполнив следующие действия:
    1. Вызовите конструктор класса ProviderSignInAttempt и передайте объект Connection <?> В качестве аргумента конструктора. Установите значения других аргументов конструктора в null .
    2. Установите объект Connection <?>, Указанный в качестве аргумента конструктора, в поле подключения .
  5. Переопределите метод getConnection () класса ProviderSignInAttempt и реализуйте его, возвращая объект, сохраненный в поле подключения .
  6. Переопределите метод addConnection (String userId) класса ProviderSignInAttempt и реализуйте его, добавив идентификатор пользователя, заданный в качестве параметра метода, в набор соединений .
  7. Добавьте открытый метод getConnections () в созданный класс и реализуйте его, возвращая набор соединений .

Исходный код TestProviderSignInAttempt выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
    }
}

Давайте продолжим и выясним, как мы можем создать класс Connection <?>, Который используется в наших модульных тестах.

Создание класса соединения

Созданный класс соединения является классом-заглушкой, который имитирует поведение «реальных» классов соединения, но он не реализует никакой логики, связанной с соединениями OAuth1 и OAuth2 . Также этот класс должен реализовывать интерфейс Connection .

Мы можем создать этот класс заглушки, выполнив следующие действия:

  1. Создайте класс TestConnection, который расширяет класс AbstractConnection . Класс AbstractConnection является базовым классом, который определяет состояние и поведение, общие для всех реализаций соединения.
  2. Добавьте поле connectionData в созданный класс. Установите тип поля в ConnectionData . ConnectionData — это объект передачи данных, который содержит внутреннее состояние соединения с используемым поставщиком SaaS API.
  3. Добавьте поле userProfile в созданный класс. Установите тип поля UserProfile . Этот класс представляет профиль пользователя используемого поставщика API SaaS и содержит информацию, которая совместно используется различными поставщиками услуг.
  4. Создайте конструктор, который принимает объекты ConnectionData и UserProfile в качестве аргументов конструктора, и реализуйте его, выполнив следующие действия:
    1. Вызовите конструктор класса AbstractConnection и передайте объект ConnectionData в качестве первого аргумента конструктора. Установите для второго аргумента конструктора значение null .
    2. Установите значение поля connectionData .
    3. Установите значение поля userProfile .
  5. Переопределите метод fetchUserProfile () класса AbstractConnection и реализуйте его, возвращая объект, сохраненный в поле userProfile .
  6. Переопределите метод getAPI () класса AbstractConnection и реализуйте его, возвращая значение null .
  7. Переопределите метод createData () класса AbstractConnection и реализуйте его, возвращая объект, сохраненный в поле connectionData .

Исходный код класса TestConnection выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
    }
}

Давайте продолжим и выясним, как мы можем создать эти тестовые двойники в наших модульных тестах.

Создание класса Builder

Теперь мы создали классы-заглушки для наших модульных тестов. Наш последний шаг — выяснить, как мы можем создавать объекты TestProviderSignInAttempt с помощью этих классов.

На данный момент мы знаем, что

  1. Конструктор класса TestProviderSignInAttempt принимает объект Connection в качестве аргумента конструктора.
  2. Конструктор класса TestConnection принимает объекты ConnectionData и UserProfile в качестве аргументов конструктора.

Это означает, что мы можем создать новые объекты TestProviderSignInAttempt , выполнив следующие действия:

  1. Создайте новый объект ConnectionData . Класс ConnectionData имеет единственный конструктор, который принимает обязательные поля в качестве аргументов конструктора.
  2. Создайте новый объект UserProfile . Мы можем создавать новые объекты UserProfile , используя класс UserProfileBuilder .
  3. Создайте новый объект TestConnection и передайте созданные объекты ConnectionData и UserProfile в качестве аргументов конструктора.
  4. Создайте новый объект TestProviderSignInAttempt и передайте созданный объект TestConnectionConnection в качестве аргумента конструктора.

Исходный код, который создает новый объект TestProviderSignInAttempt, выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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);

Хорошей новостью является то, что теперь мы знаем, как мы можем создавать объекты TestProviderSignInAttempt в наших тестах. Плохая новость заключается в том, что мы не можем использовать этот код в наших тестах.

Мы должны помнить, что мы не пишем модульные тесты просто для того, чтобы наш код работал должным образом. Каждый тестовый пример должен также показать, как наш код ведет себя в конкретной ситуации. Если мы создаем TestProviderSignInAttempt , добавляя этот код в каждый тестовый пример, мы уделяем слишком много внимания созданию объектов, необходимых для наших тестовых случаев. Это означает, что тестовый пример сложнее для чтения, и «сущность» тестового примера потеряна.

Вместо этого мы создадим класс построителя тестовых данных, который предоставляет свободный API для создания объектов TestProviderSignInAttempt . Мы можем создать этот класс, выполнив следующие действия:

  1. Создайте класс с именем TestProviderSignInAttemptBuilder .
  2. Добавьте все поля, необходимые для создания новых объектов ConnectionData и UserProfile, в класс TestProviderSignInAttemptBuilder .
  3. Добавьте методы, которые используются для установки значений полей добавленных полей. Реализуйте каждый метод, выполнив следующие действия:
    1. Установите значение, указанное в качестве параметра метода, в правильное поле.
    2. Вернуть ссылку на объект TestProviderSignInAttemptBuilder .
  4. Добавьте методы connectionData () и userProfile () в класс TestProviderSignInAttemptBuilder . Эти методы просто возвращают ссылку на объект TestProviderSignInAttemptBuilder , и их цель — сделать наш API более читабельным.
  5. Добавьте метод build () в класс построителя тестовых данных. Это создает объект TestProviderSignInAttempt , выполнив шаги, описанные ранее, и возвращает созданный объект.

Исходный код класса TestProviderSignInAttemptBuilder выглядит следующим образом:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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);
    }
}

Примечание. Нам не нужно вызывать все методы этого класса компоновщика, когда мы пишем модульные тесты для класса RegistrationController . Я добавил эти поля главным образом потому, что они будут полезны при написании интеграционных тестов для нашего примера приложения.

Код, который создает новый объект TestProviderSignInAttempt, теперь намного чище и удобочитаемее:

1
2
3
4
5
6
7
8
TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("email")
                    .firstName("firstName")
                    .lastName("lastName")
                .build();

Давайте продолжим и выясним, как мы можем очистить наши модульные тесты с помощью пользовательских утверждений FEST-Assert .

Создание пользовательских утверждений

Мы можем очистить наши модульные тесты, заменив стандартные утверждения JUnit пользовательскими утверждениями FEST-Assert. Мы должны создать три пользовательских класса утверждений, которые описаны ниже:

  • Первый класс утверждений используется для записи утверждений для объектов ExampleUserDetails . Класс ExampleUserDetails содержит информацию о зарегистрированном пользователе, которая хранится в SecurityContext нашего приложения. Другими словами, утверждения, предоставленные этим классом, используются для проверки правильности информации вошедшего в систему пользователя.
  • Второй класс утверждений используется для записи утверждений для объектов SecurityContext . Этот класс используется для записи утверждений для пользователя, чья информация хранится в SecurityContext .
  • Третий класс утверждений используется для записи утверждений для объектов TestProviderSignInAttempt . Этот класс утверждений используется для проверки того, было ли создано соединение с поставщиком API SaaS с помощью объекта TestProviderSignInAttempt .

Примечание. Если вы не знакомы с FEST-Assert, вам следует прочитать мой пост в блоге, в котором объясняется, как создавать пользовательские утверждения с помощью FEST-Assert , и почему вам следует подумать об этом.

Давайте двигаться дальше.

Создание класса ExampleUserDetailsAssert

Мы можем реализовать первый пользовательский класс утверждения, выполнив следующие действия:

  1. Создайте класс ExampleUserDetailsAssert, который расширяет класс GenericAssert . Укажите следующие параметры типа:
    1. Первый параметр типа — это тип пользовательского утверждения. Установите значение этого параметра типа для ExampleUserDetailsAssert .
    2. Второй параметр типа — это тип объекта фактического значения. Установите значение этого параметра типа в ExampleUserDetails.
  2. Добавьте приватный конструктор в созданный класс. Этот конструктор принимает объект ExampleUserDetails в качестве аргумента конструктора. Реализуйте контроллер, вызвав конструктор суперкласса и передав следующие объекты в качестве аргументов конструктора:
    1. Первый аргумент конструктора — это объект Class, который определяет тип пользовательского класса утверждения. Установите значение этого аргумента конструктора в ExampleUserDetailsAssert.class .
    2. Второй аргумент конструктора — это объект фактического значения. Передайте объект, указанный в качестве аргумента конструктора, в конструктор суперкласса.
  3. Добавьте статический метод assertThat () в созданный класс. Этот метод принимает объект ExampleUserDetails в качестве параметра метода. Реализуйте этот метод, создав новый объект ExampleUserDetailsAssert .
  4. Добавьте метод hasFirstName () в класс ExampleUserDetailsAssert . Этот метод принимает объект String в качестве параметра метода и возвращает объект ExampleUserDetailsAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект ExampleUserDetails не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что фактическое имя соответствует ожидаемому имени, указанному в качестве параметра метода.
    3. Вернуть ссылку на объект ExampleUserDetailsAssert .
  5. Добавьте метод hasId () в класс ExampleUserDetailsAssert . Этот метод принимает объект Long в качестве параметра метода и возвращает объект ExampleUserDetailsAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект ExampleUserDetails не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что фактический идентификатор равен ожидаемому идентификатору, указанному в качестве параметра метода.
    3. Вернуть ссылку на объект ExampleUserDetailsAssert .
  6. Добавьте метод hasLastName () в класс ExampleUserDetailsAssert . Этот метод принимает объект String в качестве параметра метода и возвращает объект ExampleUserDetailsAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект ExampleUserDetails не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что фактическая фамилия равна ожидаемой фамилии, указанной в качестве параметра метода.
    3. Вернуть ссылку на объект ExampleUserDetailsAssert .
  7. Добавьте метод hasPassword () в класс ExampleUserDetailsAssert . Этот метод принимает объект String в качестве параметра метода и возвращает объект ExampleUserDetailsAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект ExampleUserDetails не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что фактический пароль равен ожидаемому паролю, указанному в качестве параметра метода.
    3. Вернуть ссылку на объект ExampleUserDetailsAssert .
  8. Добавьте метод hasUsername () в класс ExampleUserDetailsAssert . Этот метод принимает объект String в качестве параметра метода и возвращает объект ExampleUserDetailsAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект ExampleUserDetails не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что фактическое имя пользователя равно ожидаемому имени пользователя, указанному в качестве параметра метода.
    3. Вернуть ссылку на объект ExampleUserDetailsAssert .
  9. Добавьте метод isActive () в класс ExampleUserDetailsAssert . Этот метод не принимает параметров метода и возвращает объект ExampleUserDetailsAssert .
    1. Убедитесь, что фактический объект ExampleUserDetails не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что пользователь, чья информация хранится в объекте ExampleUserDetails, активен.
    3. Вернуть ссылку на объект ExampleUserDetailsAssert .
  10. Добавьте метод isRegisteredUser () в класс ExampleUserDetailsAssert . Этот метод не принимает параметров метода и возвращает объект ExampleUserDetailsAssert .
    1. Убедитесь, что фактический объект ExampleUserDetails не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что пользователь, чья информация хранится в объекте ExampleUserDetails, является зарегистрированным пользователем.
    3. Вернуть ссылку на объект ExampleUserDetailsAssert .
  11. Добавьте метод isRegisteredByUsingFormRegistration () в класс ExampleUserDetailsAssert . Этот метод возвращает объект ExampleUserDetailsAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект ExampleUserDetails не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что значение поля socialSignInProvider равно нулю.
    3. Вернуть ссылку на объект ExampleUserDetailsAssert .
  12. Добавьте метод isSignedInByUsingSocialSignInProvider () в класс ExampleUserDetailsAssert . Этот метод принимает перечисление SocialMediaService в качестве параметра метода и возвращает объект ExampleUserDetailsAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект ExampleUserDetails не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что значение socialSignInProvider равно ожидаемому перечислению SocialMediaService, указанному в качестве параметра метода.
    3. Вернуть ссылку на объект ExampleUserDetailsAssert .

Исходный код класса ExampleUserDetailsAssert выглядит следующим образом:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import org.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
import org.springframework.security.core.GrantedAuthority;
 
import java.util.Collection;
 
public class ExampleUserDetailsAssert extends GenericAssert<ExampleUserDetailsAssert, ExampleUserDetails> {
 
    private ExampleUserDetailsAssert(ExampleUserDetails actual) {
        super(ExampleUserDetailsAssert.class, actual);
    }
 
    public static ExampleUserDetailsAssert assertThat(ExampleUserDetails actual) {
        return new ExampleUserDetailsAssert(actual);
    }
 
    public ExampleUserDetailsAssert hasFirstName(String firstName) {
        isNotNull();
 
        String errorMessage = String.format(
                "Expected first name to be <%s> but was <%s>",
                firstName,
                actual.getFirstName()
        );
 
        Assertions.assertThat(actual.getFirstName())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(firstName);
 
        return this;
    }
 
    public ExampleUserDetailsAssert hasId(Long id) {
        isNotNull();
 
        String errorMessage = String.format(
                "Expected id to be <%d> but was <%d>",
                id,
                actual.getId()
        );
 
        Assertions.assertThat(actual.getId())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(id);
 
        return this;
    }
 
    public ExampleUserDetailsAssert hasLastName(String lastName) {
        isNotNull();
 
        String errorMessage = String.format(
                "Expected last name to be <%s> but was <%s>",
                lastName,
                actual.getLastName()
        );
 
        Assertions.assertThat(actual.getLastName())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(lastName);
 
        return this;
    }
 
    public ExampleUserDetailsAssert hasPassword(String password) {
        isNotNull();
 
        String errorMessage = String.format(
                "Expected password to be <%s> but was <%s>",
                password,
                actual.getPassword()
        );
 
        Assertions.assertThat(actual.getPassword())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(password);
 
        return this;
    }
 
    public ExampleUserDetailsAssert hasUsername(String username) {
        isNotNull();
 
        String errorMessage = String.format(
                "Expected username to be <%s> but was <%s>",
                username,
                actual.getUsername()
        );
 
        Assertions.assertThat(actual.getUsername())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(username);
 
        return this;
    }
 
    public ExampleUserDetailsAssert isActive() {
        isNotNull();
 
        String expirationErrorMessage = "Expected account to be non expired but it was expired";
        Assertions.assertThat(actual.isAccountNonExpired())
                .overridingErrorMessage(expirationErrorMessage)
                .isTrue();
 
        String lockedErrorMessage = "Expected account to be non locked but it was locked";
        Assertions.assertThat(actual.isAccountNonLocked())
                .overridingErrorMessage(lockedErrorMessage)
                .isTrue();
 
        String credentialsExpirationErrorMessage = "Expected credentials to be non expired but they were expired";
        Assertions.assertThat(actual.isCredentialsNonExpired())
                .overridingErrorMessage(credentialsExpirationErrorMessage)
                .isTrue();
 
        String enabledErrorMessage = "Expected account to be enabled but it was not";
        Assertions.assertThat(actual.isEnabled())
                .overridingErrorMessage(enabledErrorMessage)
                .isTrue();
 
        return this;
    }
 
    public ExampleUserDetailsAssert isRegisteredUser() {
        isNotNull();
 
        String errorMessage = String.format(
                "Expected role to be <ROLE_USER> but was <%s>",
                actual.getRole()
        );
 
        Assertions.assertThat(actual.getRole())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(Role.ROLE_USER);
 
        Collection<? extends GrantedAuthority> authorities = actual.getAuthorities();
 
        String authoritiesCountMessage = String.format(
                "Expected <1> granted authority but found <%d>",
                authorities.size()
        );
 
        Assertions.assertThat(authorities.size())
                .overridingErrorMessage(authoritiesCountMessage)
                .isEqualTo(1);
 
        GrantedAuthority authority = authorities.iterator().next();
 
        String authorityErrorMessage = String.format(
                "Expected authority to be <ROLE_USER> but was <%s>",
                authority.getAuthority()
        );
 
        Assertions.assertThat(authority.getAuthority())
                .overridingErrorMessage(authorityErrorMessage)
                .isEqualTo(Role.ROLE_USER.name());
 
        return this;
    }
 
    public ExampleUserDetailsAssert isRegisteredByUsingFormRegistration() {
        isNotNull();
 
        String errorMessage = String.format(
                "Expected socialSignInProvider to be <null> but was <%s>",
                actual.getSocialSignInProvider()
        );
 
        Assertions.assertThat(actual.getSocialSignInProvider())
                .overridingErrorMessage(errorMessage)
                .isNull();
 
        return this;
    }
 
    public ExampleUserDetailsAssert isSignedInByUsingSocialSignInProvider(SocialMediaService socialSignInProvider) {
        isNotNull();
 
        String errorMessage = String.format(
                "Expected socialSignInProvider to be <%s> but was <%s>",
                socialSignInProvider,
                actual.getSocialSignInProvider()
        );
 
        Assertions.assertThat(actual.getSocialSignInProvider())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(socialSignInProvider);
 
        return this;
    }
}

Создание класса SecurityContextAssert

Мы можем создать второй класс подтверждения клиента, выполнив следующие действия:

  1. Создайте класс SecurityContextAssert, который расширяет класс GenericAssert . Укажите следующие параметры типа:
    1. Первый параметр типа — это тип пользовательского утверждения. Установите значение этого параметра типа в SecurityContextAssert .
    2. Второй параметр типа — это тип объекта фактического значения. Установите значение этого параметра типа в SecurityContext .
  2. Добавьте приватный конструктор в созданный класс. Этот конструктор принимает объект SecurityContext в качестве аргумента конструктора. Реализуйте контроллер, вызвав конструктор суперкласса и передав следующие объекты в качестве аргументов конструктора:
    1. Первый аргумент конструктора — это объект Class, который определяет тип пользовательского класса утверждения. Установите значение этого аргумента конструктора в SecurityContextAssert.class .
    2. Второй аргумент конструктора — это объект фактического значения. Передайте объект, указанный в качестве аргумента конструктора, в конструктор суперкласса.
  3. Добавьте статический метод assertThat () в созданный класс. Этот метод принимает объект SecurityContext в качестве параметра метода. Реализуйте этот метод, создав новый объект SecurityContextAssert .
  4. Добавьте метод userIsAnonymous () в класс SecurityContextAssert и реализуйте его, выполнив следующие действия:
    1. Убедитесь, что фактические объекты SecurityContext не являются нулевыми, вызвав метод isNotNull () класса GenericAssert .
    2. Получите объект Authentication из SecurityContext и убедитесь, что он нулевой .
    3. Вернуть ссылку на объект SecurityContextAssert .
  5. Добавьте метод loggedInUserIs () в класс SecurityContextAssert . Этот метод принимает объект User в качестве параметра метода и возвращает объект SecurityContextAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект SecurityContext не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Получите объект ExampleUserDetails из SecurityContext и убедитесь, что он не является нулевым.
    3. Убедитесь, что информация объекта ExampleUserDetails равна информации объекта User .
    4. Вернуть ссылку на объект SecurityContextAssert .
  6. Добавьте метод loggedInUserHasPassword () в класс SecurityContextAssert . Этот метод принимает объект String в качестве параметра метода и возвращает объект SecurityContextAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект SecurityContext не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Получите объект ExampleUserDetails из SecurityContext и убедитесь, что он не является нулевым.
    3. Убедитесь, что поле пароля объекта ExampleUserDetails равно паролю, указанному в качестве параметра метода.
    4. Вернуть ссылку на объект SecurityContextAssert .
  7. Добавьте метод loggedInUserIsRegisteredByUsingNormalRegistration () в класс SecurityContextAssert и реализуйте его, выполнив следующие действия:
    1. Убедитесь, что фактический объект SecurityContext не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Получите объект ExampleUserDetails из SecurityContext и убедитесь, что он не является нулевым.
    3. Убедитесь, что учетная запись пользователя создана с помощью обычной регистрации.
    4. Вернуть ссылку на объект SecurityContextAssert .
  8. Добавьте метод loggedInUserIsSignedInByUsingSocialProvider () в класс SecurityContextAssert . Этот метод принимает перечисление SocialMediaService в качестве параметра метода и возвращает объект SecurityContextAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект SecurityContext не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Получите объект ExampleUserDetails из SecurityContext и убедитесь, что он не является нулевым.
    3. Убедитесь, что учетная запись пользователя создана с использованием SociaMediaService, указанного в качестве параметра метода.
    4. Вернуть ссылку на объект SecurityContextAssert .

Исходный код класса SecurityContextAssert выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import org.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
 
public class SecurityContextAssert extends GenericAssert<SecurityContextAssert, SecurityContext> {
 
    private SecurityContextAssert(SecurityContext actual) {
        super(SecurityContextAssert.class, actual);
    }
 
    public static SecurityContextAssert assertThat(SecurityContext actual) {
        return new SecurityContextAssert(actual);
    }
 
    public SecurityContextAssert userIsAnonymous() {
        isNotNull();
 
        Authentication authentication = actual.getAuthentication();
 
        String errorMessage = String.format("Expected authentication to be <null> but was <%s>.", authentication);
        Assertions.assertThat(authentication)
                .overridingErrorMessage(errorMessage)
                .isNull();
 
        return this;
    }
 
    public SecurityContextAssert loggedInUserIs(User user) {
        isNotNull();
 
        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();
 
        String errorMessage = String.format("Expected logged in user to be <%s> but was <null>", user);
        Assertions.assertThat(loggedIn)
                .overridingErrorMessage(errorMessage)
                .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();
 
        String errorMessage = String.format("Expected logged in user to be <not null> but was <null>");
        Assertions.assertThat(loggedIn)
                .overridingErrorMessage(errorMessage)
                .isNotNull();
 
        ExampleUserDetailsAssert.assertThat(loggedIn)
                .hasPassword(password);
 
        return this;
    }
 
    public SecurityContextAssert loggedInUserIsRegisteredByUsingNormalRegistration() {
        isNotNull();
 
        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();
 
        String errorMessage = String.format("Expected logged in user to be <not null> but was <null>");
        Assertions.assertThat(loggedIn)
                .overridingErrorMessage(errorMessage)
                .isNotNull();
 
        ExampleUserDetailsAssert.assertThat(loggedIn)
                .isRegisteredByUsingFormRegistration();
 
        return this;
    }
 
    public SecurityContextAssert loggedInUserIsSignedInByUsingSocialProvider(SocialMediaService signInProvider) {
        isNotNull();
 
        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();
 
        String errorMessage = String.format("Expected logged in user to be <not null> but was <null>");
        Assertions.assertThat(loggedIn)
                .overridingErrorMessage(errorMessage)
                .isNotNull();
 
        ExampleUserDetailsAssert.assertThat(loggedIn)
                .hasPassword("SocialUser")
                .isSignedInByUsingSocialSignInProvider(signInProvider);
 
        return this;
    }
}

Создание класса TestProviderSignInAttemptAssert

Мы можем создать третий пользовательский класс утверждений, выполнив следующие действия:

  1. Создайте класс TestProviderSignInAttemptAssert, который расширяет класс GenericAssert . Укажите следующие параметры типа:
    1. Первый параметр типа — это тип пользовательского утверждения. Установите значение этого параметра типа в TestProviderSignInAttemptAssert .
    2. Второй параметр типа — это тип объекта фактического значения. Задайте значение этого параметра типа TestProviderSignInAttempt .
  2. Добавьте приватный конструктор в созданный класс. Этот конструктор принимает объект TestProviderSignInAttempt в качестве аргумента конструктора. Реализуйте контроллер, вызвав конструктор суперкласса и передав следующие объекты в качестве аргументов конструктора:
    1. Первый аргумент конструктора — это объект Class, который определяет тип пользовательского класса утверждения. Установите значение этого аргумента конструктора в TestProviderSignInAttemptAssert.class .
    2. Второй аргумент конструктора — это объект фактического значения. Передайте объект, указанный в качестве аргумента конструктора, в конструктор суперкласса.
  3. Добавьте статический метод assertThatSignIn () в созданный класс. Этот метод принимает объект TestProviderSignInAttempt в качестве параметра метода. Реализуйте этот метод, создав новый объект TestProviderSignInAttemptAssert .
  4. Добавьте метод createNoConnections () в созданный класс. Этот метод не принимает параметров метода и возвращает ссылку на объект TestProviderSignInAttemptAssert . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект TestProviderSignInAttempt не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что фактический объект TestProviderSignInAttempt не создал соединений.
    3. Вернуть ссылку на объект TestProviderSignInAttemptAssert .
  5. Добавьте метод createConnectionForUserId () в созданный класс. Этот метод принимает объект String в качестве параметра метода и возвращает ссылку на объект TestProviderSignInAttempt . Мы можем реализовать этот метод, выполнив следующие действия:
    1. Убедитесь, что фактический объект TestProviderSignInAttempt не является нулевым, вызвав метод isNotNull () класса GenericAssert .
    2. Убедитесь, что соединение было создано для пользователя, чей идентификатор пользователя был задан в качестве параметра метода.
    3. Вернуть ссылку на объект TestProviderSignInAttemptAssert .

Исходный код класса TestProviderSignInAttemptAssert выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
 
public class TestProviderSignInAttemptAssert extends GenericAssert<TestProviderSignInAttemptAssert, TestProviderSignInAttempt> {
 
    private TestProviderSignInAttemptAssert(TestProviderSignInAttempt actual) {
        super(TestProviderSignInAttemptAssert.class, actual);
    }
 
    public static TestProviderSignInAttemptAssert assertThatSignIn(TestProviderSignInAttempt actual) {
        return new TestProviderSignInAttemptAssert(actual);
    }
 
    public TestProviderSignInAttemptAssert createdNoConnections() {
        isNotNull();
 
        String error = String.format(
                "Expected that no connections were created but found <%d> connection",
                actual.getConnections().size()
        );
        Assertions.assertThat(actual.getConnections())
                .overridingErrorMessage(error)
                .isEmpty();
 
        return this;
    }
 
    public TestProviderSignInAttemptAssert createdConnectionForUserId(String userId) {
        isNotNull();
 
        String error = String.format(
                "Expected that connection was created for user id <%s> but found none.",
                userId
        );
 
        Assertions.assertThat(actual.getConnections())
                .overridingErrorMessage(error)
                .contains(userId);
 
        return this;
    }
}

Давайте продолжим и начнем писать некоторый модульный тест для класса RegistrationController .

Написание юнит-тестов

Мы закончили подготовку и готовы написать модульные тесты для функции регистрации. Мы должны написать модульные тесты для следующих методов контроллера:

  • Первый метод контроллера отображает страницу регистрации.
  • Второй метод контроллера обрабатывает представления формы регистрации.

Прежде чем мы сможем начать писать наши модульные тесты, мы должны их настроить. Давайте выясним, как это делается.

Примечание. В наших модульных тестах используется среда Spring MVC Test. Если вы не знакомы с ним, я рекомендую вам взглянуть на мой учебник Spring MVC Test .

Настройка наших модульных тестов

Конфигурация контекста приложения нашего примера приложения разработана таким образом, чтобы было легко написать модульные тесты для веб-слоя. Эти принципы проектирования описаны ниже:

  • Конфигурация контекста приложения разделена на несколько классов конфигурации, и каждый класс настраивает определенную часть нашего приложения (веб, безопасность, социальные сети и постоянство).
  • В нашей конфигурации контекста приложения есть «основной» класс конфигурации, который настраивает несколько «общих» компонентов и импортирует другие классы конфигурации. Этот класс конфигурации также настраивает сканирование компонентов для уровня обслуживания.

Когда мы конфигурируем контекст приложения, следуя этим принципам, легко создать конфигурацию контекста приложения для наших модульных тестов. Мы можем сделать это, повторно используя класс конфигурации контекста приложения, который настраивает веб-уровень нашего примера приложения, и создав новый класс конфигурации контекста приложения для наших модульных тестов.

Мы можем создать класс конфигурации контекста приложения для наших модульных тестов, выполнив следующие действия:

  1. Создайте класс с именем UnitTestContext .
  2. Аннотируйте созданный класс с помощью аннотации @Configuration .
  3. Добавьте метод messageSource () в созданный класс и аннотируйте метод аннотацией @Bean . Настройте bean- компонент MessageSource , выполнив следующие действия:
    1. Создайте новый объект ResourceBundleMessageSource .
    2. Задайте базовое имя файлов сообщений и убедитесь, что если сообщение не найдено, возвращается его код.
    3. Вернуть созданный объект.
  4. Добавьте метод userService () к созданному классу и аннотируйте метод аннотацией @Bean . Настройте фиктивный объект UserService , выполнив следующие действия:
    1. Вызовите метод static mock () класса Mockito и передайте UserService.class в качестве параметра метода.
    2. Вернуть созданный объект.

Исходный код класса UnitTestContext выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
    }
}

Следующее, что нам нужно сделать, это настроить наши модульные тесты. Мы можем сделать это, выполнив следующие действия:

  1. Пометьте тестовый класс аннотацией @RunWith и убедитесь, что наши тесты выполняются с использованием SpringUnit4ClassRunner .
  2. Пометьте класс аннотацией @ContextConfiguration и убедитесь, что используются правильные классы конфигурации. В нашем случае правильные классы конфигурации: WebAppContext и UnitTestContext .
  3. Аннотируйте класс с помощью аннотации @WebAppConfiguration . Эта аннотация гарантирует, что загруженный контекст приложения является WebApplicationContext .
  4. Добавьте поле MockMvc в тестовый класс.
  5. Добавьте в класс поле WebApplicationContext и аннотируйте его аннотацией @Autowired .
  6. Добавьте поле UserService в тестовый класс и аннотируйте его аннотацией @Autowired .
  7. Добавьте метод setUp () в тестовый класс и аннотируйте метод аннотацией @Before . Это гарантирует, что метод вызывается перед каждым тестовым методом. Реализуйте этот метод, выполнив следующие действия:
    1. Сброс макета UserService путем вызова статического метода reset () класса Mockito и передачи сброшенного макета в качестве параметра метода.
    2. Создайте новый объект MockMvc с помощью класса MockMvcBuilders .
    3. Убедитесь, что в SecurityContext при выполнении наших тестов не найдено ни одного объекта аутентификации . Мы можем сделать это, выполнив следующие действия:
      1. Получите ссылку на объект SecurityContext , вызвав статический метод getContext () класса SecurityContextHolder .
      2. Очистите аутентификацию, вызвав метод setAuthentication () класса SecurityContext . Передайте значение null в качестве параметра метода.

Исходный код нашего класса модульных тестов выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
    }
}

Примечание. Если вы хотите получить больше информации о конфигурации модульных тестов, использующих среду Spring MVC Test, я рекомендую вам прочитать этот блог .

Давайте продолжим и напишем модульные тесты для метода контроллера, который отображает регистрационную форму.

Предоставление регистрационной формы

Метод контроллера, который отображает регистрационную форму, имеет одну важную ответственность:

Если пользователь использует социальный вход, поля регистрации предварительно заполняются с использованием информации, которая используется предоставленным провайдером API SaaS.

Давайте освежим нашу память и посмотрим на исходный код класса RegistrationController :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;
    }
}

Понятно, что мы должны написать два модульных теста для этого метода контроллера:

  1. Мы должны написать тест, который гарантирует, что метод контроллера работает правильно, когда пользователь использует «обычную» регистрацию.
  2. Мы должны написать тест, который гарантирует, что метод контроллера работает правильно, когда пользователь использует социальный вход.

Давайте переместимся и напишем эти юнит-тесты.

Тест 1: Предоставление обычной регистрационной формы

Мы можем написать первый тестовый модуль, выполнив следующие действия:

  1. Выполните GET- запрос к URL ‘/ user / register’.
  2. Убедитесь, что возвращается код состояния HTTP 200.
  3. Убедитесь, что имя отображаемого представления — user / registrationForm.
  4. Убедитесь, что запрос перенаправлен на URL /WEB-INF/jsp/user/registrationForm.jsp.
  5. Убедитесь, что все поля атрибута модели с именем ‘user’ являются нулевыми или пустыми.
  6. Убедитесь, что никакие методы макета UserService не были вызваны.

Исходный код нашего модульного теста выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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);
    }
}

Тест 2: визуализация регистрационной формы с помощью входа в социальную сеть

Мы можем написать второй модульный тест, выполнив следующие действия:

  1. Создайте новый объект TestProviderSignInAttempt с помощью класса TestProviderSignInAttemptBuilder . Установите идентификатор провайдера, имя, фамилию и адрес электронной почты.
  2. Выполните GET-запрос для URL ‘/ user / register’ и установите созданный объект TestProviderSignInAttempt в сеанс HTTP.
  3. Убедитесь, что возвращается код состояния HTTP 200.
  4. Убедитесь, что имя отображаемого представления — user / registrationForm.
  5. Убедитесь, что запрос перенаправлен на URL /WEB-INF/jsp/user/registrationForm.jsp.
  6. Убедитесь, что поля объекта модели с именем ‘user’ предварительно заполнены, используя информацию, содержащуюся в объекте TestProviderSignInAttempt . Мы можем сделать это, выполнив следующие действия:
    1. Убедитесь, что значением поля электронной почты является «[email protected]».
    2. Убедитесь, что значением поля firstName является ‘John’.
    3. Убедитесь, что значение поля lastName равно «Smith».
    4. Убедитесь, что значение поля пароля пустое или пустое значение String.
    5. Убедитесь, что значение поля passwordVerification пустое или пустое.
    6. Убедитесь, что значение поля signInProvider равно «twitter».
  7. Убедитесь, что методы интерфейса UserService не были вызваны.

Исходный код нашего модульного теста выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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);
    }
}

Отправка регистрационной формы

Метод контроллера, который обрабатывает представления формы регистрации, имеет следующие обязанности:

  1. Это подтверждает информацию, введенную в регистрационную форму. Если информация недействительна, она отображает регистрационную форму и показывает сообщения об ошибках проверки пользователю.
  2. Если адрес электронной почты, указанный пользователем, не является уникальным, он отображает регистрационную форму и показывает сообщение об ошибке пользователю.
  3. Он создает новую учетную запись пользователя с помощью интерфейса UserService и регистрирует созданного пользователя в.
  4. Он сохраняет соединение с провайдером API SaaS, если пользователь использовал социальный вход
  5. Он перенаправляет пользователя на первую страницу.

Соответствующая часть класса RegistrationController выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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);
    }
}

Мы напишем три модульных теста для этого метода контроллера:

  1. Мы пишем модульный тест, который гарантирует, что метод контроллера работает правильно, когда проверка не проходит.
  2. Мы пишем модульный тест, который гарантирует, что метод контроллера работает, когда адрес электронной почты не является уникальным.
  3. Мы пишем модульный тест, который гарантирует, что метод контроллера работает правильно, когда регистрация прошла успешно.

Давайте выясним, как мы можем написать эти модульные тесты.

Тест 1: проверка не проходит

Мы можем написать первый тестовый модуль, выполнив следующие действия:

  1. Создайте новый объект TestProviderSignInAttempt с помощью класса TestProviderSignInAttemptBuilder . Установите идентификатор провайдера, имя, фамилию и адрес электронной почты.
  2. Создайте новый объект RegistrationForm с помощью класса RegistrationFormBuilder . Установите значение поля signInProvider .
  3. Выполните запрос POST для URL ‘/ user / register’, выполнив следующие действия:
    1. Установите тип содержимого запроса «application / x-www-form-urlencoded».
    2. Преобразуйте объект формы в байты, закодированные в url, и установите результат преобразования в тело запроса.
    3. Установите созданный объект TestProviderSignInAttempt в сеанс HTTP.
    4. Установите для объекта формы сеанс HTTP.
  4. Убедитесь, что код состояния HTTP 200 возвращается.
  5. Убедитесь, что имя отображаемого представления — user / registrationForm.
  6. Убедитесь, что запрос перенаправлен на URL /WEB-INF/jsp/user/registrationForm.jsp.
  7. Убедитесь, что значения полей объекта модели с именем ‘user’ являются правильными, выполнив следующие действия:
    1. Убедитесь, что значение поля электронной почты пустое или пустое значение String.
    2. Убедитесь, что значение поля firstName пустое или пустое значение String.
    3. Убедитесь, что значение поля lastName пустое или пустое.
    4. Убедитесь, что значение поля пароля пустое или пустое значение String.
    5. Убедитесь, что значение поля passwordVerification пустое или пустое значение String.
    6. Убедитесь, что значение поля signInProvider равно «twitter».
  8. Убедитесь, что атрибут модели с именем ‘user’ содержит ошибки в полях email , firstName и lastName .
  9. Убедитесь, что текущий пользователь не вошел в систему.
  10. Убедитесь, что с помощью объекта TestProviderSignInAttempt не было создано ни одного соединения .
  11. Убедитесь, что методы макета UserService не были вызваны.

Исходный код нашего модульного теста выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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();
 
        RegistrationForm userAccountData = new RegistrationFormBuilder()
                .signInProvider(SocialMediaService.TWITTER)
                .build();
 
        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData))
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", userAccountData)
        )
                .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);
    }
}

Тест 2: адрес электронной почты найден в базе данных

Мы можем написать второй модульный тест, выполнив следующие действия:

  1. Создайте новый объект TestProviderSignInAttempt с помощью класса TestProviderSignInAttemptBuilder . Установите идентификатор провайдера, имя, фамилию и адрес электронной почты.
  2. Создайте новый объект RegistrationForm с помощью класса RegistrationFormBuilder . Установите значения полей email , firstName , lastName и signInProvider .
  3. Сконфигурируйте макет UserService для создания исключения DuplicateEmailException при вызове его метода registerNewUserAccount () и объекта формы в качестве параметра метода.
  4. Выполните запрос POST для URL ‘/ user / register’, выполнив следующие действия:
    1. Установите тип содержимого запроса «application / x-www-form-urlencoded».
    2. Преобразуйте объект формы в байты, закодированные в url, и установите результат преобразования в тело запроса.
    3. Установите созданный объект TestProviderSignInAttempt в сеанс HTTP.
    4. Установите для объекта формы сеанс HTTP.
  5. Убедитесь, что код состояния HTTP 200 возвращается.
  6. Убедитесь, что имя отображаемого представления — user / registrationForm.
  7. Убедитесь, что запрос перенаправлен на URL /WEB-INF/jsp/user/registrationForm.jsp.
  8. Убедитесь, что значения полей объекта модели с именем ‘user’ являются правильными, выполнив следующие действия:
    1. Убедитесь, что значением поля электронной почты является «[email protected]».
    2. Убедитесь, что значением поля firstName является ‘John’.
    3. Убедитесь, что значение поля lastName равно «Smith».
    4. Убедитесь, что значение поля пароля пустое или пустое значение String.
    5. Убедитесь, что значение поля passwordVerification пустое или пустое.
    6. Убедитесь, что значение поля signInProvider равно «twitter».
  9. Убедитесь, что у атрибута модели с именем ‘user’ есть ошибка поля в поле электронной почты .
  10. Убедитесь, что текущий пользователь не вошел в систему.
  11. Убедитесь, что с помощью объекта TestProviderSignInAttempt не было создано ни одного соединения .
  12. Убедитесь , что registerNewUserAccount () метод UserService макет был назван один раз , и что RegistrationForm объект был задан в качестве параметра метода.
  13. Убедитесь, что другие методы интерфейса UserService не были вызваны во время теста.

Исходный код нашего модульного теста выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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_SocialSignInAndEmailExist_ShouldRenderRegistrationFormWithFieldError() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("[email protected]")
                    .firstName("John")
                    .lastName("Smith")
                .build();
 
        RegistrationForm userAccountData = new RegistrationFormBuilder()
                .email("[email protected]")
                .firstName("John")
                .lastName("Smith")
                .signInProvider(SocialMediaService.TWITTER)
                .build();
 
        when(userServiceMock.registerNewUserAccount(userAccountData)).thenThrow(new DuplicateEmailException(""));
 
        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData))
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", userAccountData)
        )
                .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();
 
        verify(userServiceMock, times(1)).registerNewUserAccount(userAccountData);
        verifyNoMoreInteractions(userServiceMock);
    }
}

Тест 3: регистрация прошла успешно

Мы можем написать третий модульный тест, выполнив следующие действия:

  1. Создайте новый объект TestProviderSignInAttempt с помощью класса TestProviderSignInAttemptBuilder . Установите идентификатор провайдера, имя, фамилию и адрес электронной почты.
  2. Создайте новый объект RegistrationForm с помощью класса RegistrationFormBuilder . Установите значения полей email , firstName , lastName и signInProvider .
  3. Создайте новый объект User с помощью класса UserBuilder . Установите значения полей id , email , firstName , lastName и signInProvider .
  4. Сконфигурируйте фиктивный объект UserService для возврата созданного объекта User, когда вызывается его метод registerNewUserAccount (), а объект RegistrationForm задается в качестве параметра метода.
  5. Выполните запрос POST для URL ‘/ user / register’, выполнив следующие действия:
    1. Установите тип содержимого запроса «application / x-www-form-urlencoded».
    2. Преобразуйте объект формы в байты, закодированные в url, и установите результат преобразования в тело запроса.
    3. Установите созданный объект TestProviderSignInAttempt в сеанс HTTP.
    4. Установите для объекта формы сеанс HTTP.
  6. Убедитесь, что возвращается код состояния HTTP 302.
  7. Убедитесь, что запрос перенаправлен на URL ‘/’.
  8. Убедитесь, что созданный пользователь вошел в систему с помощью Twitter.
  9. Verify that the TestProviderSignInAttempt object was used to created a connection for a user with email address ‘[email protected]’.
  10. Verify that the registerNewUserAccount() method of the UserService mock was called once and that the form object was given as a method parameter.
  11. Verify that the other methods of the UserService mock weren’t invoked during the test.

Исходный код нашего модульного теста выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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.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();
 
        RegistrationForm userAccountData = new RegistrationFormBuilder()
                .email("[email protected]")
                .firstName("John")
                .lastName("Smith")
                .signInProvider(SocialMediaService.TWITTER)
                .build();
 
        User registered = new UserBuilder()
                .id(1L)
                .email("[email protected]")
                .firstName("John")
                .lastName("Smith")
                .signInProvider(SocialMediaService.TWITTER)
                .build();
 
        when(userServiceMock.registerNewUserAccount(userAccountData)).thenReturn(registered);
 
        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData))
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", userAccountData)
        )
                .andExpect(status().isMovedTemporarily())
                .andExpect(redirectedUrl("/"));
 
        assertThat(SecurityContextHolder.getContext())
                .loggedInUserIs(registered)
                .loggedInUserIsSignedInByUsingSocialProvider(SocialMediaService.TWITTER);
        assertThatSignIn(socialSignIn).createdConnectionForUserId("[email protected]");
 
        verify(userServiceMock, times(1)).registerNewUserAccount(userAccountData);
        verifyNoMoreInteractions(userServiceMock);
    }
}

Резюме

Теперь мы написали несколько модульных тестов для функции регистрации нашего примера приложения. Этот пост научил нас четырем вещам:

  1. Мы узнали, как создать дубликаты тестов, необходимые для наших модульных тестов.
  2. Мы научились эмулировать социальный вход с помощью созданных тестовых двойных классов.
  3. Мы узнали, как мы можем проверить, что соединение с используемым провайдером API SaaS сохраняется после создания новой учетной записи пользователя, который использовал социальный вход.
  4. Мы узнали, как мы можем проверить, что пользователь вошел в систему после создания новой учетной записи пользователя.

Пример приложения этого блога имеет много тестов, которые не были рассмотрены в этом блоге. Если вы заинтересованы в их просмотре, вы можете получить пример приложения от Github .

PS В этом блоге описан один из возможных подходов написания модульных тестов для контроллера регистрации, который использует Spring Social 1.1.0. Если у вас есть идеи по улучшению, вопросы или отзывы о моем подходе, не стесняйтесь оставлять комментарии к этому сообщению в блоге.