Spring Social 1.0具有spring-social-test模块,该模块为测试Connect实现和API绑定提供支持。 该模块已从Spring Social 1.1.0中删除,并由 Spring MVC Test框架替换。

问题在于,实际上没有有关为使用Spring Social 1.1.0的应用程序编写单元测试的信息。

这篇博客文章解决了这个问题

在此博客文章中,我们将学习如何为示例应用程序的注册功能编写单元测试,该功能是在本Spring Social教程的前面部分中创建的。

注意:如果您尚未阅读Spring Social教程的先前部分,建议您在阅读此博客文章之前先阅读它们。 以下描述了此博客文章的前提条件:

  • 在Spring MVC Web应用程序中添加社交登录:配置描述了如何配置示例应用程序的应用程序上下文。
  • 在Spring MVC Web应用程序中添加社交登录:注册和登录介绍了如何向示例应用程序添加注册和登录功能。
  • Spring MVC测试教程描述了如何使用Spring MVC Test框架编写单元测试和集成测试。

让我们从发现如何使用Maven获得所需的测试标准开始。

使用Maven获取所需的依赖关系

我们可以通过在POM文件中声明以下依赖关系来获得所需的测试依赖关系:

  • FEST声明(1.4版)。 FEST-Assert是一个提供流畅接口以编写断言的库。
  • hamcrest-all(1.4版)。 我们使用Hamcrest匹配器在单元测试中编写断言。
  • JUnit (版本4.11)。 我们还需要排除hamcrest-core,因为我们已经添加了hamcrest-all依赖项。
  • 全模拟(版本1.9.5)。 我们使用Mockito作为我们的模拟库。
  • Catch-Exception(版本1.2.0)。 catch-exception库可帮助我们在不终止测试方法执行的情况下捕获异常,并使捕获的异常可用于进一步分析。 由于我们已经添加了“ mockito-all”依赖性,因此需要排除“ mockito-core”依赖性。
  • Spring测试(版本3.2.4.RELEASE)。 Spring Test Framework是一个框架,可以为基于Spring的应用程序编写测试。

pom.xml文件的相关部分如下所示:

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

让我们动起来,快速浏览一下Spring Social的内部。

展望Spring社交网络

我们可能在本教程的第二部分中还记得过 , RegistrationController类负责呈现注册表单并处理注册表单的表单提交。 它使用ProviderSignInUtils类有两个目的:

  1. 呈现注册表单时,如果用户正在使用社交登录创建新的用户帐户,则RegistrationController类会预先填充表单字段。表单对象是使用所用SaaS API提供程序提供的信息来预先填充的。 此信息存储到Connection对象。 控制器类通过调用ProviderSignInUtils类的静态getConnection()方法来获取Connection对象。
  2. 创建新用户帐户后,如果用户帐户是使用社交登录创建的,则RegistrationConnection类会将Connection对象保留在数据库中。控制器类通过调用ProviderSignInUtils类的handlePostSignUp()方法来实现此目的 。

如果我们想了解ProviderSignInUtils类的作用,请看一下其源代码。 ProviderSignInUtils类的源代码如下所示:

package org.springframework.social.connect.web;import org.springframework.social.connect.Connection;
import org.springframework.web.context.request.RequestAttributes;public class ProviderSignInUtils {public static Connection<?> getConnection(RequestAttributes request) {ProviderSignInAttempt signInAttempt = getProviderUserSignInAttempt(request);return signInAttempt != null ? signInAttempt.getConnection() : null;}public static void handlePostSignUp(String userId, RequestAttributes request) {ProviderSignInAttempt signInAttempt = getProviderUserSignInAttempt(request);if (signInAttempt != null) {signInAttempt.addConnection(userId);request.removeAttribute(ProviderSignInAttempt.SESSION_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);}      }private static ProviderSignInAttempt getProviderUserSignInAttempt(RequestAttributes request) {return (ProviderSignInAttempt) request.getAttribute(ProviderSignInAttempt.SESSION_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);}
}

我们可以从ProviderSignInUtils类的源代码中看到两件事:

  1. getConnection()方法从会话中获取ProviderSignInAttempt对象。 如果获取的对象为null,则返回null。 否则,它将调用ProviderSignInAttempt类的getConnection()方法并返回Connection对象。
  2. handlePostSignUp()方法从会话中获取ProviderSignInAttempt对象。 如果找到该对象,它将调用ProviderSignInAttempt类的addConnection()方法,并从会话中删除找到的ProviderSignInAttempt对象。

显然,为了为RegistrationController类编写单元测试,我们必须找出一种创建ProviderSignInAttempt对象并将创建的对象设置为session的方法。

让我们找出这是如何完成的。

创建测试双打

如我们所知,如果要为RegistrationController类编写单元测试,则必须找到一种创建ProviderSignInAttempt对象的方法。 本节介绍如何通过使用测试双打来实现此目标。

让我们继续前进,了解如何在单元测试中创建ProviderSignInAttempt对象。

创建ProviderSignInAttempt对象

如果我们想了解如何创建ProviderSignInAttempt对象,则必须仔细查看其源代码。 ProviderSignInAttempt类的源代码如下所示:

package org.springframework.social.connect.web;import java.io.Serializable;import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.DuplicateConnectionException;
import org.springframework.social.connect.UsersConnectionRepository;@SuppressWarnings("serial")
public class ProviderSignInAttempt implements Serializable {public static final String SESSION_ATTRIBUTE = ProviderSignInAttempt.class.getName();private final ConnectionData connectionData;private final ConnectionFactoryLocator connectionFactoryLocator;private final UsersConnectionRepository connectionRepository;public ProviderSignInAttempt(Connection<?> connection, ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository connectionRepository) {this.connectionData = connection.createData();this.connectionFactoryLocator = connectionFactoryLocator;this.connectionRepository = connectionRepository;      }public Connection<?> getConnection() {return connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId()).createConnection(connectionData);}void addConnection(String userId) {connectionRepository.createConnectionRepository(userId).addConnection(getConnection());}
}

如我们所见, ProviderSignInAttempt类具有三个依赖关系,如下所示:

  • Connection接口表示与使用的SaaS API提供程序的连接。
  • ConnectionFactoryLocator接口指定查找ConnectionFactory对象所需的方法。
  • UsersConnectionRepository接口声明用于管理用户与SaaS API提供程序之间的连接的方法。

首先想到的是模拟这些依赖关系。 尽管这似乎是一个好主意,但是这种方法有两个问题:

  1. 在编写的每个测试中,我们都必须配置模拟对象的行为。 这意味着我们的测试将更难理解。
  2. 我们正在将Spring Social的实现细节泄漏到我们的测试中。 这将使我们的测试难以维护,因为如果实施Spring Social更改,我们的测试可能会被破坏。

显然,模拟并不是解决此问题的最佳解决方案。 我们必须记住,即使模拟是一种有价值且方便的测试工具, 我们也不应过度使用它 。

这就产生了一个新问题:

如果无法进行模拟,那么什么才是正确的工具?

这个问题的答案可以从Martin Fowler的一篇文章中找到。 在本文中,马丁·福勒(Martin Fowler)指定了一个称为存根的测试双精度,如下所示:

存根提供对测试过程中进行的呼叫的固定答复,通常通常根本不响应为测试编程的内容。 存根还可以记录有关呼叫的信息,例如,电子邮件网关存根可以记住“已发送”的消息,或者仅记住“已发送”的消息数量。

使用存根非常有意义,因为我们对两件事感兴趣:

  1. 我们需要能够配置存根返回的Connection <?>对象。
  2. 创建新的用户帐户后,我们需要验证连接是否与数据库保持一致。

我们可以按照以下步骤创建一个实现这些目标的存根:

  1. 创建一个TestProviderSignInAttempt类,该类扩展了ProviderSignInAttempt类。
  2. 将私有连接字段添加到该类,并将添加的字段的类型设置为Connection <?> 。 该字段包含对用户和SaaS API提供程序之间的连接的引用。
  3. 将私有连接字段添加到该类,并将添加到的字段的类型设置Set <String> 。 该字段包含持久连接的用户标识。
  4. 向创建的类添加一个将Connection <?>对象作为构造函数参数的构造函数。 通过执行以下步骤来实现构造函数:
    1. 调用ProviderSignInAttempt类的构造函数,并将Connection <?>对象作为构造函数参数传递。 将其他构造函数参数的值设置为null
    2. 将作为构造函数参数提供的Connection <?>对象设置为connection字段。
  5. 重写ProviderSignInAttempt类的getConnection()方法,并通过将存储的对象返回到连接字段来实现它。
  6. 重写ProviderSignInAttempt类的addConnection(String userId)方法,并通过将作为方法参数给出的用户ID添加到连接集中来实现它。
  7. 将公共getConnections()方法添加到创建的类中,并通过返回连接集来实现它。

TestProviderSignInAttempt的源代码如下所示:

package org.springframework.social.connect.web;import org.springframework.social.connect.Connection;import java.util.HashSet;
import java.util.Set;public class TestProviderSignInAttempt extends ProviderSignInAttempt {private Connection<?> connection;private Set<String> connections = new HashSet<>();public TestProviderSignInAttempt(Connection<?> connection) {super(connection, null, null);this.connection = connection;}@Overridepublic Connection<?> getConnection() {return connection;}@Overridevoid addConnection(String userId) {connections.add(userId);}public Set<String> getConnections() {return connections;}
}

让我们继续前进,找出如何创建用于单元测试的Connection <?>类。

创建连接类

创建的连接类是一个存根类,它模拟“真实”连接类的行为,但是它没有实现与OAuth1和OAuth2连接关联的任何逻辑。 同样,此类必须实现Connection接口。

我们可以按照以下步骤创建此存根类:

  1. 创建一个TestConnection类,该类扩展了AbstractConnection类。 AbstractConnection类是基类,它定义了所有连接实现共享的状态和行为。
  2. connectionData字段添加到创建的类。 将字段的类型设置为ConnectionDataConnectionData是一个数据传输对象,其中包含与使用的SaaS API提供程序的连接的内部状态。
  3. userProfile字段添加到创建的类。 将字段的类型设置为UserProfile 。 此类表示所使用的SaaS API提供程序的用户配置文件,并且包含在不同服务提供程序之间共享的信息。
  4. 创建一个将ConnectionDataUserProfile对象作为构造函数参数的构造函数,并按照以下步骤实现它:
    1. 调用AbstractConnection类的构造函数,并将ConnectionData对象作为第一个构造函数参数传递。 将第二个构造函数参数设置为null
    2. 设置connectionData字段的值。
    3. 设置userProfile字段的值。
  5. 重写AbstractConnection类的fetchUserProfile()方法,并通过将存储的对象返回到userProfile字段来实现它。
  6. 重写AbstractConnection类的getAPI()方法,并通过返回null来实现它。
  7. 重写AbstractConnection类的createData()方法,并通过将存储的对象返回到connectionData字段来实现它。

TestConnection类的源代码如下所示:

package org.springframework.social.connect.support;import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.UserProfile;public class TestConnection extends AbstractConnection {private ConnectionData connectionData;private UserProfile userProfile;public TestConnection(ConnectionData connectionData, UserProfile userProfile) {super(connectionData, null);this.connectionData = connectionData;this.userProfile = userProfile;}@Overridepublic UserProfile fetchUserProfile() {return userProfile;}@Overridepublic Object getApi() {return null;}@Overridepublic ConnectionData createData() {return connectionData;}
}

让我们继续前进,弄清楚如何在单元测试中创建这些测试双打。

创建构建器类

现在,我们为单元测试创​​建了存根类。 我们的最后一步是弄清楚如何使用这些类创建TestProviderSignInAttempt对象。

至此,我们知道

  1. TestProviderSignInAttempt类的构造函数将Connection对象作为构造函数参数。
  2. TestConnection类的构造函数将ConnectionDataUserProfile对象用作构造函数参数。

这意味着我们可以按照以下步骤创建新的TestProviderSignInAttempt对象:

  1. 创建一个新的ConnectionData对象。 ConnectionData类具有单个构造函数,该构造函数将必填字段用作构造函数参数。
  2. 创建一个新的UserProfile对象。 我们可以使用UserProfileBuilder类创建新的UserProfile对象。
  3. 创建一个新的TestConnection对象,并将创建的ConnectionDataUserProfile对象作为构造函数参数传递。
  4. 创建一个新的TestProviderSignInAttempt对象,并将创建的TestConnectionConnection对象作为构造函数参数传递。

创建一个新的TestProviderSignInAttempt对象的源代码如下所示:

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

好消息是,我们现在知道如何在测试中创建TestProviderSignInAttempt对象。 坏消息是我们无法在测试中使用此代码。

我们必须记住,我们并不是为了确保我们的代码按预期工作而编写单元测试。 每个测试用例还应该揭示我们的代码在特定情况下的行为。 如果我们通过将此代码添加到每个测试用例中来创建TestProviderSignInAttempt ,那么我们将过于强调创建测试用例所需的对象。 这意味着很难理解测试用例,并且丢失了测试用例的“本质”。

相反,我们将创建一个测试数据构建器类,该类提供了用于创建TestProviderSignInAttempt对象的流利的API。 我们可以按照以下步骤创建此类:

  1. 创建一个名为TestProviderSignInAttemptBuilder的类。
  2. 将创建新的ConnectionDataUserProfile对象所需的所有字段添加到TestProviderSignInAttemptBuilder类。
  3. 添加用于设置所添加字段的字段值的方法。 通过执行以下步骤来实现每种方法:
    1. 将作为方法参数给出的值设置为正确的字段。
    2. 返回对TestProviderSignInAttemptBuilder对象的引用。
  4. connectionData()userProfile()方法添加到TestProviderSignInAttemptBuilder类。 这些方法仅返回对TestProviderSignInAttemptBuilder对象的引用,其目的是使我们的API更具可读性。
  5. build()方法添加到测试数据构建器类。 这将按照前面介绍的步骤创建TestProviderSignInAttempt对象,并返回创建的对象。

TestProviderSignInAttemptBuilder类的源代码如下所示:

package org.springframework.social.connect.support;import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.UserProfile;
import org.springframework.social.connect.UserProfileBuilder;
import org.springframework.social.connect.web.TestProviderSignInAttempt;public class TestProviderSignInAttemptBuilder {private String accessToken;private String displayName;private String email;private Long expireTime;private String firstName;private String imageUrl;private String lastName;private String profileUrl;private String providerId;private String providerUserId;private String refreshToken;private String secret;public TestProviderSignInAttemptBuilder() {}public TestProviderSignInAttemptBuilder accessToken(String accessToken) {this.accessToken = accessToken;return this;}public TestProviderSignInAttemptBuilder connectionData() {return this;}public TestProviderSignInAttemptBuilder displayName(String displayName) {this.displayName = displayName;return this;}public TestProviderSignInAttemptBuilder email(String email) {this.email = email;return this;}public TestProviderSignInAttemptBuilder expireTime(Long expireTime) {this.expireTime = expireTime;return this;}public TestProviderSignInAttemptBuilder firstName(String firstName) {this.firstName = firstName;return this;}public TestProviderSignInAttemptBuilder imageUrl(String imageUrl) {this.imageUrl = imageUrl;return this;}public TestProviderSignInAttemptBuilder lastName(String lastName) {this.lastName = lastName;return this;}public TestProviderSignInAttemptBuilder profileUrl(String profileUrl) {this.profileUrl = profileUrl;return this;}public TestProviderSignInAttemptBuilder providerId(String providerId) {this.providerId = providerId;return this;}public TestProviderSignInAttemptBuilder providerUserId(String providerUserId) {this.providerUserId = providerUserId;return this;}public TestProviderSignInAttemptBuilder refreshToken(String refreshToken) {this.refreshToken = refreshToken;return this;}public TestProviderSignInAttemptBuilder secret(String secret) {this.secret = secret;return this;}public TestProviderSignInAttemptBuilder userProfile() {return this;}public TestProviderSignInAttempt build() {ConnectionData connectionData = new ConnectionData(providerId,providerUserId,displayName,profileUrl,imageUrl,accessToken,secret,refreshToken,expireTime);UserProfile userProfile = new UserProfileBuilder().setEmail(email).setFirstName(firstName).setLastName(lastName).build();Connection connection = new TestConnection(connectionData, userProfile);return new TestProviderSignInAttempt(connection);}
}

注意:在为RegistrationController类编写单元测试时,不需要调用此构建器类的所有方法。 我添加这些字段的主要原因是,当我们为示例应用程序编写集成测试时,它们将非常有用。

现在,创建新的TestProviderSignInAttempt对象的代码更加整洁,可读性更好:

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

让我们继续前进,了解如何使用自定义FEST-Assert断言清理单元测试。

创建自定义断言

我们可以通过将标准JUnit断言替换为自定义FEST-Assert断言来清理单元测试。 我们必须创建以下三个自定义断言类:

  • 第一个断言类用于为ExampleUserDetails对象编写断言。 ExampleUserDetails类包含已登录用户的信息,该信息存储在应用程序的SecurityContext中。 换句话说,此类提供的断言用于验证登录用户的信息是否正确。
  • 第二个断言类用于为SecurityContext对象编写断言。 此类用于为其信息存储到SecurityContext的用户写断言。
  • 第三个断言类用于为TestProviderSignInAttempt对象编写断言。 此断言类用于验证是否通过使用TestProviderSignInAttempt对象创建了与SaaS API提供程序的连接。

注意:如果您不熟悉FEST-Assert,则应阅读我的博客文章,其中解释了如何使用FEST-Assert创建自定义断言 ,以及为什么要考虑这样做。

让我们继续。

创建ExampleUserDetailsAssert类

通过执行以下步骤,我们可以实现第一个自定义断言类:

  1. 创建一个ExampleUserDetailsAssert类,该类扩展了GenericAssert类。 提供以下类型参数:

    1. 第一个类型参数是自定义断言的类型。 将此类型参数的值设置为ExampleUserDetailsAssert
    2. 第二个类型参数是实际值对象的类型。 将此类型参数的值设置为ExampleUserDetails。
  2. 向创建的类添加一个私有构造函数。 此构造函数将ExampleUserDetails对象作为构造函数参数。 通过调用超类的构造函数并将以下对象作为构造函数参数传递来实现控制器:
    1. 第一个构造函数参数是一个Class对象,它指定自定义断言类的类型。 将此构造函数参数的值设置为ExampleUserDetailsAssert.class
    2. 第二个构造函数参数是实际值对象。 将作为构造函数参数给出的对象传递给超类的构造函数。
  3. 将静态assertThat()方法添加到创建的类。 此方法将ExampleUserDetails对象作为方法参数。 通过创建一个新的ExampleUserDetailsAssert对象来实现此方法。
  4. hasFirstName()方法添加到ExampleUserDetailsAssert类。 此方法将String对象作为方法参数,并返回ExampleUserDetailsAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的ExampleUserDetails对象不为null。
    2. 验证实际的名字是否等于作为方法参数给出的期望的名字。
    3. 返回对ExampleUserDetailsAssert对象的引用。
  5. 将一个hasId()方法添加到ExampleUserDetailsAssert类。 此方法将Long对象作为方法参数,并返回ExampleUserDetailsAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的ExampleUserDetails对象不为null。
    2. 验证实际ID是否等于作为方法参数给出的预期ID。
    3. 返回对ExampleUserDetailsAssert对象的引用。
  6. hasLastName()方法添加到ExampleUserDetailsAssert类。 此方法将String对象作为方法参数,并返回ExampleUserDetailsAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的ExampleUserDetails对象不为null。
    2. 验证实际的姓氏是否等于作为方法参数给出的期望的姓氏。
    3. 返回对ExampleUserDetailsAssert对象的引用。
  7. hasPassword()方法添加到ExampleUserDetailsAssert类。 此方法将String对象作为方法参数,并返回ExampleUserDetailsAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的ExampleUserDetails对象不为null。
    2. 验证实际密码是否等于作为方法参数给出的预期密码。
    3. 返回对ExampleUserDetailsAssert对象的引用。
  8. 将一个hasUsername()方法添加到ExampleUserDetailsAssert类。 此方法将String对象作为方法参数,并返回ExampleUserDetailsAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的ExampleUserDetails对象不为null。
    2. 验证实际用户名是否等于作为方法参数给出的预期用户名。
    3. 返回对ExampleUserDetailsAssert对象的引用。
  9. isActive()方法添加到ExampleUserDetailsAssert类。 此方法不带方法参数,它返回ExampleUserDetailsAssert对象。
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的ExampleUserDetails对象不为null。
    2. 验证其信息存储在ExampleUserDetails对象中的用户是否处于活动状态。
    3. 返回对ExampleUserDetailsAssert对象的引用。
  10. isRegisteredUser()方法添加到ExampleUserDetailsAssert类。 此方法不带方法参数,它返回ExampleUserDetailsAssert对象。
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的ExampleUserDetails对象不为null。
    2. 验证其信息存储在ExampleUserDetails对象中的用户是注册用户。
    3. 返回对ExampleUserDetailsAssert对象的引用。
  11. isRegisteredByUsingFormRegistration()方法添加到ExampleUserDetailsAssert类。 此方法返回ExampleUserDetailsAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的ExampleUserDetails对象不为null。
    2. 验证socialSignInProvider字段的值为空。
    3. 返回对ExampleUserDetailsAssert对象的引用。
  12. isSignedInByUsingSocialSignInProvider()方法添加到ExampleUserDetailsAssert类。 此方法将SocialMediaService枚举作为方法参数,并返回ExampleUserDetailsAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的ExampleUserDetails对象不为null。
    2. 验证socialSignInProvider的值等于作为方法参数给出的预期的SocialMediaService枚举。
    3. 返回对ExampleUserDetailsAssert对象的引用。

ExampleUserDetailsAssert类的源代码如下所示:

import org.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
import org.springframework.security.core.GrantedAuthority;import java.util.Collection;public class ExampleUserDetailsAssert extends GenericAssert<ExampleUserDetailsAssert, ExampleUserDetails> {private ExampleUserDetailsAssert(ExampleUserDetails actual) {super(ExampleUserDetailsAssert.class, actual);}public static ExampleUserDetailsAssert assertThat(ExampleUserDetails actual) {return new ExampleUserDetailsAssert(actual);}public ExampleUserDetailsAssert hasFirstName(String firstName) {isNotNull();String errorMessage = String.format("Expected first name to be <%s> but was <%s>",firstName,actual.getFirstName());Assertions.assertThat(actual.getFirstName()).overridingErrorMessage(errorMessage).isEqualTo(firstName);return this;}public ExampleUserDetailsAssert hasId(Long id) {isNotNull();String errorMessage = String.format("Expected id to be <%d> but was <%d>",id,actual.getId());Assertions.assertThat(actual.getId()).overridingErrorMessage(errorMessage).isEqualTo(id);return this;}public ExampleUserDetailsAssert hasLastName(String lastName) {isNotNull();String errorMessage = String.format("Expected last name to be <%s> but was <%s>",lastName,actual.getLastName());Assertions.assertThat(actual.getLastName()).overridingErrorMessage(errorMessage).isEqualTo(lastName);return this;}public ExampleUserDetailsAssert hasPassword(String password) {isNotNull();String errorMessage = String.format("Expected password to be <%s> but was <%s>",password,actual.getPassword());Assertions.assertThat(actual.getPassword()).overridingErrorMessage(errorMessage).isEqualTo(password);return this;}public ExampleUserDetailsAssert hasUsername(String username) {isNotNull();String errorMessage = String.format("Expected username to be <%s> but was <%s>",username,actual.getUsername());Assertions.assertThat(actual.getUsername()).overridingErrorMessage(errorMessage).isEqualTo(username);return this;}public ExampleUserDetailsAssert isActive() {isNotNull();String expirationErrorMessage = "Expected account to be non expired but it was expired";Assertions.assertThat(actual.isAccountNonExpired()).overridingErrorMessage(expirationErrorMessage).isTrue();String lockedErrorMessage = "Expected account to be non locked but it was locked";Assertions.assertThat(actual.isAccountNonLocked()).overridingErrorMessage(lockedErrorMessage).isTrue();String credentialsExpirationErrorMessage = "Expected credentials to be non expired but they were expired";Assertions.assertThat(actual.isCredentialsNonExpired()).overridingErrorMessage(credentialsExpirationErrorMessage).isTrue();String enabledErrorMessage = "Expected account to be enabled but it was not";Assertions.assertThat(actual.isEnabled()).overridingErrorMessage(enabledErrorMessage).isTrue();return this;}public ExampleUserDetailsAssert isRegisteredUser() {isNotNull();String errorMessage = String.format("Expected role to be <ROLE_USER> but was <%s>",actual.getRole());Assertions.assertThat(actual.getRole()).overridingErrorMessage(errorMessage).isEqualTo(Role.ROLE_USER);Collection<? extends GrantedAuthority> authorities = actual.getAuthorities();String authoritiesCountMessage = String.format("Expected <1> granted authority but found <%d>",authorities.size());Assertions.assertThat(authorities.size()).overridingErrorMessage(authoritiesCountMessage).isEqualTo(1);GrantedAuthority authority = authorities.iterator().next();String authorityErrorMessage = String.format("Expected authority to be <ROLE_USER> but was <%s>",authority.getAuthority());Assertions.assertThat(authority.getAuthority()).overridingErrorMessage(authorityErrorMessage).isEqualTo(Role.ROLE_USER.name());return this;}public ExampleUserDetailsAssert isRegisteredByUsingFormRegistration() {isNotNull();String errorMessage = String.format("Expected socialSignInProvider to be <null> but was <%s>",actual.getSocialSignInProvider());Assertions.assertThat(actual.getSocialSignInProvider()).overridingErrorMessage(errorMessage).isNull();return this;}public ExampleUserDetailsAssert isSignedInByUsingSocialSignInProvider(SocialMediaService socialSignInProvider) {isNotNull();String errorMessage = String.format("Expected socialSignInProvider to be <%s> but was <%s>",socialSignInProvider,actual.getSocialSignInProvider());Assertions.assertThat(actual.getSocialSignInProvider()).overridingErrorMessage(errorMessage).isEqualTo(socialSignInProvider);return this;}
}

创建SecurityContextAssert类

我们可以按照以下步骤创建第二个客户断言类:

  1. 创建一个SecurityContextAssert类,该类扩展了GenericAssert类。 提供以下类型参数:

    1. 第一个类型参数是自定义断言的类型。 将此类型参数的值设置为SecurityContextAssert
    2. 第二个类型参数是实际值对象的类型。 将此类型参数的值设置为SecurityContext
  2. 向创建的类添加一个私有构造函数。 该构造函数将SecurityContext对象作为构造函数参数。 通过调用超类的构造函数并将以下对象作为构造函数参数传递来实现控制器:
    1. 第一个构造函数参数是一个Class对象,它指定自定义断言类的类型。 将此构造函数参数的值设置为SecurityContextAssert.class
    2. 第二个构造函数参数是实际值对象。 将作为构造函数参数给出的对象传递给超类的构造函数。
  3. 将静态assertThat()方法添加到创建的类。 此方法将SecurityContext对象作为方法参数。 通过创建一个新的SecurityContextAssert对象来实现此方法。
  4. userIsAnonymous()方法添加到SecurityContextAssert类,并通过以下步骤实现它:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的SecurityContext对象不为null。
    2. SecurityContext获取Authentication对象,并确保它为null
    3. 返回对SecurityContextAssert对象的引用。
  5. 将一个loggingInUserIs()方法添加到SecurityContextAssert类。 此方法将User对象作为方法参数,并返回SecurityContextAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的SecurityContext对象不为null。
    2. SecurityContext获取ExampleUserDetails对象,并确保它不为null。
    3. 确保ExampleUserDetails对象的信息与User对象的信息相等。
    4. 返回对SecurityContextAssert对象的引用。
  6. 将一个loggingInUserHasPassword()方法添加到SecurityContextAssert类。 此方法将String对象作为方法参数,并返回SecurityContextAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的SecurityContext对象不为null。
    2. SecurityContext获取ExampleUserDetails对象,并确保它不为null。
    3. 确保ExampleUserDetails对象的密码字段等于作为方法参数给出的密码。
    4. 返回对SecurityContextAssert对象的引用。
  7. 将一个loggingInUserIsRegisteredByUsingNormalRegistration()方法添加到SecurityContextAssert类,并通过以下步骤实现它:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的SecurityContext对象不为null。
    2. SecurityContext获取ExampleUserDetails对象,并确保它不为null。
    3. 确保使用普通注册创建用户帐户。
    4. 返回对SecurityContextAssert对象的引用。
  8. 将一个loggingInUserIsSignedInByUsingSocialProvider()方法添加到SecurityContextAssert类。 此方法将SocialMediaService枚举作为方法参数,并返回SecurityContextAssert对象。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的SecurityContext对象不为null。
    2. SecurityContext获取ExampleUserDetails对象,并确保它不为null。
    3. 确保通过使用作为方法参数给出的SociaMediaService创建用户帐户。
    4. 返回对SecurityContextAssert对象的引用。

SecurityContextAssert类的源代码如下所示:

import org.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;public class SecurityContextAssert extends GenericAssert<SecurityContextAssert, SecurityContext> {private SecurityContextAssert(SecurityContext actual) {super(SecurityContextAssert.class, actual);}public static SecurityContextAssert assertThat(SecurityContext actual) {return new SecurityContextAssert(actual);}public SecurityContextAssert userIsAnonymous() {isNotNull();Authentication authentication = actual.getAuthentication();String errorMessage = String.format("Expected authentication to be <null> but was <%s>.", authentication);Assertions.assertThat(authentication).overridingErrorMessage(errorMessage).isNull();return this;}public SecurityContextAssert loggedInUserIs(User user) {isNotNull();ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();String errorMessage = String.format("Expected logged in user to be <%s> but was <null>", user);Assertions.assertThat(loggedIn).overridingErrorMessage(errorMessage).isNotNull();ExampleUserDetailsAssert.assertThat(loggedIn).hasFirstName(user.getFirstName()).hasId(user.getId()).hasLastName(user.getLastName()).hasUsername(user.getEmail()).isActive().isRegisteredUser();return this;}public SecurityContextAssert loggedInUserHasPassword(String password) {isNotNull();ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();String errorMessage = String.format("Expected logged in user to be <not null> but was <null>");Assertions.assertThat(loggedIn).overridingErrorMessage(errorMessage).isNotNull();ExampleUserDetailsAssert.assertThat(loggedIn).hasPassword(password);return this;}public SecurityContextAssert loggedInUserIsRegisteredByUsingNormalRegistration() {isNotNull();ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();String errorMessage = String.format("Expected logged in user to be <not null> but was <null>");Assertions.assertThat(loggedIn).overridingErrorMessage(errorMessage).isNotNull();ExampleUserDetailsAssert.assertThat(loggedIn).isRegisteredByUsingFormRegistration();return this;}public SecurityContextAssert loggedInUserIsSignedInByUsingSocialProvider(SocialMediaService signInProvider) {isNotNull();ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();String errorMessage = String.format("Expected logged in user to be <not null> but was <null>");Assertions.assertThat(loggedIn).overridingErrorMessage(errorMessage).isNotNull();ExampleUserDetailsAssert.assertThat(loggedIn).hasPassword("SocialUser").isSignedInByUsingSocialSignInProvider(signInProvider);return this;}
}

创建TestProviderSignInAttemptAssert类

我们可以按照以下步骤创建第三个自定义断言类:

  1. 创建一个TestProviderSignInAttemptAssert类,该类扩展了GenericAssert类。 提供以下类型参数:

    1. 第一个类型参数是自定义断言的类型。 将此类型参数的值设置为TestProviderSignInAttemptAssert
    2. 第二个类型参数是实际值对象的类型。 将此类型参数的值设置为TestProviderSignInAttempt
  2. 向创建的类添加一个私有构造函数。 此构造函数将TestProviderSignInAttempt对象作为构造函数参数。 通过调用超类的构造函数并将以下对象作为构造函数参数传递来实现控制器:
    1. 第一个构造函数参数是一个Class对象,它指定自定义断言类的类型。 将此构造函数参数的值设置为TestProviderSignInAttemptAssert.class
    2. 第二个构造函数参数是实际值对象。 将作为构造函数参数给出的对象传递给超类的构造函数。
  3. 将静态assertThatSignIn()方法添加到创建的类。 此方法将TestProviderSignInAttempt对象作为方法参数。 通过创建一个新的TestProviderSignInAttemptAssert对象来实现此方法。
  4. 向创建的类添加一个createdNoConnections()方法。 此方法不带方法参数,并且返回对TestProviderSignInAttemptAssert对象的引用。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的TestProviderSignInAttempt对象不为null。
    2. 确保实际的TestProviderSignInAttempt对象未创建任何连接。
    3. 返回对TestProviderSignInAttemptAssert对象的引用。
  5. 向创建的类中添加一个createdConnectionForUserId()方法。 此方法将String对象作为方法参数,并返回对TestProviderSignInAttempt对象的引用。 我们可以通过执行以下步骤来实现此方法:
    1. 通过调用GenericAssert类的isNotNull()方法,确保实际的TestProviderSignInAttempt对象不为null。
    2. 确保为用户ID为方法参数的用户创建了连接。
    3. 返回对TestProviderSignInAttemptAssert对象的引用。

TestProviderSignInAttemptAssert类的源代码如下所示:

import org.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
import org.springframework.social.connect.web.TestProviderSignInAttempt;public class TestProviderSignInAttemptAssert extends GenericAssert<TestProviderSignInAttemptAssert, TestProviderSignInAttempt> {private TestProviderSignInAttemptAssert(TestProviderSignInAttempt actual) {super(TestProviderSignInAttemptAssert.class, actual);}public static TestProviderSignInAttemptAssert assertThatSignIn(TestProviderSignInAttempt actual) {return new TestProviderSignInAttemptAssert(actual);}public TestProviderSignInAttemptAssert createdNoConnections() {isNotNull();String error = String.format("Expected that no connections were created but found <%d> connection",actual.getConnections().size());Assertions.assertThat(actual.getConnections()).overridingErrorMessage(error).isEmpty();return this;}public TestProviderSignInAttemptAssert createdConnectionForUserId(String userId) {isNotNull();String error = String.format("Expected that connection was created for user id <%s> but found none.",userId);Assertions.assertThat(actual.getConnections()).overridingErrorMessage(error).contains(userId);return this;}
}

让我们继续并开始为RegistrationController类编写一些单元测试。

写作单元测试

现在,我们已经完成准备工作,并准备为注册功能编写单元测试。 我们必须为以下控制器方法编写单元测试:

  • 第一种控制器方法呈现注册页面。
  • 第二种控制器方法处理注册表格的提交。

在开始编写单元测试之前,我们必须对其进行配置。 让我们找出这是如何完成的。

注意:我们的单元测试使用Spring MVC测试框架。 如果您不熟悉它,建议您看一下我的Spring MVC Test教程 。

配置我们的单元测试

我们的示例应用程序的应用程序上下文配置以易于编写Web层的单元测试的方式进行设计。 这些设计原理如下所述:

  • 应用程序上下文配置分为几个配置类,每个类都配置了应用程序的特定部分(Web,安全性,社交性和持久性)。
  • 我们的应用程序上下文配置有一个“主”配置类,该类配置一些“通用” bean并导入其他配置类。 该配置类还为服务层配置组件扫描。

当我们遵循这些原则配置应用程序上下文时,很容易为我们的单元测试创​​建应用程序上下文配置。 我们可以通过重用配置示例应用程序的Web层的应用程序上下文配置类并为单元测试创​​建一个新的应用程序上下文配置类来做到这一点。

通过执行以下步骤,我们可以为单元测试创​​建应用程序上下文配置类:

  1. 创建一个名为UnitTestContext的类。
  2. @Configuration注释对创建的类进行注释。
  3. 向创建的类中添加messageSource()方法,并使用@Bean注释对方法进行注释。 通过执行以下步骤配置MessageSource bean:
    1. 创建一个新的ResourceBundleMessageSource对象。
    2. 设置消息文件的基本名称,并确保如果未找到消息,则返回其代码。
    3. 返回创建的对象。
  4. userService()方法添加到创建的类中,并使用@Bean注释对该方法进行注释。 通过执行以下步骤配置UserService模拟对象:
    1. 调用Mockito类的静态嘲笑()方法,并将UserService.class作为方法参数传递。
    2. 返回创建的对象。

UnitTestContext类的源代码如下所示:

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;import static org.mockito.Mockito.mock;@Configuration
public class UnitTestContext {@Beanpublic MessageSource messageSource() {ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();messageSource.setBasename("i18n/messages");messageSource.setUseCodeAsDefaultMessage(true);return messageSource;}@Beanpublic UserService userService() {return mock(UserService.class);}
}

接下来要做的是配置单元测试。 我们可以按照以下步骤进行操作:

  1. 使用@RunWith注释对测试类进行注释,并确保通过使用SpringUnit4ClassRunner执行我们的测试。
  2. 使用@ContextConfiguration批注对类进行批注,并确保使用正确的配置类。 在我们的例子中,正确的配置类是: WebAppContextUnitTestContext
  3. @WebAppConfiguration批注对类进行批注。 此批注确保加载的应用程序上下文是WebApplicationContext
  4. MockMvc字段添加到测试类。
  5. WebApplicationContext字段添加到类中,并使用@Autowired批注对其进行批注。
  6. UserService字段添加到测试类,并使用@Autowired批注对其进行批注。
  7. setUp()方法添加到测试类,并使用@Before注释对方法进行注释。 这样可以确保在每个测试方法之前调用该方法。 通过执行以下步骤来实现此方法:
    1. 通过调用Mockito类的静态reset()方法并将经过重置的模拟作为方法参数传递来重置UserService模拟。
    2. 通过使用MockMvcBuilders类创建一个新的MockMvc对象。
    3. 运行我们的测试时,请确保从SecurityContext中没有找到Authentication对象。 我们可以按照以下步骤进行操作:
      1. 通过调用SecurityContextHolder类的静态getContext()方法来获取对SecurityContext对象的引用。
      2. 通过调用SecurityContext类的setAuthentication()方法清除身份验证。 将null作为方法参数传递。

我们的单元测试类的源代码如下所示:

import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest2 {private MockMvc mockMvc;@Autowiredprivate WebApplicationContext webAppContext;@Autowiredprivate UserService userServiceMock;@Beforepublic void setUp() {Mockito.reset(userServiceMock);mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();SecurityContextHolder.getContext().setAuthentication(null);}
}

注意:如果要获得有关使用Spring MVC Test框架的单元测试的配置的更多信息,建议您阅读此博客文章 。

让我们继续并为呈现注册表格的控制器方法编写单元测试。

提交注册表

呈现注册表的控制器方法具有一项重要职责:

如果用户正在使用社交登录,则使用由使用过的SaaS API提供程序提供的使用信息来预填充注册字段。

让我们刷新内存,看一下RegistrationController类的源代码:

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionKey;
import org.springframework.social.connect.UserProfile;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.request.WebRequest;@Controller
@SessionAttributes("user")
public class RegistrationController {@RequestMapping(value = "/user/register", method = RequestMethod.GET)public String showRegistrationForm(WebRequest request, Model model) {Connection<?> connection = ProviderSignInUtils.getConnection(request);RegistrationForm registration = createRegistrationDTO(connection);model.addAttribute("user", registration);return "user/registrationForm";}private RegistrationForm createRegistrationDTO(Connection<?> connection) {RegistrationForm dto = new RegistrationForm();if (connection != null) {UserProfile socialMediaProfile = connection.fetchUserProfile();dto.setEmail(socialMediaProfile.getEmail());dto.setFirstName(socialMediaProfile.getFirstName());dto.setLastName(socialMediaProfile.getLastName());ConnectionKey providerKey = connection.getKey();dto.setSignInProvider(SocialMediaService.valueOf(providerKey.getProviderId().toUpperCase()));}return dto;}
}

显然,我们必须为此控制器方法编写两个单元测试:

  1. 我们必须编写一个测试,以确保当用户使用“常规”注册时,控制器方法能够正常工作。
  2. 我们必须编写一个测试,以确保当用户使用社交登录时,控制器方法能够正常工作。

让我们移动并编写这些单元测试。

测试1:提​​交普通注册表

我们可以按照以下步骤编写第一个单元测试:

  1. 执行GET请求以发送url'/ user / register'。
  2. 确保返回HTTP状态代码200。
  3. 验证渲染视图的名称为“ user / registrationForm”。
  4. Verify that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  5. Ensure that all fields of the model attribute called 'user' are either null or empty.
  6. Verify that no methods of the UserService mock were called.

我们的单元测试的源代码如下所示:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {private MockMvc mockMvc;@Autowiredprivate WebApplicationContext webAppContext;@Autowiredprivate UserService userServiceMock;//The setUp() method is omitted for the sake of clarity@Testpublic void showRegistrationForm_NormalRegistration_ShouldRenderRegistrationPageWithEmptyForm() throws Exception {mockMvc.perform(get("/user/register")).andExpect(status().isOk()).andExpect(view().name("user/registrationForm")).andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp")).andExpect(model().attribute("user", allOf(hasProperty("email", isEmptyOrNullString()),hasProperty("firstName", isEmptyOrNullString()),hasProperty("lastName", isEmptyOrNullString()),hasProperty("password", isEmptyOrNullString()),hasProperty("passwordVerification", isEmptyOrNullString()),hasProperty("signInProvider", isEmptyOrNullString()))));verifyZeroInteractions(userServiceMock);}
}

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

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

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder class. Set the provider id, first name, last name and email address.
  2. Execute a GET request to url '/user/register' and set the created TestProviderSignInAttempt object to the HTTP session.
  3. 确保返回HTTP状态代码200。
  4. Verify that the name of the rendered view is 'user/registrationForm'.
  5. Ensure that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  6. Verify that the fields of the model object called 'user' are pre-populated by using the information contained by the TestProviderSignInAttempt object. 我们可以按照以下步骤进行操作:
    1. Ensure that the value of the email field is 'john.smith@gmail.com'.
    2. Ensure that the value of the firstName field is 'John'.
    3. Ensure that the value of the lastName field is 'Smith'.
    4. Ensure that the value of the password field is empty or null String.
    5. Ensure that the value of the passwordVerification field is empty or null String.
    6. Ensure that the value of the signInProvider field is 'twitter'.
  7. Verify that the methods of the UserService interface were not called.

我们的单元测试的源代码如下所示:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {private MockMvc mockMvc;@Autowiredprivate WebApplicationContext webAppContext;@Autowiredprivate UserService userServiceMock;//The setUp() method is omitted for the sake of clarity@Testpublic void showRegistrationForm_SocialSignInWithAllValues_ShouldRenderRegistrationPageWithAllValuesSet() throws Exception {TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder().connectionData().providerId("twitter").userProfile().email("john.smith@gmail.com").firstName("John").lastName("Smith").build();mockMvc.perform(get("/user/register").sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)).andExpect(status().isOk()).andExpect(view().name("user/registrationForm")).andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp")).andExpect(model().attribute("user", allOf(hasProperty("email", is("john.smith@gmail.com")),hasProperty("firstName", is("John")),hasProperty("lastName", is("Smith")),hasProperty("password", isEmptyOrNullString()),hasProperty("passwordVerification", isEmptyOrNullString()),hasProperty("signInProvider", is("twitter")))));verifyZeroInteractions(userServiceMock);}
}

Submitting The Registration Form

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

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

The relevant part of the RegistrationController class looks as follows:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.request.WebRequest;import javax.validation.Valid;@Controller
@SessionAttributes("user")
public class RegistrationController {private UserService service;@Autowiredpublic RegistrationController(UserService service) {this.service = service;}@RequestMapping(value ="/user/register", method = RequestMethod.POST)public String registerUserAccount(@Valid @ModelAttribute("user") RegistrationForm userAccountData,BindingResult result,WebRequest request) throws DuplicateEmailException {if (result.hasErrors()) {return "user/registrationForm";}User registered = createUserAccount(userAccountData, result);if (registered == null) {return "user/registrationForm";}SecurityUtil.logInUser(registered);ProviderSignInUtils.handlePostSignUp(registered.getEmail(), request);return "redirect:/";}private User createUserAccount(RegistrationForm userAccountData, BindingResult result) {User registered = null;try {registered = service.registerNewUserAccount(userAccountData);}catch (DuplicateEmailException ex) {addFieldError("user","email",userAccountData.getEmail(),"NotExist.user.email",result);}return registered;}private void addFieldError(String objectName, String fieldName, String fieldValue,  String errorCode, BindingResult result) {FieldError error = new FieldError(objectName,fieldName,fieldValue,false,new String[]{errorCode},new Object[]{},errorCode);result.addError(error);}
}

We will write three unit tests for this controller method:

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

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

测试1:验证失败

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

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder class. Set the provider id, first name, last name and email address.
  2. Create a new RegistrationForm object by using the RegistrationFormBuilder class. Set the value of the signInProvider field.
  3. Execute a POST request to url '/user/register' by following these steps:
    1. 将请求的内容类型设置为“ application / x-www-form-urlencoded”。
    2. Convert the form object into url encoded bytes and set the outcome of the conversion into the body of the request.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set the form object to the HTTP session.
  4. 验证是否返回HTTP状态代码200。
  5. 确保渲染视图的名称为“ user / registrationForm”。
  6. Ensure that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  7. Verify that field values of the model object called 'user' are correct by following these steps:
    1. Verify that the value of the email field is empty or null String.
    2. Verify that the value of the firstName field is empty or null String.
    3. Verify that the value of the lastName field is empty or null String.
    4. Verify that the value of the password field is empty or null String.
    5. Verify that the value of the passwordVerification field is empty or null String.
    6. Verify that the value of the signInProvider field is 'twitter'.
  8. Ensure that the model attribute called 'user' has field errors in email , firstName , and lastName fields.
  9. Verify that the current user is not logged in.
  10. Ensure that no connections were created by using the TestProviderSignInAttempt object.
  11. Verify that the methods of the UserService mock were not called.

我们的单元测试的源代码如下所示:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {private MockMvc mockMvc;@Autowiredprivate WebApplicationContext webAppContext;@Autowiredprivate UserService userServiceMock;//The setUp() method is omitted for the sake of clarity@Testpublic void registerUserAccount_SocialSignInAndEmptyForm_ShouldRenderRegistrationFormWithValidationErrors() throws Exception {TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder().connectionData().providerId("twitter").userProfile().email("john.smith@gmail.com").firstName("John").lastName("Smith").build();RegistrationForm userAccountData = new RegistrationFormBuilder().signInProvider(SocialMediaService.TWITTER).build();mockMvc.perform(post("/user/register").contentType(MediaType.APPLICATION_FORM_URLENCODED).content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData)).sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn).sessionAttr("user", userAccountData)).andExpect(status().isOk()).andExpect(view().name("user/registrationForm")).andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp")).andExpect(model().attribute("user", allOf(hasProperty("email", isEmptyOrNullString()),hasProperty("firstName", isEmptyOrNullString()),hasProperty("lastName", isEmptyOrNullString()),hasProperty("password", isEmptyOrNullString()),hasProperty("passwordVerification", isEmptyOrNullString()),hasProperty("signInProvider", is(SocialMediaService.TWITTER))))).andExpect(model().attributeHasFieldErrors("user", "email", "firstName", "lastName"));assertThat(SecurityContextHolder.getContext()).userIsAnonymous();assertThatSignIn(socialSignIn).createdNoConnections();verifyZeroInteractions(userServiceMock);}
}

测试2:从数据库中找到电子邮件地址

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

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder class. Set the provider id, first name, last name and email address.
  2. Create a new RegistrationForm object by using the RegistrationFormBuilder class. Set the values of email , firstName , lastName , and signInProvider fields.
  3. Configure the UserService mock to throw a DuplicateEmailException when its registerNewUserAccount() method is called and the form object is given as a method parameter.
  4. Execute a POST request to url '/user/register' by following these steps:
    1. 将请求的内容类型设置为“ application / x-www-form-urlencoded”。
    2. Convert the form object into url encoded bytes and set the outcome of the conversion into the body of the request.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set the form object to the HTTP session.
  5. 验证是否返回HTTP状态代码200。
  6. 确保渲染视图的名称为“ user / registrationForm”。
  7. Ensure that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  8. Verify that field values of the model object called 'user' are correct by following these steps:
    1. Ensure that the value of the email field is 'john.smith@gmail.com'.
    2. Ensure that the value of the firstName field is 'John'.
    3. Ensure that the value of the lastName field is 'Smith'.
    4. Ensure that the value of the password field is empty or null String.
    5. Ensure that the value of the passwordVerification field is empty or null String.
    6. Ensure that the value of the signInProvider field is 'twitter'.
  9. Ensure that the model attribute called 'user' has field error in email field.
  10. Verify that the current user is not logged in.
  11. Ensure that no connections were created by using the TestProviderSignInAttempt object.
  12. Verify that the registerNewUserAccount() method of the UserService mock was called once and that the RegistrationForm object was given as a method parameter.
  13. Verify that the other methods of the UserService interface weren't invoked during the test.

我们的单元测试的源代码如下所示:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {private MockMvc mockMvc;@Autowiredprivate WebApplicationContext webAppContext;@Autowiredprivate UserService userServiceMock;//The setUp() method is omitted for the sake of clarity.@Testpublic void registerUserAccount_SocialSignInAndEmailExist_ShouldRenderRegistrationFormWithFieldError() throws Exception {TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder().connectionData().providerId("twitter").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();when(userServiceMock.registerNewUserAccount(userAccountData)).thenThrow(new DuplicateEmailException(""));mockMvc.perform(post("/user/register").contentType(MediaType.APPLICATION_FORM_URLENCODED).content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData)).sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn).sessionAttr("user", userAccountData)).andExpect(status().isOk()).andExpect(view().name("user/registrationForm")).andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp")).andExpect(model().attribute("user", allOf(hasProperty("email", is("john.smith@gmail.com")),hasProperty("firstName", is("John")),hasProperty("lastName", is("Smith")),hasProperty("password", isEmptyOrNullString()),hasProperty("passwordVerification", isEmptyOrNullString()),hasProperty("signInProvider", is(SocialMediaService.TWITTER))))).andExpect(model().attributeHasFieldErrors("user", "email"));assertThat(SecurityContextHolder.getContext()).userIsAnonymous();assertThatSignIn(socialSignIn).createdNoConnections();verify(userServiceMock, times(1)).registerNewUserAccount(userAccountData);verifyNoMoreInteractions(userServiceMock);}
}

测试3:注册成功

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

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder class. Set the provider id, first name, last name and email address.
  2. Create a new RegistrationForm object by using the RegistrationFormBuilder class. Set the values of email , firstName , lastName , and signInProvider fields.
  3. Create a new User object by using the UserBuilder class. Set the values of id , email , firstName , lastName , and signInProvider fields.
  4. Configure the UserService mock object to return the created User object when its registerNewUserAccount() method is called and the RegistrationForm object is given as a method parameter.
  5. Execute a POST request to url '/user/register' by following these steps:
    1. 将请求的内容类型设置为“ application / x-www-form-urlencoded”。
    2. Convert the form object into url encoded bytes and set the outcome of the conversion into the body of the request.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set the form object to the HTTP session.
  6. 验证是否返回了HTTP状态代码302。
  7. Ensure that the request is redirected to url '/'.
  8. Verify that the created user is logged in by using Twitter.
  9. Verify that the TestProviderSignInAttempt object was used to created a connection for a user with email address 'john.smith@gmail.com'.
  10. Verify that the registerNewUserAccount() method of the UserService mock was called once and that the form object was given as a method parameter.
  11. Verify that the other methods of the UserService mock weren't invoked during the test.

我们的单元测试的源代码如下所示:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {private MockMvc mockMvc;@Autowiredprivate WebApplicationContext webAppContext;@Autowiredprivate UserService userServiceMock;//The setUp() method is omitted for the sake of clarity.@Testpublic void registerUserAccount_SocialSignIn_ShouldCreateNewUserAccountAndRenderHomePage() throws Exception {TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder().connectionData().providerId("twitter").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();User registered = new UserBuilder().id(1L).email("john.smith@gmail.com").firstName("John").lastName("Smith").signInProvider(SocialMediaService.TWITTER).build();when(userServiceMock.registerNewUserAccount(userAccountData)).thenReturn(registered);mockMvc.perform(post("/user/register").contentType(MediaType.APPLICATION_FORM_URLENCODED).content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData)).sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn).sessionAttr("user", userAccountData)).andExpect(status().isMovedTemporarily()).andExpect(redirectedUrl("/"));assertThat(SecurityContextHolder.getContext()).loggedInUserIs(registered).loggedInUserIsSignedInByUsingSocialProvider(SocialMediaService.TWITTER);assertThatSignIn(socialSignIn).createdConnectionForUserId("john.smith@gmail.com");verify(userServiceMock, times(1)).registerNewUserAccount(userAccountData);verifyNoMoreInteractions(userServiceMock);}
}

摘要

We have now written some unit tests for the registration function of our example application. 这篇博客文章教会了我们四件事:

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

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

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

Reference: Adding Social Sign In to a Spring MVC Web Application: Unit Testing from our JCG partner Petri Kainulainen at the Petri Kainulainen blog.

翻译自: https://www.javacodegeeks.com/2013/12/adding-social-sign-in-to-a-spring-mvc-web-application-unit-testing.html

在Spring MVC Web应用程序中添加社交登录:单元测试相关推荐

  1. 在Spring MVC Web应用程序中添加社交登录:集成测试

    我已经写了关于为使用Spring Social 1.1.0的应用程序编写单元测试的挑战,并为此提供了一种解决方案 . 尽管单元测试很有价值,但是它并不能真正告诉我们我们的应用程序是否正常运行. 这就是 ...

  2. 如何使用recaptcha_在Spring MVC Web应用程序中使用reCaptcha

    如何使用recaptcha CAPTCHA是一个程序,可以生成人类可以通过但计算机程序" 不能 "通过的测试并对其进行评分. 所采取的策略之一是向用户显示具有扭曲文本的图像,并且用 ...

  3. 在Spring MVC Web应用程序中使用reCaptcha

    CAPTCHA是一种程序,可以生成人类可以通过的测试并对其进行评分,而计算机程序" 不能 "通过. 所采取的策略之一是向用户显示具有扭曲文本的图像,并且用户应在输入区域中书写文本. ...

  4. 将社交登录添加到Spring MVC Web应用程序:注册和登录

    本教程的第一部分描述了如何配置Spring Social 1.1.0和Spring Security 3.2.0,但它留下了两个非常重要的问题尚未解答. 这些问题是: 用户如何创建新用户帐户? 用户如 ...

  5. 集成spring mvc_向Spring MVC Web应用程序添加社交登录:集成测试

    集成spring mvc 我已经写了关于为使用Spring Social 1.1.0的应用程序编写单元测试的挑战,并为此提供了一种解决方案 . 尽管单元测试很有价值,但它并不能真正告诉我们我们的应用程 ...

  6. 将社交登录添加到Spring MVC Web应用程序:配置

    过去,用户使用用户名和密码组合登录. 尽管如今有些人仍然偏爱传统方式,但越来越多的用户希望使用其社交媒体帐户登录. 这就是使Spring Social(及其子项目)成为Spring项目组合有用的补充的 ...

  7. spring基于注释的配置_基于注释的Spring MVC Web应用程序入门

    spring基于注释的配置 这是使Maven启动Spring 3 MVC项目的最小方法. 首先创建spring-web-annotation/pom.xml文件,并包含Spring依赖项: <? ...

  8. gradle spring_使用Gradle的简单Spring MVC Web应用程序

    gradle spring 除了我们现在将使用Spring MVC而不是原始servlet之外,该文章将与我们之前的文章Simple Gradle Web Application相似. 使用Gradl ...

  9. 基于注释的Spring MVC Web应用程序入门

    这是使Maven启动Spring 3 MVC项目的最小方法. 首先创建spring-web-annotation/pom.xml文件,并包含Spring依赖项: <?xml version=&q ...

最新文章

  1. 波士顿房价预测学习项目笔记
  2. python的try和except_python的try...except
  3. Weka加载大数据量内存不足的解决办法
  4. 在save中重写 AdminModel 方法 和 Signals
  5. jQuery和Vue的区别(转载)
  6. CV中的经典网络模型
  7. python报“IndentationError: unexpected indent“的解决方法
  8. correlation 蒙特卡洛_蒙特卡洛模拟法
  9. 类似铸剑物语的java游戏_怀旧向:GBA上的10款经典RPG游戏推荐,这些你都玩过吗?...
  10. 360n4刷android8.1,【从此,任何人都可以刷N4】关于系统降级===线刷方法,我也说两句吧。...
  11. 入门OJ 3172【导游】
  12. 一分钟内搞定!熊猫杀毒软件研发提升扫描速度的技术
  13. 北京大学数学科学学院2006\9\20声明:坚持真理、追求卓越zz
  14. 性能之巅——洞悉系统、企业与云计算 Brendan Gregg
  15. Prometheus监控Redis的配置
  16. 毕设第三周(12月19日——12月25日)
  17. 编程命名规范—四种命名方法
  18. K8S 快速入门(十六)实战篇:StorageClass(存储类)
  19. 和 Cee 聊聊如何拿 Google Offer
  20. 字节青训营抖音APP

热门文章

  1. idea左右切换页面 返回上一次鼠标位置
  2. mysql循环insert多条数据
  3. ajax调用后台java类_ajax调用java后台方法是什么
  4. derby数据库的数据_Derby数据库备份
  5. spring 定时器注释_带注释的控制器– Spring Web / Webflux和测试
  6. datastore_使用Spring Session和JDBC DataStore进行会话管理
  7. vertx rest 跨域_在基于简单Vertx Rest的应用程序上为REST资源设置基本响应HTTP标头...
  8. HibernateNONSTRICT_READ_WRITE CacheConcurrencyStrategy如何工作
  9. apache fop_Apache FOP与Eclipse和OSGi的集成
  10. 在Kafka上异步发送数据