我已经写了关于为使用Spring Social 1.1.0的应用程序编写单元测试的挑战,并为此提供了一种解决方案 。
尽管单元测试很有价值,但是它并不能真正告诉我们我们的应用程序是否正常运行。
这就是为什么我们必须为此编写集成测试的原因 。
这篇博客文章可以帮助我们做到这一点。 在此博客文章中,我们将学习如何为示例应用程序的注册和登录功能编写集成测试。
如果您尚未阅读Spring Social教程的先前部分,建议您在阅读此博客文章之前先阅读它们。 以下描述了此博客文章的前提条件:
- 在Spring MVC Web应用程序中添加社交登录:配置描述了如何配置示例应用程序。
- 在Spring MVC Web应用程序中添加社交登录:注册和登录介绍了如何向示例应用程序添加注册和登录功能。
- 在Spring MVC Web应用程序中添加社交登录:单元测试描述了如何为示例应用程序编写单元测试。
- Spring MVC测试教程描述了如何使用Spring MVC Test框架编写单元测试和集成测试。
- Spring Data JPA教程:集成测试描述了我们如何为Spring Data JPA存储库编写集成测试。 这篇博客文章帮助您了解如何使用Spring Test DBUnit和DbUnit编写集成测试。
- 使用Maven进行集成测试介绍了如何使用Maven运行集成测试和单元测试。 我们的示例应用程序的构建过程遵循此博客文章中描述的方法。
让我们从对构建过程的配置进行一些更改开始。
配置我们的构建过程
我们必须对构建过程的配置进行以下更改:
- 我们已经配置了一个本地Maven存储库,并将Spring Test DbUnit 1.1.1快照二进制文件添加到该存储库。
- 我们必须将所需的测试依赖项添加到我们的POM文件中。
- 我们必须将Liquibase变更集文件添加到classpath。
让我们找出如何进行这些更改。
将Spring Test DBUnit快照二进制文件添加到本地Maven存储库
由于Spring Test DBUnit的稳定版本与Spring Framework 4不兼容 ,因此我们必须在集成测试中使用构建快照。
我们可以按照以下步骤将Spring Test DBUnit快照添加到本地Maven存储库:
- 从Github克隆Spring Test DBUnit存储库并创建快照二进制文件。
- 创建etc / mavenrepo目录。 该目录是我们本地的Maven存储库。
- 将创建的jar文件复制到etc / mavenrepo / com / github / springtestdbunit / 1.1.1-SNAPSHOT目录。
将jar文件复制到本地Maven存储库后,我们必须在pom.xml文件中配置本地存储库的位置。 我们可以通过将以下存储库声明添加到我们的POM文件中来做到这一点:
<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测试DBUnit (版本1.1.1-SNAPSHOT)。 我们使用Spring Test DBUnit将Spring Test框架与DbUnit库集成在一起。
- DbUnit (版本2.4.9)。 在每次集成测试之前,我们使用DbUnit将数据库初始化为已知状态,并验证数据库的内容是否与预期数据匹配。
- liquibase-core (版本3.1.1)。 加载集成测试的应用程序上下文时,我们使用Liquibase创建一些数据库表。
pom.xml文件的相关部分如下所示:
<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表。
为此,我们可以使用Liquibase库的Spring集成,但这意味着我们必须将Liquibase变更集添加到类路径中。
我们可以通过使用Build Helper Maven插件来实现 。 我们可以按照以下步骤将Liquibase变更集添加到类路径中:
- 确保在generate-test-resources生命周期阶段调用了Builder Helper Maven插件的add-test-resource目标。
- 配置插件以将etc / db目录添加到类路径(此目录包含所需的文件)。
插件配置的相关部分如下所示:
<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上下文来指定
- 当创建示例应用程序的数据库时,将执行哪些变更集。
- 当我们运行集成测试时,将执行哪些变更集。
通过执行以下步骤,我们可以实现我们的目标:
- 指定当Liquibase上下文是db或integrationtest时,将执行db-0.0.1.sql changeset文件。
- 指定当Liquibase上下文为db时执行db-0.0.2.sql changeset文件。
我们的Liquibase changelog文件如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<!-- 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注释对其进行注释。 此方法配置SpringLiquibase bean,该bean在加载应用程序上下文时执行liquibase变更集。
- 通过执行以下步骤来实现liquibase()方法:
- 创建一个新的SpringLiquibase对象。
- 配置创建的对象使用的数据源。
- 配置Liquibase更改日志的位置。
- 将Liquibase上下文设置为“ integrationtest”。
- 返回创建的对象。
IntegrationTestContext类的源代码如下所示:
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数据集如下所示:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user_accounts id="1"
creation_time="2014-02-20 11:13:28"
email="facebook@socialuser.com"
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"
email="twitter@socialuser.com"
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"
email="registered@user.com"
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类的源代码如下所示:
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注释对该方法进行注释,以确保在每个测试方法之前调用此方法。
- 使用MockMvcBuilders类,实现setUp()方法并创建一个新的MockMvc对象。
空测试类的源代码如下所示:
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 Test框架。 本教程讨论了单元测试,但仍应进一步阐明该问题。
- Spring Data JPA教程:集成测试描述了如何为Spring Data JPA存储库编写集成测试。 如果您想看一下Spring Test DBUnit的配置,这篇博客文章可能有助于您理解它。
- Spring MVC应用程序的集成测试:安全性描述了如何编写Spring MVC应用程序的安全性测试。 本教程基于Spring Security 3.1,但它仍然可以帮助您了解如何编写这些测试。
现在,我们已经了解了如何配置集成测试。 让我们继续并创建一些在集成测试中使用的测试实用程序类。
创建测试实用程序类
接下来,我们将创建在集成测试中使用的三个实用程序类:
- 我们将创建IntegrationTestConstants类,其中包含多个集成测试中使用的常量。
- 我们将创建用于为集成测试创建ProviderSignInAttempt对象的类。
- 我们将创建一个测试数据构建器类,该类用于创建CsrfToken对象。
让我们找出为什么我们必须创建这些类以及如何创建它们。
创建IntegrationTestConstants类
当我们编写集成(或单元)测试时,有时我们需要在许多测试类中使用相同的信息。 将这些信息复制到所有测试类是一个坏主意,因为这会使我们的测试难以维护和理解。 相反,我们应该将此信息放在一个类中,并在需要时从该类中获取。
IntegrationTestConstants类包含以下信息,这些信息可在多个测试类中使用:
- 它具有与Spring Security 3.2的CSRF保护相关的常数。 这些常量包括:包含CSRF令牌的HTTP标头的名称,包含CSRF令牌的值的请求参数的名称,包含CsrfToken对象的会话属性的名称以及CSRF令牌的值。
- 它包含User枚举,该枚举指定了我们的集成测试中使用的用户。 每个用户都有一个用户名和密码(这不是必需的)。 该枚举的信息用于两个目的:
- 用于指定登录用户。 当我们对受保护的功能(需要某种授权的功能)进行集成测试时,这很有用。
- 在为登录功能编写集成测试时,我们需要指定尝试登录该应用程序的用户的用户名和密码。
IntegrationTestConstants类的源代码如下所示:
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 {
FACEBOOK_USER("facebook@socialuser.com", null),
REGISTERED_USER("registered@user.com", "password"),
TWITTER_USER("twitter@socialuser.com", null);
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 <?>对象,并验证特定的连接是否“持久化到数据库”。 但是,我们的存根类未持久连接到使用的数据库。 而是将用户的用户ID存储到Set对象。
因为现在我们想将连接数据持久化到数据库,所以我们必须对存根类进行更改。 我们可以通过对TestProviderSignInAttempt对象进行以下更改来进行这些更改:
- 将一个专用的usersConnectionRepositorySet字段添加到TestProviderSignInAttempt类。 该字段的类型为布尔值 ,其默认值为false。 该字段描述了我们是否可以持久连接到使用的数据存储。
- 将一个新的构造函数参数添加到TestProviderSignInAttempt类的构造函数中。 此参数的类型为UsersConnectionRepository ,用于持久连接到使用的数据存储。
- 通过执行以下步骤来实现构造函数:
- 调用超类的构造函数,并将Connection <?>和UsersConnectionRepository对象作为构造函数参数传递。
- 存储对Connection <?>对象的引用,该引用作为连接字段的构造函数参数给出。
- 如果作为构造函数参数给出的UsersConnectionRepository对象不为null,则将usersConnectionRepositoryField的值设置为true。
- 通过执行以下步骤来实现addConnection()方法:
- 将作为方法参数给出的用户ID添加到连接 Set中 。
- 如果在创建新的TestProviderSignInAttempt对象时设置了UsersConnectionRepository对象,请调用ProviderSignInAttempt类的addConnection()方法,并将用户ID作为方法参数传递。
TestProviderSignInAttempt类的源代码如下所示(修改的部分突出显示):
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; } }
因为我们通过使用TestProviderSignInAttemptBuilder来构建新的TestProviderSignInAttempt对象,所以我们也必须对该类进行更改。 我们可以按照以下步骤进行这些更改:
- 将私有的usersConnectionRepository字段添加到TestProviderSignInAttemptBuilder类,并将其类型设置为UsersConnectionRepository 。
- 向类添加一个usersConnectionRepository()方法。 将对 UsersConnectionRepository对象的引用设置为usersConnectionRepository字段,并返回对构建器对象的引用。
- 修改build()方法的最后一行,并使用我们之前创建的新构造函数创建一个新的TestProviderSignInAttempt对象。
TestProviderSignInAttemptBuilder类的源代码如下所示(修改的部分突出显示):
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对象
因为我们的示例应用程序使用了Spring Security 3.2提供的CSRF保护,所以我们必须找出一种在集成测试中创建有效CSRF令牌的方法。 CsrfToken接口声明了提供有关预期CSRF令牌信息的方法。 该接口具有一个称为DefaultCsrfToken的实现。
换句话说,我们必须找出一种创建新的DefaultCsrfToken对象的方法。 DefaultCsrfToken类只有一个构造函数 ,在集成测试中创建新的DefaultCsrfToken对象时,我们当然可以使用它。 问题是这不是很可读。
相反,我们将创建一个测试数据构建器类 , 该类提供了用于创建新CsrfToken对象的流利API 。 我们可以按照以下步骤创建此类:
- 创建一个CsrfTokenBuilder类。
- 将私有的headerName字段添加到创建的类。
- 将私有requestParameterName字段添加到创建的类中。
- 将私有tokenValue字段添加到创建的类。
- 向创建的类添加发布构造函数。
- 添加用于设置headerName , requestParameterName和tokenValue字段的字段值的方法。
- 向所创建的类中添加一个build()方法,并将其返回类型设置为CsrfToken 。 通过执行以下步骤来实现此方法:
- 创建一个新的DefaultCsrfToken对象,并提供CSRF令牌标头的名称,CSRF令牌请求参数的名称以及CSRF令牌的值作为构造函数参数。
- 返回创建的对象。
CsrfTokenBuilder类的源代码如下所示:
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对象:
CsrfToken csrfToken = new CsrfTokenBuilder() .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME) .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME) .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE) .build();
现在,我们已经创建了必需的测试实用程序类。 让我们继续并开始为示例应用程序编写集成测试。
编写集成测试
我们终于准备好为示例应用程序编写一些集成测试。 我们将编写以下集成测试:
- 我们将编写集成测试,以确保表单登录正常工作。
- 我们将编写集成测试,以验证使用社交登录时注册是否正常运行。
但是在开始编写这些集成测试之前,我们将学习如何为Spring Security提供有效的CSRF令牌。
向Spring Security提供有效的CSRF令牌
之前我们了解了如何在集成测试中创建CsrfToken对象。 但是,我们仍然必须找出一种将这些CSRF令牌提供给Spring Security的方法。
现在是时候仔细研究一下Spring Security处理CSRF令牌的方式了。
CsrfTokenRepository接口声明生成,保存和加载CSRF令牌所需的方法。 该接口的默认实现是HttpSessionCsrfTokenRepository类,该类将CSRF令牌存储到HTTP会话。
我们需要找到以下问题的答案:
- CSRF令牌如何保存到HTTP会话?
- 如何从HTTP会话加载CSRF令牌?
通过查看HttpSessionCsrfTokenRepository类的源代码,我们可以找到这些问题的答案。 HttpSessionCsrfTokenRepository类的相关部分如下所示:
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令牌作为CsrfToken对象存储到HTTP会话,并且使用sessionAttributeName属性的值重试和存储这些对象。 这意味着,如果我们想向Spring Security提供有效的CSRF令牌,则必须遵循以下步骤:
- 使用我们的测试数据构建器创建一个新的CsrfToken对象。
- 发送CSRF令牌的值作为请求参数。
- 将创建的DefaultCsrfToken对象存储到HTTP会话中,以便HttpSessionCsrfTokenRepository找到它。
我们的虚拟测试的源代码如下所示:
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 )将数据库初始化为已知状态,其内容如下所示:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user_accounts id="1"
creation_time="2014-02-20 11:13:28"
email="facebook@socialuser.com"
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"
email="twitter@socialuser.com"
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"
email="registered@user.com"
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设置为session。
- 确保返回HTTP状态代码302。
- 验证请求已重定向到URL“ /”。
我们的集成测试的源代码如下所示:
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对象设置为session。
- 确保返回HTTP状态代码302。
- 验证请求是否重定向到URL'/ login?error = bad_credentials'。
我们的集成测试的源代码如下所示:
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对象设置为session。
- 将CSRF令牌的值设置为请求作为请求参数。
- 将创建的CsrfToken对象设置为session。
- 将创建的表单对象设置为session。
- 确保返回HTTP请求状态200。
- 确保渲染视图的名称为“ user / registrationForm”。
- 验证请求是否转发到URL'/WEB-INF/jsp/user/registrationForm.jsp'。
- 验证名为“ user”的模型属性的字段正确。
- 确保名为'user'的模型属性在email , firstName和lastName字段中存在字段错误。
- 使用@ExpectedDatabase批注注释测试方法,并确保未将新用户帐户保存到数据库(使用用于初始化数据库的相同数据集)。
我们的集成测试的源代码如下所示:
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)
.imageUrl("https://www.twitter.com/images/johnsmith.jpg")
.profileUrl("https://www.twitter.com/johnsmith")
.providerId("twitter")
.providerUserId("johnsmith")
.refreshToken("refreshToken")
.secret("secret")
.usersConnectionRepository(usersConnectionRepository)
.userProfile()
.email("john.smith@gmail.com")
.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"));
}
}
我们的集成测试使用一个名为no-users.xml的DbUnit数据集文件,该文件如下所示:
<?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对象设置为session。
- 将CSRF令牌的值设置为请求作为请求参数。
- 将创建的CsrfToken对象设置为session。
- 将创建的表单对象设置为session。
- 确保返回HTTP请求状态200。
- 确保渲染视图的名称为“ user / registrationForm”。
- 验证请求是否转发到URL'/WEB-INF/jsp/user/registrationForm.jsp'。
- 验证名为“ user”的模型属性的字段正确。
- 确保名为“用户”的模型属性在电子邮件字段中存在字段错误。
- 使用@ExpectedDatabase批注注释测试方法,并确保未将新用户帐户保存到数据库(使用用于初始化数据库的相同数据集)。
我们的集成测试的源代码如下所示:
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)
.imageUrl("https://www.twitter.com/images/johnsmith.jpg")
.profileUrl("https://www.twitter.com/johnsmith")
.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"));
}
}
此集成测试使用一个名为users.xml的DbUnit数据集,该数据集如下所示:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user_accounts id="1"
creation_time="2014-02-20 11:13:28"
email="facebook@socialuser.com"
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"
email="twitter@socialuser.com"
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"
email="registered@user.com"
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对象设置为session。
- 将CSRF令牌的值设置为请求作为请求参数。
- 将创建的CsrfToken对象设置为session。
- 将创建的表单对象设置为session。
- 确保返回HTTP请求状态302。
- 验证请求是否重定向到URL“ /”。 这还可以确保创建的用户已登录,因为匿名用户无法访问该URL。
- 使用@ExpectedDatabase批注对测试方法进行批注,并确保将新用户帐户保存到数据库中,并且与使用的社交媒体提供者的连接得以持久。
我们的集成测试的源代码如下所示:
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)
.imageUrl("https://www.twitter.com/images/johnsmith.jpg")
.profileUrl("https://www.twitter.com/johnsmith")
.providerId("twitter")
.providerUserId("johnsmith")
.refreshToken("refreshToken")
.secret("secret")
.usersConnectionRepository(usersConnectionRepository)
.userProfile()
.email("john.smith@gmail.com")
.firstName("John")
.lastName("Smith")
.build();
RegistrationForm userAccountData = new RegistrationFormBuilder()
.email("john.smith@gmail.com")
.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 )如下所示:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user_accounts/>
<UserConnection/>
</dataset>
名为register-social-user-expected.xml的DbUnit数据集用于验证是否成功创建了用户帐户,并且与使用的社交登录提供者的连接已持久保存到数据库中。 它看起来如下:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user_accounts email="john.smith@gmail.com"
first_name="John" last_name="Smith"
role="ROLE_USER"
sign_in_provider="TWITTER"
version="0"/>
<UserConnection userId="john.smith@gmail.com"
providerId="twitter"
providerUserId="johnsmith"
rank="1"
displayName="John Smith"
profileUrl="https://www.twitter.com/johnsmith"
imageUrl="https://www.twitter.com/images/johnsmith.jpg"
accessToken="accessToken"
secret="secret"
refreshToken="refreshToken"
expireTime="100000"/>
</dataset>
摘要
现在,我们已经了解了如何为使用Spring Social 1.1.0的常规Spring MVC应用程序编写集成测试。 本教程教会了我们很多东西,但是这两件事是本博客文章的主要课程:
- 我们了解了如何通过创建ProviderSignInAttempt对象并在集成测试中使用它们来“模拟”社交登录。
- 我们学习了如何创建CSRF令牌并将创建的令牌提供给Spring Security。
让我们花点时间来分析此博客文章中描述的方法的优缺点:
优点:
- 我们可以编写集成测试,而无需使用外部社交登录提供程序。 这使我们的测试不那么脆弱,更易于维护。
- Spring Social( ProviderSignInAttempt )和Spring Security CSRF保护( CsrfToken )的实现细节被“隐藏”以测试数据构建器类。 这使我们的测试更具可读性,更易于维护。
缺点:
- 本教程没有描述我们如何编写社交登录集成测试(使用社交登录提供程序登录)。 我试图找出一种无需使用外部登录提供程序即可编写这些测试的方法,但我只是用光了时间(这似乎很复杂,我想发布此博客文章)。
这篇博客文章结束了我的“向Spring MVC应用程序添加社交登录”教程。
我将写一个类似的教程,描述将来如何在社会支持的REST API中添加社交登录。 同时,您可能需要阅读本教程的其他部分 。
- 您可以从Github获得此博客文章的示例应用程序。