Я написал о проблемах написания модульных тестов для приложений, использующих Spring Social 1.1.0, и предоставил одно решение для этого .
Хотя модульное тестирование является ценным, оно не говорит нам, правильно ли работает наше приложение.
Вот почему мы должны написать интеграционные тесты для него .
Этот пост в блоге помогает нам сделать это. В этом посте мы узнаем, как написать интеграционные тесты для функций регистрации и входа в нашем примере приложения.
Если вы не читали предыдущие части моего учебника Spring Social, я рекомендую вам прочитать их, прежде чем читать этот пост в блоге. Предварительные условия этого сообщения в блоге описаны ниже:
- Добавление входа в социальную сеть в веб-приложение Spring MVC: «Конфигурация» описывает, как мы можем настроить наше примерное приложение.
- Добавление входа в социальные сети в веб-приложение Spring MVC: Регистрация и вход в систему описывает, как мы можем добавить функции регистрации и входа в наш пример приложения.
- Добавление социального входа в веб-приложение Spring MVC: модульное тестирование описывает, как мы можем написать модульные тесты для нашего примера приложения.
- Учебное пособие по тестированию Spring MVC описывает, как мы можем писать как модульные, так и интеграционные тесты, используя среду тестирования Spring MVC.
- Учебное пособие по Spring Data JPA: Интеграционное тестирование описывает, как мы можем написать интеграционные тесты для репозиториев Spring Data JPA. Этот пост поможет вам понять, как можно написать интеграционные тесты с помощью Spring Test DBUnit и DbUnit.
- Интеграционное тестирование с Maven описывает, как мы можем запускать интеграционные и модульные тесты с помощью Maven. Процесс сборки наших примеров приложений следует подходу, описанному в этом блоге.
Давайте начнем с внесения некоторых изменений в конфигурацию нашего процесса сборки.
Настройка нашего процесса сборки
Мы должны внести следующие изменения в конфигурацию нашего процесса сборки:
- Мы настроили локальный репозиторий Maven и добавили двоичные файлы моментальных снимков Spring Test DbUnit 1.1.1 в этот репозиторий.
- Мы должны добавить необходимые тестовые зависимости в наш файл POM.
- Мы должны добавить файлы наборов изменений Liquibase в classpath.
Давайте выясним, как мы можем сделать эти изменения.
Добавление бинарных файлов моментальных снимков DBUnit Spring Test в локальный репозиторий Maven
Поскольку стабильная версия Spring Test DBUnit не совместима с Spring Framework 4 , мы должны использовать снимок сборки в наших интеграционных тестах.
Мы можем добавить снимок Spring Test DBUnit в локальный репозиторий Maven, выполнив следующие действия:
- Клонируйте репозиторий Spring Test DBUnit из Github и создайте двоичные файлы моментальных снимков.
- Создайте каталог etc / mavenrepo . Этот каталог является нашим локальным хранилищем Maven.
- Скопируйте созданные файлы jar в каталог etc / mavenrepo / com / github / springtestdbunit / 1.1.1-SNAPSHOT.
После того, как мы скопировали файлы JAR в наш локальный репозиторий Maven, мы должны настроить местоположение локального репозитория в нашем файле pom.xml . Мы можем сделать это, добавив следующую декларацию репозитория в наш файл POM:
1
2
3
4
5
6
7
8
|
< repositories > <!-- Other repositories are omitted for the sake of clarity --> < repository > < id >local-repository</ id > < name >local repository</ name > < url >file://${project.basedir}/etc/mavenrepo</ url > </ repository > </ repositories > |
Получение необходимых зависимостей тестирования с Maven
Мы можем получить необходимые тестовые зависимости, добавив следующее объявление зависимости в наш POM-файл:
- Spring Test DBUnit (версия 1.1.1-SNAPSHOT). Мы используем Spring Test DBUnit для интеграции среды Spring Test с библиотекой DbUnit.
- DbUnit (версия 2.4.9). Мы используем DbUnit для инициализации нашей базы данных в известное состояние перед каждым интеграционным тестом и проверки соответствия содержимого базы данных ожидаемым данным.
- Жидкостное ядро (версия 3.1.1). Мы используем Liquibase для создания некоторых таблиц базы данных, когда загружается контекст приложения наших интеграционных тестов.
Соответствующая часть нашего файла pom.xml выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
< dependency > < groupId >com.github.springtestdbunit</ groupId > < artifactId >spring-test-dbunit</ artifactId > < version >1.1.1-SNAPSHOT</ version > < scope >test</ scope > </ dependency > < dependency > < groupId >org.dbunit</ groupId > < artifactId >dbunit</ artifactId > < version >2.4.9</ version > < scope >test</ scope > </ dependency > < dependency > < groupId >org.liquibase</ groupId > < artifactId >liquibase-core</ artifactId > < version >3.1.1</ version > < scope >test</ scope > </ dependency > |
Добавление наборов изменений Liquibase в путь к классам
Обычно мы должны позволить Hibernate создать базу данных, которая используется в наших интеграционных тестах. Однако этот подход работает, только если каждая таблица базы данных настроена в нашей модели предметной области.
Сейчас это не так. База данных примера приложения имеет таблицу UserConnection, которая не настроена в модели домена примера приложения. Вот почему нам нужно найти другой способ создания таблицы UserConnection до запуска наших интеграционных тестов.
Для этой цели мы можем использовать интеграцию Spring библиотеки Liquibase, но это означает, что мы должны добавить наборы изменений Liquibase в путь к классам.
Мы можем сделать это с помощью плагина Build Helper Maven . Мы можем добавить наборы изменений Liquibase в путь к классам, выполнив следующие действия:
- Убедитесь, что цель add-test-resource для подключаемого модуля Builder Helper Maven вызывается на этапе жизненного цикла generate-test-resources .
- Сконфигурируйте плагин для добавления каталога etc / db в classpath (этот каталог содержит необходимые файлы).
Соответствующая часть конфигурации плагина выглядит следующим образом:
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
|
< plugin > < groupId >org.codehaus.mojo</ groupId > < artifactId >build-helper-maven-plugin</ artifactId > < version >1.7</ version > < executions > <!-- Other executions are omitted for the sake of clarity --> < execution > < id >add-integration-test-resources</ id > <!-- Run this execution in the generate-test-sources lifecycle phase --> < phase >generate-test-resources</ phase > < goals > <!-- Invoke the add-test-resource goal of this plugin --> < goal >add-test-resource</ goal > </ goals > < configuration > < resources > <!-- Other resources are omitted for the sake of clarity --> <!-- Add the directory which contains Liquibase change sets to classpath --> < resource > < directory >etc/db</ directory > </ resource > </ resources > </ configuration > </ execution > </ executions > </ plugin > |
Если вы хотите получить больше информации об использовании плагина Builder Helper Maven, вы можете взглянуть на следующие веб-страницы:
Теперь мы завершили настройку нашего процесса сборки. Давайте выясним, как мы можем настроить наши интеграционные тесты.
Настройка наших интеграционных тестов
Мы можем настроить наши интеграционные тесты, выполнив следующие действия:
- Измените файл журнала изменений Liquibase.
- Сконфигурируйте контекст приложения для запуска наборов изменений Liquibase перед вызовом наших тестов.
- Создайте собственный загрузчик набора данных DbUnit.
- Настройте интеграционные тесты
Давайте двигаться дальше и внимательнее посмотрим на каждый шаг.
Изменение списка изменений Liquibase
В нашем примере приложения есть два набора изменений Liquibase, которые можно найти в каталоге etc / db / schema . Эти наборы изменений:
- Файл db-0.0.1.sql создает таблицу UserConnection, которая используется для постоянного подключения пользователя к поставщику используемого социального входа.
- Файл db-0.0.2.sql создает таблицу user_accounts, которая содержит учетные записи пользователей нашего примера приложения.
Поскольку мы хотим запустить только первый набор изменений, мы должны внести некоторые изменения в файл журнала изменений Liquibase . Чтобы быть более конкретным, мы должны использовать контексты Liquibase для определения
- Какие наборы изменений выполняются, когда мы создаем базу данных нашего примера приложения.
- Какие наборы изменений выполняются, когда мы запускаем наши интеграционные тесты.
Мы можем достичь нашей цели, выполнив следующие действия:
- Укажите, что файл набора изменений db-0.0.1.sql выполняется, когда для контекста Liquibase задано значение db или интеграционный тест.
- Укажите, что файл набора изменений db-0.0.2.sql выполняется, когда контекст Liquibase равен db .
Наш файл журнала изменений Liquibase выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
<? xml version = "1.0" encoding = "UTF-8" ?> < databaseChangeLog xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog <!-- Run this change set when the database is created and integration tests are run --> < changeSet id = "0.0.1" author = "Petri" context = "db,integrationtest" > < sqlFile path = "schema/db-0.0.1.sql" /> </ changeSet > <!-- Run this change set when the database is created --> < changeSet id = "0.0.2" author = "Petri" context = "db" > < sqlFile path = "schema/db-0.0.2.sql" /> </ changeSet > </ databaseChangeLog > |
Выполнение ревизий Liquibase до запуска интеграционных тестов
Мы можем выполнить наборы изменений Liquibase до запуска наших интеграционных тестов, выполнив их при загрузке контекста приложения. Мы можем сделать это, выполнив следующие действия:
- Создайте класс IntegrationTestContext и аннотируйте его аннотацией @Configuration .
- Добавьте поле DataSource в созданный класс и аннотируйте его аннотацией @Autowired .
- Добавьте в класс метод liquibase () и аннотируйте его аннотацией @Bean . Этот метод настраивает bean- компонент SpringLiquibase, который выполняет наборы изменений liquibase при загрузке контекста приложения.
- Реализуйте метод liquibase () , выполнив следующие действия:
- Создайте новый объект SpringLiquibase .
- Настройте источник данных, используемый созданным объектом.
- Настройте расположение журнала изменений Liquibase.
- Установите для контекста Liquibase значение «testtest ».
- Вернуть созданный объект.
Исходный код класса IntegrationTestContext выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import liquibase.integration.spring.SpringLiquibase; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class IntegrationTestContext { @Autowired private DataSource dataSource; @Bean public SpringLiquibase liquibase() { SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(dataSource); liquibase.setChangeLog( "classpath:changelog.xml" ); liquibase.setContexts( "integrationtest" ); return liquibase; } } |
Создание пользовательского класса DataSetLoader
Набор данных DbUnit, который содержит информацию о различных учетных записях пользователей, выглядит следующим образом:
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
|
<? xml version = '1.0' encoding = 'UTF-8' ?> < dataset > < user_accounts id = "1" creation_time = "2014-02-20 11:13:28" first_name = "Facebook" last_name = "User" modification_time = "2014-02-20 11:13:28" role = "ROLE_USER" sign_in_provider = "FACEBOOK" version = "0" /> < user_accounts id = "2" creation_time = "2014-02-20 11:13:28" first_name = "Twitter" last_name = "User" modification_time = "2014-02-20 11:13:28" role = "ROLE_USER" sign_in_provider = "TWITTER" version = "0" /> < user_accounts id = "3" creation_time = "2014-02-20 11:13:28" first_name = "RegisteredUser" last_name = "User" modification_time = "2014-02-20 11:13:28" password = "$2a$10$PFSfOaC2IFPG.1HjO05KoODRVSdESQ5q7ek4IyzVfTf.VWlKDa/.e" role = "ROLE_USER" version = "0" /> < UserConnection /> </ dataset > |
Мы можем видеть две вещи из этого набора данных:
- Пользователи, которые создали свою учетную запись с помощью входа в систему, не имеют пароля.
- Пользователь, который создал свою учетную запись с помощью обычной регистрации, имеет пароль, но у него нет поставщика входа.
Это проблема, потому что мы используем так называемые плоские наборы данных XML, а загрузчик набора данных по умолчанию DbUnit не может справиться с этой ситуацией . Конечно, мы могли бы начать использовать стандартные наборы данных XML, но на мой вкус его синтаксис слишком многословен. Вот почему мы должны создать собственный загрузчик набора данных, который может справиться с этой ситуацией.
Мы можем создать собственный загрузчик набора данных, выполнив следующие действия:
- Создайте класс ColumnSensingFlatXMLDataSetLoader, который расширяет класс AbstractDataSetLoader .
- Переопределите метод createDataSet () и реализуйте его, выполнив следующие действия:
- Создайте новый объект FlatXmlDataSetBuilder .
- Включить распознавание столбцов. Определение столбцов означает, что DbUnit считывает весь набор данных из файла набора данных и добавляет новые столбцы, когда они обнаруживаются в наборе данных. Это гарантирует, что значение каждого столбца будет правильно вставлено в базу данных.
- Создайте новый объект IDataSet и верните созданный объект.
Исходный код класса ColumnSensingFlatXMLDataSetLoader выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
import com.github.springtestdbunit.dataset.AbstractDataSetLoader; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.xml.FlatXmlDataSetBuilder; import org.springframework.core.io.Resource; import java.io.InputStream; public class ColumnSensingFlatXMLDataSetLoader extends AbstractDataSetLoader { @Override protected IDataSet createDataSet(Resource resource) throws Exception { FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder(); builder.setColumnSensing( true ); InputStream inputStream = resource.getInputStream(); try { return builder.build(inputStream); } finally { inputStream.close(); } } } |
Однако создания пользовательского класса загрузчика набора данных недостаточно. Нам все еще нужно настроить наши тесты для использования этого класса при загрузке наших наборов данных. Мы можем сделать это, пометив тестовый класс аннотацией @DbUnitConfiguration и установив значение его атрибута dataSetLoader в ColumnSensingFlatXMLDataSetLoader.class .
Давайте посмотрим, как это делается.
Настройка наших интеграционных тестов
Мы можем настроить наши интеграционные тесты, выполнив следующие действия:
- Убедитесь, что тесты выполняются Spring SpringJUnit4ClassRunner . Мы можем сделать это, пометив тестовый класс аннотацией @RunWith и установив его значение в SpringJUnit4ClassRunner.class .
- Загрузите контекст приложения, пометив тестовый класс аннотацией @ContextConfiguration и настройте используемые классы или файлы конфигурации контекста приложения.
- Аннотируйте тестовый класс с помощью аннотации @WebAppConfiguration . Это гарантирует, что контекст приложения, загруженный для наших интеграционных тестов, является WebApplicationContext .
- Аннотируйте класс с помощью аннотации @TestExecutionListeners и передайте стандартные прослушиватели Spring и DBUnitTestExecutionListener в качестве значения. DBUnitTestExecutionListener гарантирует, что Spring обработает аннотации DbUnit, найденные в нашем тестовом классе.
- Сконфигурируйте тестовый класс для использования нашего пользовательского загрузчика набора данных, аннотируя тестовый класс аннотацией @DbUnitConfiguration . Установите значение его атрибута dataSetLoader в ColumnSensingFlatXMLDataSetLoader.class .
- Добавьте поле FilterChainProxy в тестовый класс и добавьте аннотацию @Autowired.
- Добавьте поле WebApplicationContext в тестовый класс и аннотируйте поле аннотацией @Autowired .
- Добавьте поле MockMvc в тестовый класс.
- Добавьте метод setUp () в тестовый класс и аннотируйте этот метод аннотацией @Before, которая гарантирует, что этот метод вызывается перед каждым тестовым методом.
- Реализуйте метод setUp () и создайте новый объект MockMvc с помощью класса MockMvcBuilders .
Исходный код пустого тестового класса выглядит следующим образом:
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 com.github.springtestdbunit.DbUnitTestExecutionListener; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.web.FilterChainProxy; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; 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 = {ExampleApplicationContext. class , IntegrationTestContext. class }) //@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"}) @WebAppConfiguration @TestExecutionListeners ({ DependencyInjectionTestExecutionListener. class , DirtiesContextTestExecutionListener. class , TransactionalTestExecutionListener. class , DbUnitTestExecutionListener. class }) @DbUnitConfiguration (dataSetLoader = ColumnSensingFlatXMLDataSetLoader. class ) public class ITTest { @Autowired private FilterChainProxy springSecurityFilterChain; @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .addFilter(springSecurityFilterChain) .build(); } } |
Если вам нужна дополнительная информация о конфигурации наших интеграционных тестов, я рекомендую вам прочитать следующие сообщения в блоге:
- Модульное тестирование контроллеров Spring MVC: в разделе « Конфигурация» объясняется, как можно настроить среду тестирования Spring MVC. В этом руководстве рассказывается о модульном тестировании, но оно должно пролить больше света на эту проблему.
- Учебное пособие по Spring Data JPA: Интеграционное тестирование описывает, как можно написать интеграционные тесты для репозиториев Spring Data JPA. Если вы хотите взглянуть на конфигурацию Spring Test DBUnit, этот пост в блоге может помочь понять это.
- Интеграционное тестирование приложений Spring MVC: Безопасность описывает, как вы можете написать тесты безопасности для приложений Spring MVC. Это руководство основано на Spring Security 3.1, но все же может помочь вам понять, как написаны эти тесты.
Теперь мы узнали, как мы можем настроить наши интеграционные тесты. Давайте продолжим и создадим несколько классов утилит тестирования, которые используются в наших интеграционных тестах.
Создание классов утилит тестирования
Далее мы создадим три служебных класса, которые используются в наших интеграционных тестах:
- Мы создадим класс IntegrationTestConstants, который содержит константы, используемые в нескольких интеграционных тестах.
- Мы создадим классы, которые используются для создания объектов ProviderSignInAttempt для наших интеграционных тестов.
- Мы создадим класс построителя тестовых данных, который используется для создания объектов CsrfToken .
Давайте выясним, почему мы должны создавать эти классы и как мы можем их создавать.
Создание класса IntegrationTestConstants
Когда мы пишем интеграционные (или модульные) тесты, иногда нам нужно использовать одну и ту же информацию во многих тестовых классах. Дублирование этой информации для всех классов тестов — плохая идея, потому что это затрудняет поддержку и понимание наших тестов. Вместо этого мы должны поместить эту информацию в один класс и получить ее из этого класса, когда нам это нужно.
Класс IntegrationTestConstants содержит следующую информацию, которая используется в нескольких тестовых классах:
- Он имеет константы, связанные с защитой CSRF в Spring Security 3.2. Эти константы включают в себя: имя HTTP-заголовка, который содержит токен CSRF, имя параметра запроса, который содержит значение токена CSRF, имя атрибута сеанса, который содержит объект CsrfToken , и значение токена CSRF ,
- Он содержит перечисление User, которое указывает пользователей, использованных в нашем интеграционном тесте. У каждого пользователя есть имя пользователя и пароль (это не обязательно). Информация этого перечисления используется в двух целях:
- Используется для указания авторизованного пользователя. Это полезно, когда мы проводим интеграционные тесты для защищенных функций (функций, которые требуют какой-либо авторизации).
- Когда мы пишем интеграционные тесты для функции входа в систему, нам необходимо указать имя пользователя и пароль пользователя, который пытается войти в приложение.
Исходный код класса IntegrationTestConstants выглядит следующим образом:
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
|
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; public class IntegrationTestConstants { public static final String CSRF_TOKEN_HEADER_NAME = "X-CSRF-TOKEN" ; public static final String CSRF_TOKEN_REQUEST_PARAM_NAME = "_csrf" ; public static final String CSRF_TOKEN_SESSION_ATTRIBUTE_NAME = HttpSessionCsrfTokenRepository. class .getName().concat( ".CSRF_TOKEN" ); public static final String CSRF_TOKEN_VALUE = "f416e226-bebc-401d-a1ed-f10f47aa9c56" ; public enum User { private String password; private String username; private User(String username, String password) { this .password = password; this .username = username; } public String getPassword() { return password; } public String getUsername() { return username; } } } |
Создание объектов ProviderSignInAttempt
Когда мы написали модульные тесты для нашего примера приложения, мы быстро взглянули на класс ProviderSignInUtils и поняли, что нам нужно найти способ создания объектов ProviderSignInAttempt .
Мы решили эту проблему, создав класс-заглушку, который использовался в наших модульных тестах. Этот класс-заглушка дает нам возможность сконфигурировать возвращенный объект Connection <?> И убедиться, что определенное соединение было «сохранено в базе данных». Однако наш класс-заглушка не сохранил подключения к используемой базе данных. Вместо этого он сохранил идентификатор пользователя для объекта Set .
Поскольку теперь мы хотим сохранить данные о подключении к базе данных, мы должны внести изменения в наш класс-заглушку. Мы можем внести эти изменения, внеся эти изменения в объект TestProviderSignInAttempt :
- Добавьте частное поле usersConnectionRepositorySet в класс TestProviderSignInAttempt . Тип этого поля — логический, и его значение по умолчанию — false. Это поле описывает, можем ли мы сохранить соединения с используемым хранилищем данных.
- Добавьте новый аргумент конструктора в конструктор класса TestProviderSignInAttempt . Тип этого аргумента — UsersConnectionRepository, и он используется для сохранения соединений с используемым хранилищем данных.
- Реализуйте конструктор, выполнив следующие действия:
- Вызовите конструктор суперкласса и передайте объекты Connection <?> И UsersConnectionRepository в качестве аргументов конструктора.
- Сохраните ссылку на объект Connection <?>, Заданный в качестве аргумента конструктора, в поле подключения .
- Если объект UsersConnectionRepository, указанный в качестве аргумента конструктора, не равен нулю, установите для значения usersConnectionRepositoryField значение true.
- Реализуйте метод addConnection (), выполнив следующие действия:
- Добавьте идентификатор пользователя, указанный в качестве параметра метода, в набор соединений .
- Если объект UsersConnectionRepository был установлен при создании нового объекта TestProviderSignInAttempt , вызовите метод addConnection () класса ProviderSignInAttempt и передайте идентификатор пользователя в качестве параметра метода.
- Добавьте частное поле usersConnectionRepository в класс TestProviderSignInAttemptBuilder и установите для его типа значение UsersConnectionRepository .
- Добавьте метод usersConnectionRepository () в класс. Установите ссылку на объект UsersConnectionRepository в поле usersConnectionRepository и верните ссылку на объект построителя.
- Измените последнюю строку метода build () и создайте новый объект TestProviderSignInAttempt , используя новый конструктор, который мы создали ранее.
- Создайте класс CsrfTokenBuilder .
- Добавьте личное поле headerName в созданный класс.
- Добавьте личное поле requestParameterName в созданный класс.
- Добавьте личное поле tokenValue в созданный класс.
- Добавьте конструктор публикации в созданный класс.
- Добавьте методы, используемые для установки значений полей полей headerName , requestParameterName и tokenValue .
- Добавьте метод build () к созданному классу и установите его тип возвращаемого значения CsrfToken . Реализуйте этот метод, выполнив следующие действия:
- Создайте новый объект DefaultCsrfToken и укажите имя заголовка токена CSRF, имя параметра запроса токена CSRF и значение токена CSRF в качестве аргументов конструктора.
- Вернуть созданный объект.
Исходный код класса 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
33
34
35
36
37
38
39
40
|
import org.springframework.social.connect.Connection; import org.springframework.social.connect.UsersConnectionRepository; import java.util.HashSet; import java.util.Set; public class TestProviderSignInAttempt extends ProviderSignInAttempt { private Connection<?> connection; private Set<String> connections = new HashSet<>(); private boolean usersConnectionRepositorySet = false ; public TestProviderSignInAttempt(Connection<?> connection, UsersConnectionRepository usersConnectionRepository) { super (connection, null , usersConnectionRepository); this .connection = connection; if (usersConnectionRepository != null ) { this .usersConnectionRepositorySet = true ; } } @Override public Connection<?> getConnection() { return connection; } @Override void addConnection(String userId) { connections.add(userId); if (usersConnectionRepositorySet) { super .addConnection(userId); } } public Set<String> getConnections() { return connections; } } |
Поскольку мы создаем новые объекты TestProviderSignInAttempt с помощью TestProviderSignInAttemptBuilder , мы также должны внести изменения в этот класс. Мы можем внести эти изменения, выполнив следующие действия:
Исходный код класса 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
129
130
|
import org.springframework.social.connect.*; 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; private UsersConnectionRepository usersConnectionRepository; 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 usersConnectionRepository(UsersConnectionRepository usersConnectionRepository) { this .usersConnectionRepository = usersConnectionRepository; 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, usersConnectionRepository); } } |
Создание объектов CsrfToken
Поскольку в нашем примере приложения используется защита CSRF, предоставляемая Spring Security 3.2, мы должны найти способ создания допустимых токенов CSRF в наших интеграционных тестах. Интерфейс CsrfToken объявляет методы, которые предоставляют информацию об ожидаемом токене CSRF. Этот интерфейс имеет одну реализацию под названием DefaultCsrfToken .
Другими словами, нам нужно найти способ создания новых объектов DefaultCsrfToken . Класс DefaultCsrfToken имеет один конструктор , и мы, конечно же, можем использовать его при создании новых объектов DefaultCsrfToken в наших интеграционных тестах. Проблема в том, что это не очень читабельно.
Вместо этого мы создадим класс построителя тестовых данных, который предоставляет свободный API для создания новых объектов CsrfToken . Мы можем создать этот класс, выполнив следующие действия:
Исходный код класса CsrfTokenBuilder выглядит следующим образом:
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
|
import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.DefaultCsrfToken; public class CsrfTokenBuilder { private String headerName; private String requestParameterName; private String tokenValue; public CsrfTokenBuilder() { } public CsrfTokenBuilder headerName(String headerName) { this .headerName = headerName; return this ; } public CsrfTokenBuilder requestParameterName(String requestParameterName) { this .requestParameterName = requestParameterName; return this ; } public CsrfTokenBuilder tokenValue(String tokenValue) { this .tokenValue = tokenValue; return this ; } public CsrfToken build() { return new DefaultCsrfToken(headerName, requestParameterName, tokenValue); } } |
Мы можем создать новые объекты CsrfToken , используя этот код:
1
2
3
4
5
|
CsrfToken csrfToken = new CsrfTokenBuilder() .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME) .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME) .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE) .build(); |
Теперь мы создали необходимые классы утилит тестирования. Давайте продолжим и начнем писать интеграционные тесты для нашего примера приложения.
Написание интеграционных тестов
Мы наконец готовы написать несколько интеграционных тестов для нашего примера приложения. Мы напишем следующие интеграционные тесты:
- Мы напишем интеграционные тесты, которые гарантируют, что форма входа в систему работает правильно.
- Мы напишем интеграционные тесты, которые проверят, что регистрация работает правильно, когда используется социальный вход.
Но прежде чем мы начнем писать эти интеграционные тесты, мы узнаем, как мы можем предоставить действительные токены CSRF для Spring Security.
Предоставление действительных токенов CSRF для Spring Security
Ранее мы узнали, как мы можем создавать объекты CsrfToken в наших интеграционных тестах. Однако нам все еще нужно найти способ предоставления этих токенов CSRF в Spring Security.
Настало время поближе познакомиться с тем, как Spring Security обрабатывает токены CSRF.
Интерфейс CsrfTokenRepository объявляет методы, необходимые для генерации, сохранения и загрузки токенов CSRF. Реализация по умолчанию этого интерфейса — класс HttpSessionCsrfTokenRepository, который хранит токены CSRF в сеансе HTTP.
Нам нужно найти ответы на следующие вопросы:
- Как токены CSRF сохраняются в сеансе HTTP?
- Как токены CSRF загружаются из HTTP-сессии?
Мы можем найти ответы на эти вопросы, взглянув на исходный код класса HttpSessionCsrfTokenRepository . Соответствующая часть класса HttpSessionCsrfTokenRepository выглядит следующим образом:
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
|
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository { private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository. class .getName().concat( ".CSRF_TOKEN" ); private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME; public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { if (token == null ) { HttpSession session = request.getSession( false ); if (session != null ) { session.removeAttribute(sessionAttributeName); } } else { HttpSession session = request.getSession(); session.setAttribute(sessionAttributeName, token); } } public CsrfToken loadToken(HttpServletRequest request) { HttpSession session = request.getSession( false ); if (session == null ) { return null ; } return (CsrfToken) session.getAttribute(sessionAttributeName); } //Other methods are omitted. } |
Теперь ясно, что токен CSRF сохраняется в сеансе HTTP как объекты CsrfToken , и эти объекты повторяются и сохраняются с использованием значения свойства sessionAttributeName . Это означает, что если мы хотим предоставить действительный токен CSRF для Spring Security, мы должны выполнить следующие шаги:
- Создайте новый объект CsrfToken с помощью нашего построителя тестовых данных.
- Отправьте значение токена CSRF в качестве параметра запроса.
- Сохраните созданный объект DefaultCsrfToken в сеансе HTTP, чтобы HttpSessionCsrfTokenRepository нашел его.
Исходный код нашего фиктивного теста выглядит следующим образом:
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
|
import org.junit.Test; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.DefaultCsrfToken; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; public class ITCSRFTest { private MockMvc mockMvc; @Test public void test() throws Exception { //1. Create a new CSRF token CsrfToken csrfToken = new CsrfTokenBuilder() .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME) .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME) .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE) .build(); mockMvc.perform(post( "/login/authenticate" ) //2. Send the value of the CSRF token as request parameter .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE) //3. Set the created CsrfToken object to session so that the CsrfTokenRepository finds it .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken) ) //Add assertions here. } } |
Хватит теории. Теперь мы готовы написать несколько интеграционных тестов для нашего приложения. Давайте начнем с написания интеграции в функцию входа в нашем примере приложения.
Написание тестов для функции входа
Пришло время написать интеграционные тесты для функции входа в систему нашего примера приложения. Мы напишем для этого следующие интеграционные тесты:
- Мы напишем интеграционный тест, который гарантирует, что при успешной регистрации все будет работать так, как ожидается.
- Мы напишем интеграционный тест, который гарантирует, что все работает, когда вход в систему не удается.
Оба этих интеграционных теста инициализируют базу данных до известного состояния с использованием одного и того же файла набора данных DbUnit ( users.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
|
<? xml version = '1.0' encoding = 'UTF-8' ?> < dataset > < user_accounts id = "1" creation_time = "2014-02-20 11:13:28" first_name = "Facebook" last_name = "User" modification_time = "2014-02-20 11:13:28" role = "ROLE_USER" sign_in_provider = "FACEBOOK" version = "0" /> < user_accounts id = "2" creation_time = "2014-02-20 11:13:28" first_name = "Twitter" last_name = "User" modification_time = "2014-02-20 11:13:28" role = "ROLE_USER" sign_in_provider = "TWITTER" version = "0" /> < user_accounts id = "3" creation_time = "2014-02-20 11:13:28" first_name = "RegisteredUser" last_name = "User" modification_time = "2014-02-20 11:13:28" password = "$2a$10$PFSfOaC2IFPG.1HjO05KoODRVSdESQ5q7ek4IyzVfTf.VWlKDa/.e" role = "ROLE_USER" version = "0" /> < UserConnection /> </ dataset > |
Давайте начнем.
Тест 1: вход успешен
Мы можем написать первый интеграционный тест, выполнив следующие действия:
- Пометьте тестовый класс аннотацией @DatabaseSetup и настройте набор данных, который используется для инициализации базы данных в известное состояние перед вызовом интеграционного теста.
- Создайте новый объект CsrfToken .
- Отправьте запрос POST на URL / login / authenticate, выполнив следующие действия:
- Установите значения параметров запроса имени пользователя и пароля . Используйте правильный пароль.
- Установите значение токена CSRF для запроса.
- Установите созданный CsrfToken в сеанс.
- убедитесь, что возвращается код состояния HTTP 302.
- Убедитесь, что запрос перенаправлен на URL ‘/’.
Исходный код нашего интеграционного теста выглядит следующим образом:
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
|
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DbUnitConfiguration; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (classes = {ExampleApplicationContext. class , IntegrationTestContext. class }) //@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"}) @WebAppConfiguration @TestExecutionListeners ({ DependencyInjectionTestExecutionListener. class , DirtiesContextTestExecutionListener. class , TransactionalTestExecutionListener. class , DbUnitTestExecutionListener. class }) @DbUnitConfiguration (dataSetLoader = ColumnSensingFlatXMLDataSetLoader. class ) @DatabaseSetup ( "/net/petrikainulainen/spring/social/signinmvc/user/users.xml" ) public class ITFormLoginTest { private static final String REQUEST_PARAM_PASSWORD = "password" ; private static final String REQUEST_PARAM_USERNAME = "username" ; //Some fields are omitted for the sake of clarity private MockMvc mockMvc; //The setUp() method is omitted for the sake of clarify. @Test public void login_CredentialsAreCorrect_ShouldRedirectUserToFrontPage() throws Exception { CsrfToken csrfToken = new CsrfTokenBuilder() .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME) .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME) .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE) .build(); mockMvc.perform(post( "/login/authenticate" ) .param(REQUEST_PARAM_USERNAME, IntegrationTestConstants.User.REGISTERED_USER.getUsername()) .param(REQUEST_PARAM_PASSWORD, IntegrationTestConstants.User.REGISTERED_USER.getPassword()) .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE) .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken) ) .andExpect(status().isMovedTemporarily()) .andExpect(redirectedUrl( "/" )); } } |
Тест 2: Ошибка входа
Мы можем написать второй интеграционный тест, выполнив следующие действия:
- Пометьте тестовый класс аннотацией @DatabaseSetup и настройте набор данных, который используется для инициализации базы данных в известное состояние перед вызовом интеграционного теста.
- Создайте новый объект CsrfToken .
- Отправьте запрос POST на URL / login / authenticate, выполнив следующие действия:
- Установите значения параметров запроса имени пользователя и пароля . Используйте неверный пароль.
- Установите значение токена CSRF для запроса в качестве параметра запроса.
- Установите созданный объект CsrfToken в сеанс.
- Убедитесь, что возвращается код состояния HTTP 302.
- Убедитесь, что запрос перенаправлен на URL ‘/ login? Error = bad_credentials’.
Исходный код нашего интеграционного теста выглядит следующим образом:
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
|
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DbUnitConfiguration; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (classes = {ExampleApplicationContext. class , IntegrationTestContext. class }) //@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"}) @WebAppConfiguration @TestExecutionListeners ({ DependencyInjectionTestExecutionListener. class , DirtiesContextTestExecutionListener. class , TransactionalTestExecutionListener. class , DbUnitTestExecutionListener. class }) @DbUnitConfiguration (dataSetLoader = ColumnSensingFlatXMLDataSetLoader. class ) @DatabaseSetup ( "/net/petrikainulainen/spring/social/signinmvc/user/users.xml" ) public class ITFormLoginTest { private static final String REQUEST_PARAM_PASSWORD = "password" ; private static final String REQUEST_PARAM_USERNAME = "username" ; //Some fields are omitted for the sake of clarity private MockMvc mockMvc; //The setUp() method is omitted for the sake of clarify. @Test public void login_InvalidPassword_ShouldRedirectUserToLoginForm() throws Exception { CsrfToken csrfToken = new CsrfTokenBuilder() .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME) .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME) .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE) .build(); mockMvc.perform(post( "/login/authenticate" ) .param(REQUEST_PARAM_USERNAME, IntegrationTestConstants.User.REGISTERED_USER.getUsername()) .param(REQUEST_PARAM_PASSWORD, "invalidPassword" ) .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE) .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken) ) .andExpect(status().isMovedTemporarily()) .andExpect(redirectedUrl( "/login?error=bad_credentials" )); } } |
Написание тестов для функции регистрации
Мы напишем следующие интеграционные тесты для функции регистрации:
- Мы напишем интеграционный тест, который гарантирует, что наше приложение работает должным образом, когда пользователь создает новую учетную запись пользователя, используя социальный вход, но проверка отправленной регистрационной формы не проходит.
- Мы напишем интеграционный тест, который проверяет, что все работает правильно, когда пользователь создает новую учетную запись пользователя, используя социальный вход и адрес электронной почты, найденный в базе данных.
- Мы напишем интеграционный тест, который обеспечит возможность создания новой учетной записи пользователя с помощью входа в систему.
Давайте начнем.
Тест 1: проверка не проходит
Мы можем написать первый интеграционный тест, выполнив следующие действия:
- Добавьте поле UsersConnectionRepository в тестовый класс и аннотируйте его аннотацией @Autowired .
- Аннотируйте метод теста аннотацией @DatabaseSetup и настройте набор данных, который используется для инициализации базы данных в известное состояние до запуска нашего интеграционного теста.
- Создайте новый объект TestProviderSignInAttempt . Не забудьте установить используемый объект UsersConnectionRepository .
- Создайте новый объект RegistrationForm и установите значение его поля signInProvider .
- Создайте новый объект CsrfToken .
- Отправьте запрос POST на URL / user / register, выполнив следующие действия:
- Установите тип содержимого запроса «application / x-www-form-urlencoded».
- Преобразуйте объект формы в байты, закодированные в URL, и установите его в теле запроса.
- Установите созданный объект TestProviderSignInAttempt в сеанс.
- Установите значение токена CSRF для запроса в качестве параметра запроса.
- Установите созданный объект CsrfToken в сеанс.
- Установите созданный объект формы в сеанс.
- Убедитесь, что статус HTTP-запроса 200 возвращается.
- Убедитесь, что имя отображаемого представления — user / registrationForm.
- Убедитесь, что запрос пересылается по URL-адресу ‘/WEB-INF/jsp/user/registrationForm.jsp’.
- Убедитесь, что поля атрибута модели с именем ‘user’ являются правильными.
- Убедитесь, что у атрибута модели с именем ‘user’ есть ошибки в полях email , firstName и lastName .
- Аннотируйте метод теста аннотацией @ExpectedDatabase и убедитесь, что новая учетная запись пользователя не была сохранена в базе данных (используйте тот же набор данных, который использовался для инициализации базы данных).
Исходный код нашего интеграционного теста выглядит следующим образом:
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
|
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DbUnitConfiguration; import com.github.springtestdbunit.annotation.ExpectedDatabase; import com.github.springtestdbunit.assertion.DatabaseAssertionMode; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.social.connect.UsersConnectionRepository; 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.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import static net.petrikainulainen.spring.social.signinmvc.user.controller.TestProviderSignInAttemptAssert.assertThatSignIn; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (classes = {ExampleApplicationContext. class , IntegrationTestContext. class }) //@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"}) @WebAppConfiguration @TestExecutionListeners ({ DependencyInjectionTestExecutionListener. class , DirtiesContextTestExecutionListener. class , TransactionalTestExecutionListener. class , DbUnitTestExecutionListener. class }) @DbUnitConfiguration (dataSetLoader = ColumnSensingFlatXMLDataSetLoader. class ) public class ITRegistrationControllerTest { @Autowired private UsersConnectionRepository usersConnectionRepository; //Some fields are omitted for the sake of clarity. private MockMvc mockMvc; //The setUp() is omitted for the sake of clarity. @Test @DatabaseSetup ( "no-users.xml" ) @ExpectedDatabase (value= "no-users.xml" , assertionMode = DatabaseAssertionMode.NON_STRICT) public void registerUserAccount_SocialSignInAndEmptyForm_ShouldRenderRegistrationFormWithValidationErrors() throws Exception { TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder() .connectionData() .accessToken( "accessToken" ) .displayName( "John Smith" ) .expireTime(100000L) .providerId( "twitter" ) .providerUserId( "johnsmith" ) .refreshToken( "refreshToken" ) .secret( "secret" ) .usersConnectionRepository(usersConnectionRepository) .userProfile() .firstName( "John" ) .lastName( "Smith" ) .build(); RegistrationForm userAccountData = new RegistrationFormBuilder() .signInProvider(SocialMediaService.TWITTER) .build(); CsrfToken csrfToken = new CsrfTokenBuilder() .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME) .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME) .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE) .build(); mockMvc.perform(post( "/user/register" ) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData)) .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn) .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE) .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken) .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" )); } } |
Наш интеграционный тест использует файл набора данных DbUnit с именем no-users.xml, который выглядит следующим образом:
1
2
3
4
5
|
<? xml version = '1.0' encoding = 'UTF-8' ?> < dataset > < user_accounts /> < UserConnection /> </ dataset > |
Тест 2: адрес электронной почты найден в базе данных
Мы можем написать второй интеграционный тест, выполнив следующие действия:
- Добавьте поле UsersConnectionRepository в тестовый класс и аннотируйте его аннотацией @Autowired .
- Аннотируйте метод теста аннотацией @DatabaseSetup и настройте набор данных, который используется для инициализации базы данных в известное состояние до запуска нашего интеграционного теста.
- Создайте новый объект TestProviderSignInAttempt . Не забудьте установить используемый объект UsersConnectionRepository.
- Создайте новый объект RegistrationForm и установите значения его полей email , firstName , lastName и signInProvider . Используйте существующий адрес электронной почты.
- Создайте новый объект CsrfToken .
- Отправьте запрос POST на URL / user / register, выполнив следующие действия:
- Установите тип содержимого запроса «application / x-www-form-urlencoded».
- Преобразуйте объект формы в байты, закодированные в URL, и установите его в теле запроса.
- Установите созданный объект TestProviderSignInAttempt в сеанс.
- Установите значение токена CSRF для запроса в качестве параметра запроса.
- Установите созданный объект CsrfToken в сеанс.
- Установите созданный объект формы в сеанс.
- Убедитесь, что статус HTTP-запроса 200 возвращается.
- Убедитесь, что имя отображаемого представления — user / registrationForm.
- Убедитесь, что запрос пересылается по URL-адресу ‘/WEB-INF/jsp/user/registrationForm.jsp’.
- Убедитесь, что поля атрибута модели с именем ‘user’ являются правильными.
- Убедитесь, что у атрибута модели с именем ‘user’ есть ошибка поля в поле электронной почты .
- Аннотируйте метод теста аннотацией @ExpectedDatabase и убедитесь, что новая учетная запись пользователя не была сохранена в базе данных (используйте тот же набор данных, который использовался для инициализации базы данных).
Исходный код нашего интеграционного теста выглядит следующим образом:
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
|
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DbUnitConfiguration; import com.github.springtestdbunit.annotation.ExpectedDatabase; import com.github.springtestdbunit.assertion.DatabaseAssertionMode; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.social.connect.UsersConnectionRepository; 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.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (classes = {ExampleApplicationContext. class , IntegrationTestContext. class }) //@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"}) @WebAppConfiguration @TestExecutionListeners ({ DependencyInjectionTestExecutionListener. class , DirtiesContextTestExecutionListener. class , TransactionalTestExecutionListener. class , DbUnitTestExecutionListener. class }) @DbUnitConfiguration (dataSetLoader = ColumnSensingFlatXMLDataSetLoader. class ) public class ITRegistrationControllerTest { @Autowired private UsersConnectionRepository usersConnectionRepository; //Some fields are omitted for the sake of clarity. private MockMvc mockMvc; //The setUp() is omitted for the sake of clarity. @Test @DatabaseSetup ( "/net/petrikainulainen/spring/social/signinmvc/user/users.xml" ) @ExpectedDatabase (value = "/net/petrikainulainen/spring/social/signinmvc/user/users.xml" , assertionMode = DatabaseAssertionMode.NON_STRICT) public void registerUserAccount_SocialSignInAndEmailExist_ShouldRenderRegistrationFormWithFieldError() throws Exception { TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder() .connectionData() .accessToken( "accessToken" ) .displayName( "John Smith" ) .expireTime(100000L) .providerId( "twitter" ) .providerUserId( "johnsmith" ) .refreshToken( "refreshToken" ) .secret( "secret" ) .usersConnectionRepository(usersConnectionRepository) .userProfile() .email(IntegrationTestConstants.User.REGISTERED_USER.getUsername()) .firstName( "John" ) .lastName( "Smith" ) .build(); RegistrationForm userAccountData = new RegistrationFormBuilder() .email(IntegrationTestConstants.User.REGISTERED_USER.getUsername()) .firstName( "John" ) .lastName( "Smith" ) .signInProvider(SocialMediaService.TWITTER) .build(); CsrfToken csrfToken = new CsrfTokenBuilder() .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME) .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME) .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE) .build(); mockMvc.perform(post( "/user/register" ) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData)) .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn) .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE) .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken) .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(IntegrationTestConstants.User.REGISTERED_USER.getUsername())), hasProperty( "firstName" , is( "John" )), hasProperty( "lastName" , is( "Smith" )), hasProperty( "password" , isEmptyOrNullString()), hasProperty( "passwordVerification" , isEmptyOrNullString()), hasProperty( "signInProvider" , is(SocialMediaService.TWITTER)) ))) .andExpect(model().attributeHasFieldErrors( "user" , "email" )); } } |
В этом интеграционном тесте используется набор данных DbUnit с именем users.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
|
<? xml version = '1.0' encoding = 'UTF-8' ?> < dataset > < user_accounts id = "1" creation_time = "2014-02-20 11:13:28" first_name = "Facebook" last_name = "User" modification_time = "2014-02-20 11:13:28" role = "ROLE_USER" sign_in_provider = "FACEBOOK" version = "0" /> < user_accounts id = "2" creation_time = "2014-02-20 11:13:28" first_name = "Twitter" last_name = "User" modification_time = "2014-02-20 11:13:28" role = "ROLE_USER" sign_in_provider = "TWITTER" version = "0" /> < user_accounts id = "3" creation_time = "2014-02-20 11:13:28" first_name = "RegisteredUser" last_name = "User" modification_time = "2014-02-20 11:13:28" password = "$2a$10$PFSfOaC2IFPG.1HjO05KoODRVSdESQ5q7ek4IyzVfTf.VWlKDa/.e" role = "ROLE_USER" version = "0" /> < UserConnection /> </ dataset > |
Тест 3: регистрация прошла успешно
Мы можем написать третий интеграционный тест, выполнив следующие действия:
- Добавьте поле UsersConnectionRepository в тестовый класс и аннотируйте его аннотацией @Autowired .
- Аннотируйте метод теста аннотацией @DatabaseSetup и настройте набор данных, который используется для инициализации базы данных в известное состояние до запуска нашего интеграционного теста.
- Создайте новый объект TestProviderSignInAttempt . Не забудьте установить используемый объект UsersConnectionRepository .
- Создайте новый объект RegistrationForm и установите значения его полей email , firstName , lastName и signInProvider .
- Создайте новый объект CsrfToken .
- Отправьте запрос POST на URL / user / register, выполнив следующие действия:
- Установите тип содержимого запроса «application / x-www-form-urlencoded».
- Преобразуйте объект формы в байты, закодированные в URL, и установите его в теле запроса.
- Установите созданный объект TestProviderSignInAttempt в сеанс.
- Установите значение токена CSRF для запроса в качестве параметра запроса.
- Установите созданный объект CsrfToken в сеанс.
- Установите созданный объект формы в сеанс.
- Убедитесь, что возвращается статус HTTP-запроса 302.
- Убедитесь, что запрос перенаправлен на URL ‘/’. Это также гарантирует, что созданный пользователь вошел в систему, потому что анонимные пользователи не могут получить доступ к этому URL.
- Аннотируйте метод теста с помощью аннотации @ExpectedDatabase и убедитесь, что новая учетная запись пользователя была сохранена в базе данных, а соединение с используемым поставщиком социальных сетей сохранено.
Исходный код нашего интеграционного теста выглядит следующим образом:
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
|
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DbUnitConfiguration; import com.github.springtestdbunit.annotation.ExpectedDatabase; import com.github.springtestdbunit.assertion.DatabaseAssertionMode; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.social.connect.UsersConnectionRepository; 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.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (classes = {ExampleApplicationContext. class , IntegrationTestContext. class }) //@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"}) @WebAppConfiguration @TestExecutionListeners ({ DependencyInjectionTestExecutionListener. class , DirtiesContextTestExecutionListener. class , TransactionalTestExecutionListener. class , DbUnitTestExecutionListener. class }) @DbUnitConfiguration (dataSetLoader = ColumnSensingFlatXMLDataSetLoader. class ) public class ITRegistrationControllerTest2 { @Autowired private UsersConnectionRepository usersConnectionRepository; //Some fields are omitted for the sake of clarity. private MockMvc mockMvc; //The setUp() is omitted for the sake of clarity. @Test @DatabaseSetup ( "no-users.xml" ) @ExpectedDatabase (value= "register-social-user-expected.xml" , assertionMode = DatabaseAssertionMode.NON_STRICT) public void registerUserAccount_SocialSignIn_ShouldCreateNewUserAccountAndRenderHomePage() throws Exception { TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder() .connectionData() .accessToken( "accessToken" ) .displayName( "John Smith" ) .expireTime(100000L) .providerId( "twitter" ) .providerUserId( "johnsmith" ) .refreshToken( "refreshToken" ) .secret( "secret" ) .usersConnectionRepository(usersConnectionRepository) .userProfile() .firstName( "John" ) .lastName( "Smith" ) .build(); RegistrationForm userAccountData = new RegistrationFormBuilder() .firstName( "John" ) .lastName( "Smith" ) .signInProvider(SocialMediaService.TWITTER) .build(); CsrfToken csrfToken = new CsrfTokenBuilder() .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME) .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME) .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE) .build(); mockMvc.perform(post( "/user/register" ) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData)) .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn) .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE) .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken) .sessionAttr( "user" , userAccountData) ) .andExpect(status().isMovedTemporarily()) .andExpect(redirectedUrl( "/" )); } } |
Набор данных ( no-users.xml ), который используется для инициализации базы данных в известное состояние, выглядит следующим образом:
1
2
3
4
5
|
<? xml version = '1.0' encoding = 'UTF-8' ?> < dataset > < user_accounts /> < UserConnection /> </ dataset > |
Набор данных DbUnit с именем register-social-user-Ожидаемый.xml используется для проверки того, что учетная запись пользователя была успешно создана и соединение с используемым поставщиком социального входа в систему было сохранено в базе данных. Это выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
<? xml version = '1.0' encoding = 'UTF-8' ?> < dataset > first_name = "John" last_name = "Smith" role = "ROLE_USER" sign_in_provider = "TWITTER" version = "0" /> providerId = "twitter" providerUserId = "johnsmith" rank = "1" displayName = "John Smith" accessToken = "accessToken" secret = "secret" refreshToken = "refreshToken" expireTime = "100000" /> </ dataset > |
Резюме
Теперь мы узнали, как мы можем написать интеграционные тесты для обычного приложения Spring MVC, которое использует Spring Social 1.1.0. Этот урок научил нас многим вещам, но эти две вещи являются ключевыми уроками этого сообщения в блоге:
- Мы узнали, как мы можем «симулировать» социальный вход, создав объекты ProviderSignInAttempt и используя их в наших интеграционных тестах.
- Мы узнали, как мы можем создавать токены CSRF и предоставлять созданные токены в Spring Security.
Давайте потратим немного времени и проанализируем все за и против подхода, описанного в этом посте:
Плюсы:
- Мы можем написать интеграционные тесты без использования внешнего социального входа в провайдера. Это делает наши тесты менее хрупкими и проще в обслуживании.
- Детали реализации Spring Social ( ProviderSignInAttempt ) и Spring Security CSRF protection ( CsrfToken ) «скрыты» для тестирования классов построителя данных. Это делает наши тесты более удобочитаемыми и простыми в обслуживании.
Минусы:
- В этом руководстве не описывается, как мы можем написать интеграционные тесты для входа в социальную сеть (войти в систему с помощью поставщика социальной регистрации). Я пытался найти способ написания этих тестов без использования внешнего провайдера входа, но у меня просто не хватило времени (это казалось сложным, и я хотел опубликовать этот пост в блоге).
Этот пост завершает мое руководство «Добавление входа в социальную сеть в приложение Spring MVC».
Я напишу аналогичное руководство, которое описывает, как мы можем добавить социальный вход в REST API на базе Spring в будущем. А пока вы можете прочитать другие части этого урока .
- Вы можете получить пример приложения к этому сообщению в блоге от Github .