6

Spring 对集成测试的支持和单元的最佳实践测试(二)

 1 year ago
source link: https://blog.51cto.com/u_15326439/5860740
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Spring 对集成测试的支持和单元的最佳实践测试(二)

精选 原创
Spring 对集成测试的支持和单元的最佳实践测试(二)_spring

3.5.7. 测试治具的依赖注入

当您使用(由 默认),测试实例的依赖项是从 您配置的应用程序上下文或相关 附注。您可以使用二传手注入、现场注入或两者,具体取决于 选择哪些批注以及是否将它们放在 setter 方法或字段中。 如果您使用的是JUnit Jupiter,您还可以选择使用构造函数注入 (参见SpringExtension 的依赖注入)。为了与 Spring 基于注释的一致性 注入支持,您也可以使用 JSR-330 中的 Spring'sannotation 或 theannotation 进行字段和二传手注入。​​DependencyInjectionTestExecutionListener​​​​@ContextConfiguration​​​​@Autowired​​​​@Inject​

因为用于执行自动接线 类型​,如果您有同一类型的多个 Bean 定义,则不能依赖此 那些特定豆子的方法。在这种情况下,您可以使用 结合。您也可以选择与一起使用。或者,如果您的测试类可以访问其,则您 可以使用(例如)调用 to 来执行显式查找。​​@Autowired​​​​@Autowired​​​​@Qualifier​​​​@Inject​​​​@Named​​​​ApplicationContext​​​​applicationContext.getBean("titleRepository", TitleRepository.class)​

如果您不希望将依赖关系注入应用于测试实例,请不要批注 字段或 setter 方法与 withor。或者,您可以禁用 通过显式配置类和省略侦听器列表来完全依赖注入。​​@Autowired​​​​@Inject​​​​@TestExecutionListeners​​​​DependencyInjectionTestExecutionListener.class​

考虑测试 aclass 的方案,如目标部分所述。接下来的两个代码清单演示了 使用Ofon字段和二传手方法。应用程序上下文配置 显示在所有示例代码清单之后。​​HibernateTitleRepository​​​​@Autowired​

第一个代码清单显示了基于 JUnit Jupiter 的测试类实现,该 场注入用途:​​@Autowired​

@ExtendWith(SpringExtension.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {

// this instance will be dependency injected by type
@Autowired
HibernateTitleRepository titleRepository;

@Test
void findById() {
Title title = titleRepository.findById(new Long(10));
assertNotNull(title);
}
}

或者,您可以将类配置为用于二传手注入,如 遵循:​​@Autowired​

@ExtendWith(SpringExtension.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {

// this instance will be dependency injected by type
HibernateTitleRepository titleRepository;

@Autowired
void setTitleRepository(HibernateTitleRepository titleRepository) {
this.titleRepository = titleRepository;
}

@Test
void findById() {
Title title = titleRepository.findById(new Long(10));
assertNotNull(title);
}
}

前面的代码清单使用由注释引用的同一 XML 上下文文件(即)。以下 显示此配置:​​@ContextConfiguration​​​​repository-config.xml​

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- this bean will be injected into the HibernateTitleRepositoryTests class -->
<bean id="titleRepository" class="com.foo.repository.hibernate.HibernateTitleRepository">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<!-- configuration elided for brevity -->
</bean>

</beans>

3.5.8. 测试请求和会话范围的 Bean

Spring 支持请求和会话范围 从早年开始的 bean,您可以测试请求范围和会话范围 豆子,请按照以下步骤操作:

  • 通过注释测试来确保为您的测试加载 ais 类与。WebApplicationContext@WebAppConfiguration
  • 将模拟请求或会话注入测试实例并准备测试 适当的夹具。
  • 调用从配置(使用依赖项注入)检索的 Web 组件。WebApplicationContext
  • 对模拟执行断言。

下一个代码片段显示了登录用例的 XML 配置。请注意,thebean 依赖于请求范围的 bean。此外,通过使用SpEL 表达式实例化 从当前 HTTP 请求中检索用户名和密码。在我们的测试中,我们希望 通过测试上下文框架管理的模拟来配置这些请求参数。 以下清单显示了此用例的配置:​​userService​​​​loginAction​​​​LoginAction​

请求范围的 Bean 配置

<beans>

<bean id="userService" class="com.example.SimpleUserService"
c:loginAction-ref="loginAction"/>

<bean id="loginAction" class="com.example.LoginAction"
c:username="#{request.getParameter('user')}"
c:password="#{request.getParameter('pswd')}"
scope="request">
<aop:scoped-proxy/>
</bean>

</beans>

在,我们注入两个(即,主题下 测试)并进入我们的测试实例。在我们的测试方法中,我们通过在 提供的。当在我们的方法上调用该方法时,我们可以确保用户服务可以访问当前请求范围(即我们只是 设置参数)。然后,我们可以根据已知的结果对结果执行断言 用户名和密码的输入。以下清单显示了如何执行此操作:​​RequestScopedBeanTests​​​​UserService​​​​MockHttpServletRequest​​​​requestScope()​​​​MockHttpServletRequest​​​​loginUser()​​​​userService​​​​loginAction​​​​MockHttpServletRequest​

@SpringJUnitWebConfig
class RequestScopedBeanTests {

@Autowired UserService userService;
@Autowired MockHttpServletRequest request;

@Test
void requestScope() {
request.setParameter("user", "enigma");
request.setParameter("pswd", "$pr!ng");

LoginResults results = userService.loginUser();
// assert results
}
}

以下代码片段类似于我们之前看到的请求范围的代码片段 豆。但是,这一次,thebean 依赖于会话范围的 bean。请注意,该 bean 是通过使用 从当前 HTTP 会话检索主题的 SpEL 表达式。在我们的测试中,我们 需要在测试上下文框架管理的模拟会话中配置主题。这 以下示例演示如何执行此操作:​​userService​​​​userPreferences​​​​UserPreferences​

会话范围的 Bean 配置

<beans>

<bean id="userService" class="com.example.SimpleUserService"
c:userPreferences-ref="userPreferences" />

<bean id="userPreferences" class="com.example.UserPreferences"
c:theme="#{session.getAttribute('theme')}"
scope="session">
<aop:scoped-proxy/>
</bean>

</beans>

在,我们注入和进入 我们的测试实例。在我们的测试方法中,我们通过以下方式设置我们的测试夹具 在提供的中设置预期属性。当在我们的方法上调用该方法时,我们确信 用户服务有权访问当前会话范围的会话范围,我们可以根据 配置的主题。以下示例演示如何执行此操作:​​SessionScopedBeanTests​​​​UserService​​​​MockHttpSession​​​​sessionScope()​​​​theme​​​​MockHttpSession​​​​processUserPreferences()​​​​userService​​​​userPreferences​​​​MockHttpSession​

@SpringJUnitWebConfig
class SessionScopedBeanTests {

@Autowired UserService userService;
@Autowired MockHttpSession session;

@Test
void sessionScope() throws Exception {
session.setAttribute("theme", "blue");

Results results = userService.processUserPreferences();
// assert results
}
}

3.5.9. 事务管理

在 TestContext 框架中,事务由 管理,默认情况下配置,即使您不这样做 显式声明测试类。启用对 事务,但是,您必须在加载语义(进一步 详细信息将在后面提供)。此外,您必须在测试的类或方法级别声明 Spring'sannotation。​​TransactionalTestExecutionListener​​​​@TestExecutionListeners​​​​PlatformTransactionManager​​​​ApplicationContext​​​​@ContextConfiguration​​​​@Transactional​

测试管理的事务

测试管理的事务是通过使用 theor 以编程方式以声明方式管理的事务(稍后介绍)。您不应将此类事务与 Spring 托管的事务混淆 事务(由 Spring 在加载的事务中直接管理的交易 测试)或应用程序管理的事务(以编程方式管理的事务 由测试调用的应用程序代码)。弹簧管理和应用程序管理 事务通常参与测试管理的事务。但是,您应该使用 如果 Spring 管理的事务或应用程序管理的事务配置了任何 传播类型(详见事务传播讨论)。​​TransactionalTestExecutionListener​​​​TestTransaction​​​​ApplicationContext​​​​REQUIRED​​​​SUPPORTS​

启用和禁用事务

注释测试方法会导致测试在 默认情况下,事务在测试完成后自动回滚。 如果用注释了测试类,则该类中的每个测试方法都带有注释 层次结构在事务中运行。未注释的测试方法(在类或方法级别)不会在事务中运行。注意 测试生命周期方法(例如,方法)不支持 用朱庇特的注释等。此外,测试该 被注释但具有属性集太罕见不在事务中运行。​​@Transactional​​​​@Transactional​​​​@Transactional​​​​@Transactional​​​​@BeforeAll​​​​@BeforeEach​​​​@Transactional​​​​propagation​​​​NOT_SUPPORTED​​​​NEVER​

表 1.属性支持​​@Transactional​

支持测试管理的事务

​value​​​和​​transactionManager​

​propagation​

仅且受支持​​Propagation.NOT_SUPPORTED​​​​Propagation.NEVER​

​isolation​

​timeout​

​readOnly​

​rollbackFor​​​和​​rollbackForClassName​

否:使用代替​​TestTransaction.flagForRollback()​

​noRollbackFor​​​和​​noRollbackForClassName​

否:使用代替​​TestTransaction.flagForCommit()​

请注意,AbstractTransactionalJUnit4SpringContextTests和AbstractTransactionalTestNGSpringContextTests已预先配置为类级别的事务支持。

下面的示例演示编写集成测试的常见方案 a 基于休眠:​​UserRepository​

@SpringJUnitConfig(TestConfig.class)
@Transactional
class HibernateUserRepositoryTests {

@Autowired
HibernateUserRepository repository;

@Autowired
SessionFactory sessionFactory;

JdbcTemplate jdbcTemplate;

@Autowired
void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}

@Test
void createUser() {
// track initial state in test database:
final int count = countRowsInTable("user");

User user = new User(...);
repository.save(user);

// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush();
assertNumUsers(count + 1);
}

private int countRowsInTable(String tableName) {
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
}

private void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}

如事务回滚和提交行为中所述,无需 在方法运行后清理数据库,因为对 数据库由 自动回滚。​​createUser()​​​​TransactionalTestExecutionListener​

事务回滚和提交行为

默认情况下,测试事务将在完成 测试;但是,可以通过声明方式配置事务提交和回滚行为 通过神注释。有关更多详细信息,请参阅注释支持部分中的相应条目。​​@Commit​​​​@Rollback​

程序化交易管理

可以使用静态 方法在。例如,您可以在测试中使用 方法、方法之前和之后的方法开始或结束当前测试管理的 事务或配置当前测试管理的事务以进行回滚或提交。 只要启用,支持foris就会自动可用。​​TestTransaction​​​​TestTransaction​​​​TestTransaction​​​​TransactionalTestExecutionListener​

下面的示例演示了 的一些功能。请参阅的 javadoc forTestTransaction了解更多详情。​​TestTransaction​

@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
AbstractTransactionalJUnit4SpringContextTests {

@Test
public void transactionalTest() {
// assert initial state in test database:
assertNumUsers(2);

deleteFromTables("user");

// changes to the database will be committed!
TestTransaction.flagForCommit();
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertNumUsers(0);

TestTransaction.start();
// perform other actions against the database that will
// be automatically rolled back after the test completes...
}

protected void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}
在事务外部运行代码

有时,您可能需要在事务测试之前或之后运行某些代码 方法,但在事务上下文之外 — 例如,验证初始 运行测试或验证预期事务提交之前的数据库状态 测试运行后的行为(如果测试配置为提交事务)。支持此类方案的注释。您可以使用其中之一注释测试类中的任何方法或测试接口中的任何默认方法。 注释,并确保之前 事务方法或事务方法在适当的时间运行后事务方法。​​TransactionalTestExecutionListener​​​​@BeforeTransaction​​​​@AfterTransaction​​​​void​​​​void​​​​TransactionalTestExecutionListener​

配置事务管理器

​TransactionalTestExecutionListener​​预计阿豆会 在春季中定义用于测试。如果有多个实例 在测试中,您可以声明 限定符由 Usingor 实现,或者可以由 anclass 实现。咨询javadoc forTestContextTransactionUtils.retrieveTransactionManager()有关 用于在测试中查找事务管理器的算法。​​PlatformTransactionManager​​​​ApplicationContext​​​​PlatformTransactionManager​​​​ApplicationContext​​​​@Transactional("myTxMgr")​​​​@Transactional(transactionManager = "myTxMgr")​​​​TransactionManagementConfigurer​​​​@Configuration​​​​ApplicationContext​

演示所有与交易相关的注释

以下基于 JUnit Jupiter 的示例显示了一个虚构的集成测试 突出显示所有与事务相关的注释的方案。该示例不是故意的 演示最佳实践,而不是演示这些注释如何 使用。有关更多详细信息,请参阅注释支持部分 信息和配置示例。事务管理@Sql包含一个附加示例,该示例用于 具有默认事务回滚语义的声明性 SQL 脚本执行。这 以下示例显示了相关注释:​​@Sql​

@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {

@BeforeTransaction
void verifyInitialDatabaseState() {
// logic to verify the initial state before a transaction is started
}

@BeforeEach
void setUpTestDataWithinTransaction() {
// set up test data within the transaction
}

@Test
// overrides the class-level @Commit setting
@Rollback
void modifyDatabaseWithinTransaction() {
// logic which uses the test data and modifies database state
}

@AfterEach
void tearDownWithinTransaction() {
// run "tear down" logic within the transaction
}

@AfterTransaction
void verifyFinalDatabaseState() {
// logic to verify the final state after transaction has rolled back
}

}

3.5.10. 执行 SQL 脚本

在针对关系数据库编写集成测试时,通常有利于 运行 SQL 脚本以修改数据库架构或将测试数据插入表中。该模块支持初始化嵌入式或现有数据库 通过在加载 Springis 时执行 SQL 脚本。请参阅嵌入式数据库支持和使用 嵌入式数据库了解详细信息。​​spring-jdbc​​​​ApplicationContext​

尽管在加载时初始化数据库以进行测试非常有用,但有时必须能够修改 集成测试期间的数据库。以下各节说明如何运行 SQL 在集成测试期间以编程方式和声明方式编写脚本。​​ApplicationContext​

以编程方式执行 SQL 脚本

Spring 提供了以下选项,用于在 集成测试方法。

  • ​org.springframework.jdbc.datasource.init.ScriptUtils​
  • ​org.springframework.jdbc.datasource.init.ResourceDatabasePopulator​
  • ​org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests​
  • ​org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests​

​ScriptUtils​​提供用于处理 SQL 的静态实用工具方法的集合 脚本,主要用于框架内的内部使用。但是,如果您 需要完全控制SQL脚本的解析和运行方式,可能适合 您的需求比后面描述的其他一些替代方案更好。请参阅个人的 javadoc 方法,以获取更多详细信息。​​ScriptUtils​​​​ScriptUtils​

​ResourceDatabasePopulator​​提供基于对象的 API,用于以编程方式填充, 使用外部中定义的 SQL 脚本初始化或清理数据库 资源。提供用于配置字符的选项 编码、语句分隔符、注释分隔符和错误处理标志在以下情况下使用 解析和运行脚本。每个配置选项都有一个合理的 默认值。请参阅javadoc了解 有关默认值的详细信息。要运行 中配置的脚本,您可以调用任一方法来 针对 AOR 方法运行填充器 对 A 运行填充器。以下示例 为测试架构和测试数据指定 SQL 脚本,将语句分隔符设置为,并针对以下对象运行脚本:​​ResourceDatabasePopulator​​​​ResourceDatabasePopulator​​​​populate(Connection)​​​​java.sql.Connection​​​​execute(DataSource)​​​​javax.sql.DataSource​​​​@@​​​​DataSource​

@Test
void databaseTest() {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScripts(
new ClassPathResource("test-schema.sql"),
new ClassPathResource("test-data.sql"));
populator.setSeparator("@@");
populator.execute(this.dataSource);
// run code that uses the test schema and data
}

请注意,内部委托给解析 并运行 SQL 脚本。类似地,AbstractTransactionalJUnit4SpringContextTests和AbstractTransactionalTestNGSpringContextTests中的方法在内部使用a来运行SQL脚本。请参阅 Javadoc 了解 各种方法以获取更多详细信息。​​ResourceDatabasePopulator​​​​ScriptUtils​​​​executeSqlScript(..)​​​​ResourceDatabasePopulator​​​​executeSqlScript(..)​

使用 @Sql 以声明方式执行 SQL 脚本

除了上述以编程方式运行 SQL 脚本的机制之外, 您可以在 Spring TestContext Framework 中以声明方式配置 SQL 脚本。 具体来说,您可以在测试类或测试方法上声明注释 配置单个 SQL 语句或应为 SQL 脚本的资源路径 在集成测试方法之前或之后针对给定数据库运行。支持foris提供,默认启用。​​@Sql​​​​@Sql​​​​SqlScriptsTestExecutionListener​

路径资源语义

每条路径都被解释为一个弹簧。纯路径(例如)被视为相对于 定义测试类。以斜杠开头的路径被视为绝对路径 类路径资源(例如,)。引用 URL(例如,前缀为 ,,的路径)使用 指定的资源协议。​​Resource​​​​"schema.sql"​​​​"/org/example/schema.sql"​​​​classpath:​​​​file:​​​​http:​

下面的示例演示如何在类级别和方法级别使用 在基于 JUnit Jupiter 的集成测试类中:​​@Sql​

@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {

@Test
void emptySchemaTest() {
// run code that uses the test schema without any test data
}

@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})
void userTest() {
// run code that uses the test schema and test data
}
}
默认脚本检测

如果未指定 SQL 脚本或语句,则会尝试检测 ascript,具体取决于声明的位置。如果无法检测到默认值,则抛出 anis 。​​default​​​​@Sql​​​​IllegalStateException​

  • 类级声明:如果带注释的测试类是,则 对应的默认脚本是。com.example.MyTestclasspath:com/example/MyTest.sql
  • 方法级声明:如果带批注的测试方法已命名且 在类中定义,相应的默认脚本是。testMethod()com.example.MyTestclasspath:com/example/MyTest.testMethod.sql
声明多个集合​​@Sql​

如果需要为给定的测试类或测试配置多组 SQL 脚本 方法,但具有不同的语法配置、不同的错误处理规则或 每组不同的执行阶段,可以声明多个实例。跟 Java 8,可以用作可重复的注解。否则,您可以使用注释作为显式容器来声明多个实例。​​@Sql​​​​@Sql​​​​@SqlGroup​​​​@Sql​

以下示例显示了如何在 Java 8 中用作可重复的注释:​​@Sql​

@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
// run code that uses the test schema and test data
}

在前面示例中提供的方案中,脚本使用 单行注释的不同语法。​​test-schema.sql​

下面的示例与前面的示例相同,只是声明被组合在一起。对于 Java 8 及更高版本,使用 of 是可选的,但您可能需要使用 其他 JVM 语言,如 Kotlin。​​@Sql​​​​@SqlGroup​​​​@SqlGroup​​​​@SqlGroup​

@Test
@SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
)}
void userTest() {
// run code that uses the test schema and test data
}
脚本执行阶段

默认情况下,SQL 脚本在相应的测试方法之前运行。但是,如果 您需要在测试方法之后运行一组特定的脚本(例如,清理 向上数据库状态),您可以使用属性 in,作为 以下示例显示:​​executionPhase​​​​@Sql​

@Test
@Sql(
scripts = "create-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
scripts = "delete-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD
)
void userTest() {
// run code that needs the test data to be committed
// to the database outside of the test's transaction
}

请注意,and是分别静态导入自和的。​​ISOLATED​​​​AFTER_TEST_METHOD​​​​Sql.TransactionMode​​​​Sql.ExecutionPhase​

脚本配置​​@SqlConfig​

您可以使用注释配置脚本分析和错误处理。 当声明为集成测试类的类级注释时,用作测试类层次结构中所有 SQL 脚本的全局配置。什么时候 直接使用注解的属性声明,作为封闭注解中声明的SQL脚本的本地配置。每个属性都有一个隐式默认值,即 记录在相应属性的 javadoc 中。由于定义的规则 Java 语言规范中的注释属性,不幸的是,它不是 可以将值 of 分配给注释属性。因此,为了 支持覆盖继承的全局配置,属性具有 显式默认值为 (对于字符串)、(对于数组)或(对于 枚举)。此方法允许本地声明选择性地覆盖 全局声明中的单个属性通过提供值 其他 比,或。全局属性在任何时候都会继承 本地属性不提供除 、、 or 之外的显式值。因此,显式本地配置将覆盖全局配置。​​@SqlConfig​​​​@SqlConfig​​​​config​​​​@Sql​​​​@SqlConfig​​​​@Sql​​​​@SqlConfig​​​​null​​​​@SqlConfig​​​​""​​​​{}​​​​DEFAULT​​​​@SqlConfig​​​​@SqlConfig​​​​""​​​​{}​​​​DEFAULT​​​​@SqlConfig​​​​@SqlConfig​​​​""​​​​{}​​​​DEFAULT​

提供的配置选项等同于那些 支持的 byandbut 是其中的超集 由 XML 命名空间元素提供。参见 javadoc 有关详细信息@Sql和@SqlConfig中的单个属性。​​@Sql​​​​@SqlConfig​​​​ScriptUtils​​​​ResourceDatabasePopulator​​​​<jdbc:initialize-database/>​

事务管理​​@Sql​

默认情况下,推断所需的事务 使用配置的脚本的语义。具体来说,运行 SQL 脚本 没有事务,在现有的 Spring 管理的事务中(例如,一个 由 for 的测试管理的事务注释),或在隔离事务中,具体取决于配置的值 的属性和 ain 测试的存在。作为最低限度, 但是,A必须存在于测试中。​​SqlScriptsTestExecutionListener​​​​@Sql​​​​TransactionalTestExecutionListener​​​​@Transactional​​​​transactionMode​​​​@SqlConfig​​​​PlatformTransactionManager​​​​ApplicationContext​​​​javax.sql.DataSource​​​​ApplicationContext​

如果 byto 检测 aandand 使用的算法推断事务语义不符合您的需求, 您可以通过设置属性来指定显式名称。此外,您可以控制事务传播 通过设置属性的行为(例如,是否 脚本应在隔离事务中运行)。虽然彻底讨论了所有 支持的事务管理选项超出了此范围 参考手册,javadoc for@SqlConfig和SqlScriptsTestExecutionListener提供了详细信息,以下示例显示了典型的测试场景 使用JUnit Jupiter和事务测试:​​SqlScriptsTestExecutionListener​​​​DataSource​​​​PlatformTransactionManager​​​​dataSource​​​​transactionManager​​​​@SqlConfig​​​​transactionMode​​​​@SqlConfig​​​​@Sql​​​​@Sql​

@SpringJUnitConfig(TestDatabaseConfig.class)
@Transactional
class TransactionalSqlScriptsTests {

final JdbcTemplate jdbcTemplate;

@Autowired
TransactionalSqlScriptsTests(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}

@Test
@Sql("/test-data.sql")
void usersTest() {
// verify state in test database:
assertNumUsers(2);
// run code that uses the test data...
}

int countRowsInTable(String tableName) {
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
}

void assertNumUsers(int expected) {
assertEquals(expected, countRowsInTable("user"),
"Number of rows in the [user] table.");
}
}

请注意,在该方法之后,无需清理数据库 运行,因为对数据库所做的任何更改(在测试方法内或在脚本中)都会由 (请参阅事务管理 详细信息)。​​usersTest()​​​​/test-data.sql​​​​TransactionalTestExecutionListener​

合并和覆盖配置​​@SqlMergeMode​

从 Spring Framework 5.2 开始,可以将方法级声明与 类级声明。例如,这允许您为 每个测试类一次数据库模式或一些公共测试数据,然后提供额外的, 每个测试方法的用例特定测试数据。要启用合并,请注释 您的测试类或测试方法。要禁用合并 特定测试方法(或特定测试子类),可以切换回默认模式 通过。有关示例和更多详细信息,请参阅@SqlMergeMode注释文档部分。​​@Sql​​​​@Sql​​​​@SqlMergeMode(MERGE)​​​​@SqlMergeMode(OVERRIDE)​

3.5.11. 并行测试执行

Spring Framework 5.0 引入了对在 使用 Spring TestContext Framework 时的单个 JVM。一般来说,这意味着大多数 测试类或测试方法可以并行运行,而无需对测试代码进行任何更改 或配置。

请记住,在测试套件中引入并发可能会导致 意外的副作用、奇怪的运行时行为以及间歇性失败的测试或 看似随机。因此,春季团队提供以下一般准则 何时不并行运行测试。

如果测试符合以下条件,请不要并行运行测试:

  • 使用 Spring Framework 的支持。@DirtiesContext
  • 使用Spring Boot的sorsupport。@MockBean@SpyBean
  • 使用 JUnit 4 的支持或任何测试框架功能 旨在确保测试方法按特定顺序运行。注意 但是,如果整个测试类并行运行,则这不适用。@FixMethodOrder
  • 更改共享服务或系统(如数据库、消息代理、 文件系统等。这适用于嵌入式系统和外部系统。

3.5.12. 测试上下文框架支持类

本节介绍支持 Spring TestContext Framework 的各种类。

春季 JUnit 4 跑步者

Spring TestContext Framework 通过自定义提供与 JUnit 4 的完全集成 runner(在 JUnit 4.12 或更高版本上受支持)。通过使用较短的变体注释测试类,开发人员可以实现基于 JUnit 4 的标准单元和集成测试,以及 同时获得 TestContext 框架的好处,例如支持 加载应用程序上下文、测试实例的依赖关系注入、事务测试 方法执行等。如果要将 Spring TestContext 框架与 替代跑步者(如 JUnit 4 的跑步者)或第三方跑步者 (例如),您可以选择使用​ ​Spring 对 JUnit 规则的支持​​。​​@RunWith(SpringJUnit4ClassRunner.class)​​​​@RunWith(SpringRunner.class)​​​​Parameterized​​​​MockitoJUnitRunner​

下面的代码清单显示了将测试类配置为 使用自定义弹簧运行:​​Runner​

@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {

@Test
public void testMethod() {
// test logic...
}
}

在前面的示例中,配置了一个空列表,以 禁用默认侦听器,否则需要 anto 通过配置。​​@TestExecutionListeners​​​​ApplicationContext​​​​@ContextConfiguration​

春季 JUnit 4 规则

该软件包提供以下 JUnit 4 条规则(在 JUnit 4.12 或更高版本上受支持):​​org.springframework.test.context.junit4.rules​

  • ​SpringClassRule​
  • ​SpringMethodRule​

​SpringClassRule​​是一个支持Spring类级功能的JUnit TestContext Framework,而 JUnitthat 支持 Spring TestContext Framework 的实例级和方法级功能。​​TestRule​​​​SpringMethodRule​​​​MethodRule​

相比之下,Spring基于规则的JUnit支持具有以下优点: 独立于任何实现,因此可以 与现有的替代跑步者(如 JUnit 4)结合使用,或 第三方运行器(例如)。​​SpringRunner​​​​org.junit.runner.Runner​​​​Parameterized​​​​MockitoJUnitRunner​

要支持 TestContext 框架的全部功能,必须将 a与 a 结合使用。以下示例显示了正确的方法 要在集成测试中声明这些规则,请执行以下操作:​​SpringClassRule​​​​SpringMethodRule​

// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
public class IntegrationTest {

@ClassRule
public static final SpringClassRule springClassRule = new SpringClassRule();

@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();

@Test
public void testMethod() {
// test logic...
}
}
JUnit 4 支持类

该软件包提供以下支持 基于 JUnit 4 的测试用例的类(在 JUnit 4.12 或更高版本上受支持):​​org.springframework.test.context.junit4​

  • ​AbstractJUnit4SpringContextTests​
  • ​AbstractTransactionalJUnit4SpringContextTests​

​AbstractJUnit4SpringContextTests​​是一个抽象的基测试类,它集成了 Spring TestContext 框架,在 JUnit 4 环境。扩展时,您可以访问可用于执行显式的实例变量 Bean 查找或测试整个上下文的状态。​​ApplicationContext​​​​AbstractJUnit4SpringContextTests​​​​protected​​​​applicationContext​

​AbstractTransactionalJUnit4SpringContextTests​​是一个抽象的事务扩展,它为 JDBC 添加了一些便利功能 访问。此类期望 abean 和 abean 在 中定义。当你 扩展,您可以访问可用于运行 SQL 语句的实例变量来查询 数据库。可以使用此类查询来确认之前和之后的数据库状态 运行与数据库相关的应用程序代码,Spring 确保此类查询在 与应用程序代码相同的事务的范围。与 一个ORM工具,一定要避免误报。 如JDBC测试支持中所述,还提供了方便的方法 通过使用上述方法委托给方法。 此外,还提供了针对配置运行 SQL 脚本的方法。​​AbstractJUnit4SpringContextTests​​​​javax.sql.DataSource​​​​PlatformTransactionManager​​​​ApplicationContext​​​​AbstractTransactionalJUnit4SpringContextTests​​​​protected​​​​jdbcTemplate​​​​AbstractTransactionalJUnit4SpringContextTests​​​​JdbcTestUtils​​​​jdbcTemplate​​​​AbstractTransactionalJUnit4SpringContextTests​​​​executeSqlScript(..)​​​​DataSource​

SpringExtension for JUnit Jupiter

Spring TestContext Framework 提供与 JUnit Jupiter 测试的完全集成 框架,在 JUnit 5 中引入。通过注释测试类,您可以实现基于 JUnit 木星的标准单元 和集成测试,同时获得TestContext框架的好处, 例如支持加载应用程序上下文、测试实例的依赖注入、 事务测试方法执行等。​​@ExtendWith(SpringExtension.class)​

此外,由于JUnit Jupiter中丰富的扩展API,Spring提供了 以下功能超出了 Spring 对 JUnit 4 和 测试:

  • 测试构造函数、测试方法和测试生命周期回调的依赖项注入 方法。有关更多详细信息,请参阅SpringExtension的依赖注入。
  • 对条件的强大支持 基于SpEL 表达式、环境变量、系统属性、 等等。有关更多详细信息和示例,请参阅文档 春季JUnit 木星测试注释。@EnabledIf@DisabledIf
  • 自定义组合的注释,结合了来自 Spring 和 JUnit Jupiter 的注释。看 和示例 元注释 支持测试以获取更多详细信息。@TransactionalDevTestConfig@TransactionalIntegrationTest

下面的代码清单演示如何配置测试类以与 in 结合使用:​​SpringExtension​​​​@ContextConfiguration​

// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension.class)
// Instructs Spring to load an ApplicationContext from TestConfig.class
@ContextConfiguration(classes = TestConfig.class)
class SimpleTests {

@Test
void testMethod() {
// test logic...
}
}

由于您也可以在 JUnit 5 中使用注释作为元注释,因此 Spring 提供了 theand组合注释来简化 测试的配置和JUnit Jupiter。​​@SpringJUnitConfig​​​​@SpringJUnitWebConfig​​​​ApplicationContext​

以下示例用于减少配置量 在上一个示例中使用:​​@SpringJUnitConfig​

// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig.class)
class SimpleTests {

@Test
void testMethod() {
// test logic...
}
}

类似地,以下示例用于创建与 JUnit Jupiter 一起使用的 a:​​@SpringJUnitWebConfig​​​​WebApplicationContext​

// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig.class
@SpringJUnitWebConfig(TestWebConfig.class)
class SimpleWebTests {

@Test
void testMethod() {
// test logic...
}
}

有关更多详细信息,请参阅文档 春季JUnit Jupiter 测试注释。​​@SpringJUnitConfig​​​​@SpringJUnitWebConfig​

依赖注入​​SpringExtension​

​SpringExtension​​实现了来自JUnit Jupiter的ParameterResolver扩展API,它允许Spring为测试提供依赖注入 构造函数、测试方法和测试生命周期回调方法。

具体来说,可以从测试的测试构造函数和方法中注入依赖项,,,,,,, 等。​​SpringExtension​​​​ApplicationContext​​​​@BeforeAll​​​​@AfterAll​​​​@BeforeEach​​​​@AfterEach​​​​@Test​​​​@RepeatedTest​​​​@ParameterizedTest​

构造函数注入

如果 JUnit Jupiter 测试类的构造函数中的特定参数是类型(或其子类型),或者用 ,,或者,Spring 注入该特定值 参数,具有测试中的相应 bean 或值。​​ApplicationContext​​​​@Autowired​​​​@Qualifier​​​​@Value​​​​ApplicationContext​

Spring 还可以配置为自动连接测试类构造函数的所有参数,如果 构造函数被认为是可自动的。构造函数被视为 如果满足以下条件之一(按优先级顺序),则可自动执行。

  • 构造函数被批注。@Autowired
  • ​@TestConstructor​​在属性设置为 的测试类上存在或元存在。autowireModeALL
  • 默认测试构造函数自动连线模式已更改为。ALL

有关全局测试构造函数自动连线模式的使用以及如何更改的详细信息,请参阅@TestConstructor。​​@TestConstructor​

在下面的示例中,Spring 将 bean 从加载的 from注入到构造函数中。​​OrderService​​​​ApplicationContext​​​​TestConfig.class​​​​OrderServiceIntegrationTests​

@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

private final OrderService orderService;

@Autowired
OrderServiceIntegrationTests(OrderService orderService) {
this.orderService = orderService;
}

// tests that use the injected OrderService
}

请注意,此功能允许测试依赖项因此不可变。​​final​

如果属性是 to(see​ ​@TestConstructor​​),我们可以省略上一个示例中构造函数的声明,结果如下。​​spring.test.constructor.autowire.mode​​​​all​​​​@Autowired​

@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

private final OrderService orderService;

OrderServiceIntegrationTests(OrderService orderService) {
this.orderService = orderService;
}

// tests that use the injected OrderService
}

如果 JUnit Jupiter 测试方法或测试生命周期回调方法中的参数 类型(或其子类型)或注释或元注释,或,Spring 注入该特定值 参数,以及测试中的相应 bean。​​ApplicationContext​​​​@Autowired​​​​@Qualifier​​​​@Value​​​​ApplicationContext​

在下面的示例中,Spring 注入了 from 加载的 fromto 测试方法:​​OrderService​​​​ApplicationContext​​​​TestConfig.class​​​​deleteOrder()​

@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

@Test
void deleteOrder(@Autowired OrderService orderService) {
// use orderService from the test's ApplicationContext
}
}

由于JUnit Jupiter中支持的健壮性,您还可以 将多个依赖项注入到单个方法中,不仅来自 Spring,还来自 来自JUnit Jupiter本身或其他第三方扩展。​​ParameterResolver​

下面的例子展示了如何让 Spring 和 JUnit Jupiter 注入依赖关系 同时进入测试方法。​​placeOrderRepeatedly()​

@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

@RepeatedTest(10)
void placeOrderRepeatedly(RepetitionInfo repetitionInfo,
@Autowired OrderService orderService) {

// use orderService from the test's ApplicationContext
// and repetitionInfo from JUnit Jupiter
}
}

请注意,使用 from JUnit Jupiter 可以让测试方法获得访问权限 到。​​@RepeatedTest​​​​RepetitionInfo​

​@Nested​​测试类配置

自Spring Framework 5.0以来,Spring TestContext Framework支持在JUnit Jupiter的测试类中使用与测试相关的注释;然而,直到春天 框架 5.3 类级测试配置注释不是 封闭类,就像它们来自超类一样。​​@Nested​

Spring Framework 5.3 引入了对继承测试类的一流支持 封闭类的配置,这种配置将由 违约。要从默认模式更改为模式,您可以注释 个人测试类。显式声明将适用于带注释的测试类以及 它的任何子类和嵌套类。因此,您可以注释顶级测试类 with,这将适用于其所有嵌套测试类 递 归。​​INHERIT​​​​OVERRIDE​​​​@Nested​​​​@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)​​​​@NestedTestConfiguration​​​​@NestedTestConfiguration​

为了允许开发团队将默认值更改为 - 例如, 为了与 Spring Framework 5.0 到 5.2 兼容 – 可以更改默认模式 全局通过 JVM 系统属性或根目录中的文件 类路径。请参阅“更改 默认封闭配置继承模式“注意了解详情。​​OVERRIDE​​​​spring.properties​

虽然下面的“Hello World”示例非常简单,但它展示了如何声明 由 itstest 继承的顶级类上的通用配置 类。在此特定示例中,只有配置类是 继承。每个嵌套测试类都提供自己的一组活动配置文件,从而产生 对于每个嵌套测试类的不同(有关详细信息,请参阅上下文缓存)。请参阅支持的注释列表以查看 哪些注释可以在测试类中继承。​​@Nested​​​​TestConfig​​​​ApplicationContext​​​​@Nested​

@SpringJUnitConfig(TestConfig.class)
class GreetingServiceTests {

@Nested
@ActiveProfiles("lang_en")
class EnglishGreetings {

@Test
void hello(@Autowired GreetingService service) {
assertThat(service.greetWorld()).isEqualTo("Hello World");
}
}

@Nested
@ActiveProfiles("lang_de")
class GermanGreetings {

@Test
void hello(@Autowired GreetingService service) {
assertThat(service.greetWorld()).isEqualTo("Hallo Welt");
}
}
}
测试NG支持类

该软件包提供以下支持 基于 TestNG 的测试用例的类:​​org.springframework.test.context.testng​

  • ​AbstractTestNGSpringContextTests​
  • ​AbstractTransactionalTestNGSpringContextTests​

​AbstractTestNGSpringContextTests​​是一个抽象的基测试类,它集成了 Spring TestContext 框架,在 测试NG环境。扩展时,您可以访问可用于执行显式的实例变量 Bean 查找或测试整个上下文的状态。​​ApplicationContext​​​​AbstractTestNGSpringContextTests​​​​protected​​​​applicationContext​

​AbstractTransactionalTestNGSpringContextTests​​是一个抽象的事务扩展,它为 JDBC 添加了一些便利功能 访问。此类期望 abean 和 abean 在 中定义。当你 扩展,您可以访问可用于运行 SQL 语句的实例变量来查询 数据库。可以使用此类查询来确认之前和之后的数据库状态 运行与数据库相关的应用程序代码,Spring 确保此类查询在 与应用程序代码相同的事务的范围。与 一个ORM工具,一定要避免误报。 如JDBC测试支持中所述,还提供了方便的方法 通过使用上述方法委托给方法。 此外,还提供了针对配置运行 SQL 脚本的方法。​​AbstractTestNGSpringContextTests​​​​javax.sql.DataSource​​​​PlatformTransactionManager​​​​ApplicationContext​​​​AbstractTransactionalTestNGSpringContextTests​​​​protected​​​​jdbcTemplate​​​​AbstractTransactionalTestNGSpringContextTests​​​​JdbcTestUtils​​​​jdbcTemplate​​​​AbstractTransactionalTestNGSpringContextTests​​​​executeSqlScript(..)​​​​DataSource​

3.5.13. 测试的提前支持

本章介绍 Spring 的提前 (AOT) 对集成测试的支持,使用 Spring TestContext Framework。

测试支持扩展了 Spring的核心 AOT 支持 以下功能。

  • 对当前项目中使用 要加载的测试上下文框架。ApplicationContext
  • 为基于 JUnit Jupiter 和 JUnit 4 的测试类提供显式支持 作为对TestNG和其他使用Spring核心的测试框架的隐式支持 测试注释 — 只要测试是使用为当前项目注册的 JUnit 平台运行的。TestEngine
  • 构建时 AOT 处理:当前项目中每个唯一的测试 将刷新以进行 AOT 处理。ApplicationContext
  • 运行时 AOT 支持:在 AOT 运行时模式下执行时,Spring 集成测试将 使用 AOT 优化该 以透明方式参与上下文缓存。ApplicationContext

要提供特定于测试的运行时提示以在 GraalVM 本机映像中使用,您需要 以下选项。

  • 实现自定义TestRuntimeHintsRegistrar并通过以下方式全局注册它。META-INF/spring/aot.factories
  • 实现自定义运行时提示注册器,并通过测试类本地全局注册它 通过@ImportRuntimeHints。META-INF/spring/aot.factories
  • 使用@Reflective或@RegisterReflectionForBinding注释测试类。
  • 有关 Spring 核心运行时提示的详细信息,请参阅运行时提示 和注释支持。

如果实现自定义,则必须在​ 以提供 AOT 构建时处理和 AOT 运行时执行支持。注意 但是,Spring 框架提供的所有上下文加载器实现和 Spring Boot 已经实现。​​ContextLoader​​​​AotContextLoader​

如果实现自定义,则必须实现AotTestExecutionListener才能参与 AOT 处理。见 模块为例。​​TestExecutionListener​​​​SqlScriptsTestExecutionListener​​​​spring-test​

3.6. 网络测试客户端

​WebTestClient​​是专为测试服务器应用程序而设计的 HTTP 客户端。它包裹 Spring 的WebClient并使用它来执行请求 但公开用于验证响应的测试外观。可用于 执行端到端 HTTP 测试。它也可以用来测试Spring MVC和Spring WebFlux。 没有正在运行的服务器的应用程序通过模拟服务器请求和响应对象。​​WebTestClient​

3.6.1. 设置

要进行设置,您需要选择要绑定到的服务器设置。这可以是一个 多个模拟服务器设置选项或与实时服务器的连接。​​WebTestClient​

绑定到控制器

此设置允许您通过模拟请求和响应对象测试特定控制器, 没有正在运行的服务器。

对于 WebFlux 应用程序,请使用以下命令加载等效于WebFlux Java 配置的基础架构,注册给定的 控制器,并创建一个WebHandler 链来处理请求:

WebTestClient client =
WebTestClient.bindToController(new TestController()).build();

对于Spring MVC,请使用以下内容委托给StandaloneMockMvcBuilder来加载等效于WebMvc Java配置的基础架构, 注册给定的控制器,并创建一个MockMvc实例来处理请求:

WebTestClient client =
MockMvcWebTestClient.bindToController(new TestController()).build();
绑定到​​ApplicationContext​

此设置允许您使用Spring MVC或Spring WebFlux加载Spring配置。 基础设施和控制器声明,并使用它来通过模拟请求处理请求 和响应对象,没有正在运行的服务器。

对于 WebFlux,请使用 Springis 传递给WebHttpHandlerBuilder的以下内容来创建要处理的WebHandler 链。 请求:​​ApplicationContext​

@SpringJUnitConfig(WebConfig.class)
class MyTests {

WebTestClient client;

@BeforeEach
void setUp(ApplicationContext context) {
client = WebTestClient.bindToApplicationContext(context).build();
}
}

指定要加载的配置

创建​​WebTestClient​

对于Spring MVC,请使用以下内容,其中Springis传递给MockMvcBuilders.webAppContextSetup以创建一个MockMvc实例来处理 请求:​​ApplicationContext​

@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources")
@ContextHierarchy({
@ContextConfiguration(classes = RootConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class MyTests {

@Autowired
WebApplicationContext wac;

WebTestClient client;

@BeforeEach
void setUp() {
client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build();
}
}
绑定路由器功能

此设置允许您通过以下方式测试功能终结点 模拟请求和响应对象,无需正在运行的服务器。

对于 WebFlux,请使用以下内容 委托 toto 创建服务器设置以处理请求:​​RouterFunctions.toWebHandler​

RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();

对于Spring MVC,目前没有测试WebMvc功能端点的选项。

绑定到服务器

此设置连接到正在运行的服务器以执行完整的端到端 HTTP 测试:

client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
客户端配置

除了前面描述的服务器设置选项外,您还可以配置客户端 选项,包括基本 URL、默认标头、客户端筛选器等。这些选项 以下是现成的。对于所有其他配置选项, 您需要使用从服务器到客户端配置的转换,因为 遵循:​​bindToServer()​​​​configureClient()​

client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.build();

3.6.2. 编写测试

​WebTestClient​​提供与WebClient相同的 API,直到使用执行请求为止。有关如何执行以下操作的示例,请参阅WebClient文档 准备包含任何内容(包括表单数据、多部分数据等)的请求。​​exchange()​

调用后,偏离和 而是继续执行工作流来验证响应。​​exchange()​​​​WebTestClient​​​​WebClient​

若要断言响应状态和标头,请使用以下内容:

client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON);

如果您希望即使其中一个失败也能断言所有期望,您可以 使用代替多个链式调用。此功能是 类似于 AssertJ 中的软断言支持和 朱尼特木星。​​expectAll(..)​​​​expect*(..)​​​​assertAll()​

client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
spec -> spec.expectStatus().isOk(),
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
);

然后,可以选择通过以下方法之一解码响应正文:

  • ​expectBody(Class<T>)​​:解码为单个对象。
  • ​expectBodyList(Class<T>)​​:解码并收集对象。List<T>
  • ​expectBody()​​:解码为JSON 内容或空正文。byte[]

并对生成的更高级别对象执行断言:

client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList(Person.class).hasSize(3).contains(person);

如果内置断言不足,则可以改用对象和 执行任何其他断言:

client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (e.g. AssertJ)...
});

或者,您可以退出工作流并获取:​​EntityExchangeResult​

EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();

如果响应不应包含内容,则可以断言如下:

client.post().uri("/persons")
.body(personMono, Person.class)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();

如果要忽略响应内容,下面发布的内容没有 任何断言:

client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
杰伦斯内容

您可以使用没有目标类型对原始数据执行断言 内容,而不是通过更高级别的对象。​​expectBody()​

要使用JSONAssert 验证完整的 JSON 内容,请执行以下操作:

client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")

要使用JSONPath 验证 JSON 内容,请执行以下操作:

client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
流式处理响应

要测试潜在的无限流(如 asor),请首先验证响应状态和标头,然后 获得:​​"text/event-stream"​​​​"application/x-ndjson"​​​​FluxExchangeResult​

FluxExchangeResult<MyEvent> result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(MyEvent.class);

现在,你已准备好使用来自以下位置的响应流:​​StepVerifier​​​​reactor-test​

Flux<Event> eventFlux = result.getResponseBody();

StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith(p -> ...)
.thenCancel()
.verify();
模拟Mvc断言

​WebTestClient​​是一个 HTTP 客户端,因此它只能验证客户端中的内容 响应,包括状态、标头和正文。

当使用MockMvc服务器设置测试Spring MVC应用程序时,您有额外的 选择对服务器响应执行进一步断言。要做到这一点,请从 获得断言身体后:​​ExchangeResult​

// For a response with a body
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();

// For a response without a body
EntityExchangeResult<Void> result = client.get().uri("/path")
.exchange()
.expectBody().isEmpty();

然后切换到 MockMvc 服务器响应断言:

MockMvcWebTestClient.resultActionsFor(result)
.andExpect(model().attribute("integer", 3))
.andExpect(model().attribute("string", "a string value"));

3.7. 模拟Mvc

Spring MVC 测试框架,也称为 MockMvc,为测试 Spring 提供支持。 MVC 应用程序。它执行完整的Spring MVC请求处理,但通过模拟请求和 响应对象而不是正在运行的服务器。

MockMvc 可以单独用于执行请求和验证响应。它也可以是 通过WebTestClient使用,其中MockMvc入作为服务器来处理 请求。优点是可以选择使用更高级别 对象而不是原始数据,以及切换到完整的端到端HTTP的能力 针对实时服务器进行测试并使用相同的测试 API。​​WebTestClient​

3.7.1. 概述

你可以通过实例化一个控制器,注入它来为Spring MVC编写简单的单元测试 ,并调用其方法。但是,此类测试不会验证请求 映射、数据绑定、消息转换、类型转换、验证等 它们是否涉及任何支持,或方法。​​@InitBinder​​​​@ModelAttribute​​​​@ExceptionHandler​

Spring MVC 测试框架,也称为,旨在提供更完整的 测试没有正在运行的服务器的 Spring MVC 控制器。它通过调用 并从模块传递 Servlet API 的“模拟”实现,该模块复制了完整的 Spring MVC 请求处理,而无需 正在运行的服务器。​​MockMvc​​​​DispatcherServlet​​​​spring-test​

MockMvc 是一个服务器端测试框架,可让您验证大多数功能 使用轻量级和有针对性的测试的Spring MVC应用程序。您可以在 它自己来执行请求和验证响应,或者您也可以通过 WebTestClientAPI,插入MockMvc作为服务器来处理请求 跟。

直接使用 MockMvc 执行请求时,您需要静态导入:

  • ​MockMvcBuilders.*​
  • ​MockMvcRequestBuilders.*​
  • ​MockMvcResultMatchers.*​
  • ​MockMvcResultHandlers.*​

记住这一点的一种简单方法是搜索。如果使用 Eclipse,请确保也 将上述内容添加为 Eclipse 首选项中的“收藏夹静态成员”。​​MockMvc*​

当通过WebTestClient使用MockMvc时,你不需要静态导入。 提供流畅的API,无需静态导入。​​WebTestClient​

MockMvc可以通过以下两种方式之一进行设置。一种是直接指向控制器你 想要以编程方式测试和配置 Spring MVC 基础结构。二是 指向包含 Spring MVC 和控制器基础结构的 Spring 配置。

要设置 MockMvc 以测试特定控制器,请使用以下命令:

class MyWebTests {

MockMvc mockMvc;

@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}

// ...

}

或者,您也可以在通过委托给同一构建器的WebTestClient进行测试时使用此设置 如上图。

要通过 Spring 配置设置 MockMvc,请使用以下命令:

@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
class MyWebTests {

MockMvc mockMvc;

@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}

// ...

}

或者,您也可以在通过委托给同一构建器的WebTestClient进行测试时使用此设置 如上图。

您应该使用哪个设置选项?

加载您的实际 Spring MVC 配置,从而产生更多 完整的集成测试。由于 TestContext 框架缓存了加载的 Spring 配置,它有助于保持测试快速运行,即使您在 测试套件。此外,您可以通过 Spring 将模拟服务注入控制器 配置以保持专注于测试 Web 图层。以下示例声明 与Mockito的模拟服务:​​webAppContextSetup​

<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.example.AccountService"/>
</bean>

然后,您可以将模拟服务注入测试中,以设置和验证您的 期望,如以下示例所示:

@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class AccountTests {

@Autowired
AccountService accountService;

MockMvc mockMvc;

@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}

// ...

}

另一方面,它更接近单元测试。它测试一个 一次控制器。您可以手动注入具有模拟依赖项的控制器,并且 它不涉及加载弹簧配置。此类测试更侧重于风格 并更容易查看正在测试的控制器,是否有任何特定的弹簧 需要 MVC 配置才能工作,依此类推。泰斯也是一个非常 编写临时测试以验证特定行为或调试问题的便捷方法。​​standaloneSetup​​​​standaloneSetup​

与大多数“集成与单元测试”的争论一样,没有对错之分。 答。但是,使用 thedo 意味着需要进行额外的测试来验证您的 Spring MVC 配置。 或者,您可以编写所有测试,以便始终 针对您的实际 Spring MVC 配置进行测试。​​standaloneSetup​​​​webAppContextSetup​​​​webAppContextSetup​

无论您使用哪个 MockMvc 构建器,所有实现都提供 一些常见且非常有用的功能。例如,您可以声明 所有请求,并期望状态为 200 以及所有标头 响应,如下所示:​​MockMvcBuilder​​​​Accept​​​​Content-Type​

// static import of MockMvcBuilders.standaloneSetup

MockMvc mockMvc = standaloneSetup(new MusicController())
.defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build();

此外,第三方框架(和应用程序)可以预先打包设置 说明,例如 A 中的说明。Spring 框架有一个这样的 内置实现,有助于跨请求保存和重用 HTTP 会话。 您可以按如下方式使用它:​​MockMvcConfigurer​

// static import of SharedHttpSessionConfigurer.sharedHttpSession

MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
.apply(sharedHttpSession())
.build();

// Use mockMvc to perform requests...

请参阅ConfigurableMockMvcBuilder的 javadoc 以获取所有 MockMvc 构建器功能的列表,或使用 IDE 浏览可用选项。

本节介绍如何单独使用 MockMvc 来执行请求和验证响应。 如果通过 MockMvc 使用 请参阅有关编写测试的相应部分。​​WebTestClient​

执行使用任何 HTTP 方法的请求,如以下示例所示:

// static import of MockMvcRequestBuilders.*

mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));

您还可以执行内部使用的文件上传请求,以便不会实际解析多部分 请求。相反,您必须将其设置为类似于以下示例:​​MockMultipartHttpServletRequest​

mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));

可以在 URI 模板样式中指定查询参数,如以下示例所示:

mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));

您还可以添加表示查询或表单的 Servlet 请求参数 参数,如以下示例所示:

mockMvc.perform(get("/hotels").param("thing", "somewhere"));

如果应用程序代码依赖于 Servlet 请求参数并且不检查查询 字符串显式(最常见的情况),使用哪个选项并不重要。 但请记住,随 URI 模板提供的查询参数将被解码 而通过该方法提供的请求参数预计已经 被解码。​​param(…)​

在大多数情况下,最好将上下文路径和 Servlet 路径排除在 请求 URI。如果必须使用完整的请求 URI 进行测试,请确保相应地设置 and,以便请求映射正常工作,如以下示例所示 显示:​​contextPath​​​​servletPath​

mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))

在前面的示例中,设置每个执行的请求都会很麻烦。相反,您可以设置默认请求 属性,如以下示例所示:​​contextPath​​​​servletPath​

class MyWebTests {

MockMvc mockMvc;

@BeforeEach
void setup() {
mockMvc = standaloneSetup(new AccountController())
.defaultRequest(get("/")
.contextPath("/app").servletPath("/main")
.accept(MediaType.APPLICATION_JSON)).build();
}
}

上述属性会影响通过实例执行的每个请求。 如果在给定请求上也指定了相同的属性,它将覆盖默认值 价值。这就是为什么默认请求中的 HTTP 方法和 URI 无关紧要的原因,因为 必须在每个请求上指定它们。​​MockMvc​

您可以通过在之后附加一个或多个调用来定义期望 执行请求,如以下示例所示。一旦一个期望失败, 不会断言其他期望。​​andExpect(..)​

// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());

您可以通过追加来定义多个期望在执行 请求,如以下示例所示。相反,保证所有提供的期望都将被断言,并且 将跟踪和报告所有故障。​​andExpectAll(..)​​​​andExpect(..)​​​​andExpectAll(..)​

// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

mockMvc.perform(get("/accounts/1")).andExpectAll(
status().isOk(),
content().contentType("application/json;charset=UTF-8"));

​MockMvcResultMatchers.*​​提供了许多期望,其中一些是进一步的 嵌套了更详细的期望。

期望分为两大类。第一类断言验证 响应的属性(例如,响应状态、标头和内容)。 这些是要断言的最重要的结果。

第二类断言超越了回应。这些断言让你 检查Spring MVC的具体方面,例如哪个控制器方法处理了 请求,是否引发和处理异常,模型的内容是什么, 选择了什么视图,添加了哪些闪存属性,等等。他们也让你 检查 Servlet 的特定方面,例如请求和会话属性。

以下测试断言绑定或验证失败:

mockMvc.perform(post("/persons"))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));

很多时候,在编写测试时,转储执行的结果很有用 请求。您可以按如下方式执行此操作,其中静态导入来自:​​print()​​​​MockMvcResultHandlers​

mockMvc.perform(post("/persons"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));

只要请求处理不会导致未处理的异常,该方法 将所有可用的结果数据打印到。还有一个方法和 该方法的另外两个变体,一个接受阿南德 一个接受 a.例如,调用打印结果 数据到,同时调用将结果数据打印到自定义 作家。如果要记录结果数据而不是打印结果数据,可以调用该方法,该方法将结果数据记录为日志记录类别下的单个消息。​​print()​​​​System.out​​​​log()​​​​print()​​​​OutputStream​​​​Writer​​​​print(System.err)​​​​System.err​​​​print(myWriter)​​​​log()​​​​DEBUG​​​​org.springframework.test.web.servlet.result​

在某些情况下,您可能希望直接访问结果并验证某些内容 否则无法验证。这可以通过附加来实现毕竟 其他期望,如以下示例所示:​​.andReturn()​

MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...

如果所有测试都重复相同的期望,则可以在以下情况下设置一次共同的期望 生成实例,如以下示例所示:​​MockMvc​

standaloneSetup(new SimpleController())
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build()

请注意,始终应用共同的期望,并且不能在没有 创建单独的实例。​​MockMvc​

当 JSON 响应内容包含使用Spring HATEOAS 创建的超媒体链接时,您可以验证 使用 JsonPath 表达式生成链接,如以下示例所示:

mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));

当XML响应内容包含使用Spring HATEOAS创建的超媒体链接时,您可以验证 使用 XPath 表达式生成的链接:

Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
.andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));

本节介绍如何单独使用 MockMvc 来测试异步请求处理。 如果通过WebTestClient使用MockMvc,则没有什么特别的事情要做。 异步请求工作,自动执行所描述的操作 在本节中。​​WebTestClient​

Servlet 异步请求,在 Spring MVC 中受支持 通过退出 Servlet 容器线程并允许应用程序计算来工作 异步响应,之后进行异步调度以完成 在 Servlet 容器线程上处理。

在Spring MVC测试中,可以通过断言生成的异步值来测试异步请求 首先,然后手动执行异步调度,最后验证响应。 下面是返回的控制器方法的示例测试, 或反应类型,如反应器:​​DeferredResult​​​​Callable​​​​Mono​

// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

@Test
void test() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(get("/path"))
.andExpect(status().isOk())
.andExpect(request().asyncStarted())
.andExpect(request().asyncResult("body"))
.andReturn();

this.mockMvc.perform(asyncDispatch(mvcResult))
.andExpect(status().isOk())
.andExpect(content().string("body"));
}

检查响应状态仍未更改

异步处理必须已启动

等待并断言异步结果

手动执行异步调度(因为没有正在运行的容器)

验证最终响应

流式处理响应

测试流响应(如服务器发送的事件)的最佳方法是通过WebTestClient,该客户端可用作测试客户端以连接到实例 在没有运行服务器的情况下对 Spring MVC 控制器执行测试。例如:​​MockMvc​

WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build();

FluxExchangeResult<Person> exchangeResult = client.get()
.uri("/persons")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType("text/event-stream")
.returnResult(Person.class);

// Use StepVerifier from Project Reactor to test the streaming response

StepVerifier.create(exchangeResult.getResponseBody())
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
.expectNextCount(4)
.consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
.thenCancel()
.verify();

​WebTestClient​​还可以连接到实时服务器并执行完整的端到端集成 测试。这在 Spring 引导中也受支持,您可以在其中测试正在运行的服务器。

过滤器注册

设置实例时,可以注册一个或多个 Servlet 实例,如以下示例所示:​​MockMvc​​​​Filter​

mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();

注册的过滤器通过 from 调用,并且 最后一个筛选器委托给。​​MockFilterChain​​​​spring-test​​​​DispatcherServlet​

模拟MVC vs 端到端测试

MockMVc建立在模块的Servlet API模拟实现之上,不依赖于正在运行的容器。因此,有 与具有实际的完整端到端集成测试相比,存在一些差异 客户端和正在运行的实时服务器。​​spring-test​

考虑这个问题的最简单方法是从空白开始。 无论您向其中添加什么内容,请求都会变成什么样子。可能会让你措手不及的事情 默认情况下没有上下文路径;诺饼干;无转发, 错误或异步调度;因此,没有实际的 JSP 渲染。相反 “转发”和“重定向”的 URL 保存在 和 可以 满怀期望。​​MockHttpServletRequest​​​​jsessionid​​​​MockHttpServletResponse​

这意味着,如果您使用 JSP,则可以验证请求所在的 JSP 页面。 已转发,但不呈现 HTML。换句话说,不会调用 JSP。注意 但是,所有其他不依赖于转发的渲染技术,例如 Thymeleaf 和 Freemarker 按预期将 HTML 呈现到响应正文。事实也是如此 用于通过方法呈现 JSON、XML 和其他格式。​​@ResponseBody​

或者,您可以考虑从 弹簧启动。请参阅Spring 引导参考指南。​​@SpringBootTest​

每种方法都有优点和缺点。Spring MVC 测试中提供的选项是 从经典单元测试到完全集成测试的规模上有不同的停止。待成为 当然,Spring MVC 测试中的任何选项都不属于经典单元类别 测试,但他们离它更近一点。例如,您可以隔离 Web 图层 通过将模拟服务注入控制器,在这种情况下,您正在测试 Web 层仅通过但与实际的弹簧配置,如你 可能会独立于其上层测试数据访问层。此外,您可以使用 独立设置,一次专注于一个控制器并手动提供 需要配置才能使其工作。​​DispatcherServlet​

使用 Spring MVC 测试时的另一个重要区别是,从概念上讲,这样的 测试是服务器端的,因此您可以检查使用了哪个处理程序(如果异常是 使用 HandlerExceptionResolver 处理,模型的内容是什么,什么绑定 有错误,以及其他细节。这意味着写期望更容易, 由于服务器不是一个不透明的盒子,就像通过实际的HTTP测试它时一样 客户。这通常是经典单元测试的一个优点:它更容易编写, 原因和调试,但不能取代对完全集成测试的需求。在 同时,重要的是不要忽视这样一个事实,即响应是最 要检查的重要事项。简而言之,这里有多种风格和策略的空间 即使在同一个项目中进行测试。

该框架自己的测试包括许多示例测试,旨在展示如何单独或通过WebTestClient使用MockMvc。浏览这些示例以获取更多想法。

3.7.2. HTML单元集成

Spring 提供了MockMvc和HtmlUnit 之间的集成。这简化了端到端测试的执行 使用基于 HTML 的视图时。通过此集成,您可以:

  • 通过使用HtmlUnit、WebDriver 和Geb等工具轻松测试 HTML 页面,而无需 部署到 Servlet 容器。
  • 在页面中测试 JavaScript。
  • (可选)使用模拟服务进行测试以加快测试速度。
  • 在容器内端到端测试和容器外集成测试之间共享逻辑。
为什么选择HtmlUnit集成?

我想到的最明显的问题是“我为什么需要这个?答案是 最好通过探索一个非常基本的示例应用程序来找到。假设你有一个Spring MVC网站 支持对对象执行 CRUD 操作的应用程序。该应用程序还 支持对所有消息进行分页。您将如何测试它?​​Message​

使用 Spring MVC 测试,我们可以轻松测试是否能够创建一个,如下所示:​​Message​

MockHttpServletRequestBuilder createMessage = post("/messages/")
.param("summary", "Spring Rocks")
.param("text", "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/messages/123"));

如果我们想测试允许我们创建消息的表单视图怎么办?例如 假设我们的表单类似于以下代码片段:

<form id="messageForm" action="/messages/" method="post">
<div class="pull-right"><a href="/messages/">Messages</a></div>

<label for="summary">Summary</label>
<input type="text" class="required" id="summary" name="summary" value="" />

<label for="text">Message</label>
<textarea id="text" name="text"></textarea>

<div class="form-actions">
<input type="submit" value="Create" />
</div>
</form>

我们如何确保表单生成正确的请求来创建新消息?一个 幼稚的尝试可能类似于以下内容:

mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='summary']").exists())
.andExpect(xpath("//textarea[@name='text']").exists());

该测试有一些明显的缺点。如果我们更新控制器以使用参数代替,我们的表单测试将继续通过,即使 HTML 表单 与控制器不同步。为了解决这个问题,我们可以结合我们的两个测试,如 遵循:​​message​​​​text​

String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
.andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());

MockHttpServletRequestBuilder createMessage = post("/messages/")
.param(summaryParamName, "Spring Rocks")
.param(textParamName, "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/messages/123"));

这将降低我们的测试错误通过的风险,但仍有一些 问题:

  • 如果我们的页面上有多个表单怎么办?诚然,我们可以更新我们的XPath 表达式,但随着我们考虑更多因素,它们变得更加复杂: 字段的类型是否正确?是否启用了字段?等等。
  • 另一个问题是,我们所做的工作是预期的两倍。我们必须首先 验证视图,然后使用刚刚验证的相同参数提交视图。 理想情况下,这可以一次完成。
  • 最后,我们仍然无法解释一些事情。例如,如果表单具有 我们也希望测试JavaScript验证?

总体问题是测试网页不涉及单个交互。 相反,它是用户如何与网页交互以及该网页如何的组合 页面与其他资源交互。例如,窗体视图的结果用作 用于创建消息的用户输入。此外,我们的表单视图可能会 使用影响页面行为的其他资源,例如 JavaScript 验证。

集成测试来救援?

为了解决前面提到的问题,我们可以执行端到端的集成测试, 但这有一些缺点。考虑测试允许我们分页浏览 消息。我们可能需要以下测试:

  • 我们的页面是否向用户显示通知以指示没有结果 消息为空时可用?
  • 我们的页面是否正确显示一条消息?
  • 我们的页面是否正确支持分页?

要设置这些测试,我们需要确保我们的数据库包含正确的消息。这 这带来了一些额外的挑战:

  • 确保数据库中有正确的消息可能很乏味。(考虑外键 约束。
  • 测试可能会变慢,因为每个测试都需要确保数据库位于 正确的状态。
  • 由于我们的数据库需要处于特定状态,因此我们无法并行运行测试。
  • 对自动生成的 ID、时间戳等项执行断言可以 很难。

这些挑战并不意味着我们应该放弃端到端的集成测试 完全。相反,我们可以通过以下方式减少端到端集成测试的数量 重构我们的详细测试,以使用运行更快、更可靠的模拟服务, 并且没有副作用。然后,我们可以实现少量真正的端到端 集成测试,用于验证简单的工作流程,以确保一切协同工作 适当地。

进入 HtmlUnit Integration

那么我们如何才能在测试页面的交互和仍然之间取得平衡 在我们的测试套件中保持良好的性能?答案是:“通过集成MockMvc。 与HtmlUnit。

Html单元集成选项

当您想要将MockMvc与HtmlUnit集成时,您有许多选择:

  • MockMvc 和 HtmlUnit:如果您,请使用此选项 想要使用原始的 HtmlUnit 库。
  • MockMvc 和 WebDriver:使用此选项 简化集成和端到端测试之间的开发并重用代码。
  • MockMvc 和 Geb:如果需要,请使用此选项 使用 Groovy 进行测试、简化开发以及在集成和 端到端测试。
MockMvc 和 HtmlUnit

本节介绍如何集成 MockMvc 和 HtmlUnit。如果需要,请使用此选项 以使用原始 HtmlUnit 库。

MockMvc 和 HtmlUnit 设置

首先,请确保已包含测试依赖项。为了将 HtmlUnit 与 Apache HttpComponents 一起使用 4.5+,您需要使用 HtmlUnit 2.18 或更高版本。​​net.sourceforge.htmlunit:htmlunit​

我们可以很容易地创建一个与MockMvc集成的HtmlUnit,如下所示:​​WebClient​​​​MockMvcWebClientBuilder​

WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}

这可确保引用服务器的任何 URL 都定向到我们的实例,而无需真正的 HTTP 连接。任何其他网址都是 像往常一样,使用网络连接请求。这使我们能够轻松测试 CDN。​​localhost​​​​MockMvc​

MockMvc 和 HtmlUnit 用法

现在我们可以像往常一样使用 HtmlUnit,但不需要部署我们的 应用程序到 Servlet 容器。例如,我们可以请求视图创建一个 包含以下内容的消息:

HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");

一旦我们有了参考,我们就可以填写表格并提交 以创建消息,如以下示例所示:​​HtmlPage​

HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();

最后,我们可以验证是否已成功创建新消息。以下 断言使用AssertJ库:

assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");

前面的代码以多种方式改进了我们的MockMvc 测试。 首先,我们不再需要显式验证我们的表单,然后创建一个请求 看起来像表单。相反,我们要求表格,填写并提交,从而 显著降低开销。

另一个重要因素是HtmlUnit。 使用 Mozilla Rhino 引擎来评估 JavaScript。这意味着我们也可以测试 JavaScript 在我们页面中的行为。

请参阅HtmlUnit 文档 有关使用 HtmlUnit 的其他信息。

高深​​MockMvcWebClientBuilder​

在到目前为止的示例中,我们以最简单的方式使用 可能,通过构建基于加载为我们 Spring TestContext Framework。以下示例中重复此方法:​​MockMvcWebClientBuilder​​​​WebClient​​​​WebApplicationContext​

WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}

我们还可以指定其他配置选项,如以下示例所示:

WebClient webClient;

@BeforeEach
void setup() {
webClient = MockMvcWebClientBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
}

作为替代方案,我们可以通过单独配置实例并将其提供给实例来执行完全相同的设置,如下所示:​​MockMvc​​​​MockMvcWebClientBuilder​

MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();

webClient = MockMvcWebClientBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();

这更冗长,但是,通过构建实例,我们有 MockMvc的全部力量触手可及。​​WebClient​​​​MockMvc​

MockMvc 和 WebDriver

在前面的部分中,我们已经看到了如何将 MockMvc 与 raw 结合使用 HtmlUnit API。在本节中,我们使用SeleniumWebDriver中的其他抽象来使事情变得更加容易。

为什么选择WebDriver和MockMvc?

我们已经可以使用 HtmlUnit 和 MockMvc,那么我们为什么要使用 WebDriver呢?这 Selenium WebDriver提供了一个非常优雅的API,让我们可以轻松地组织我们的代码。自 为了更好地展示它是如何工作的,我们将在本节中探讨一个示例。

假设我们需要确保正确创建消息。测试涉及发现 HTML 表单输入元素,填写它们,并进行各种断言。

这种方法会导致许多单独的测试,因为我们想测试错误条件 也。例如,我们希望确保如果我们只填写部分 窗体。如果我们填写整个表单,则应显示新创建的消息 之后。

如果其中一个字段被命名为“摘要”,我们可能会有类似于 在我们的测试中,以下在多个地方重复:

HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);

那么,如果我们改变theto会发生什么?这样做会迫使我们更新所有 我们的测试以纳入此更改。这违反了 DRY 原则,所以我们应该 理想情况下,将此代码提取到其自己的方法中,如下所示:​​id​​​​smmry​

public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
setSummary(currentPage, summary);
// ...
}

public void setSummary(HtmlPage currentPage, String summary) {
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
}

这样做可确保在更改 UI 时不必更新所有测试。

我们甚至可以更进一步,将这个逻辑放在一个 表示我们当前处于打开状态,如以下示例所示:​​Object​​​​HtmlPage​

public class CreateMessagePage {

final HtmlPage currentPage;

final HtmlTextInput summaryInput;

final HtmlSubmitInput submit;

public CreateMessagePage(HtmlPage currentPage) {
this.currentPage = currentPage;
this.summaryInput = currentPage.getHtmlElementById("summary");
this.submit = currentPage.getHtmlElementById("submit");
}

public <T> T createMessage(String summary, String text) throws Exception {
setSummary(summary);

HtmlPage result = submit.click();
boolean error = CreateMessagePage.at(result);

return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
}

public void setSummary(String summary) throws Exception {
summaryInput.setValueAttribute(summary);
}

public static boolean at(HtmlPage page) {
return "Create Message".equals(page.getTitleText());
}
}

以前,此模式称为页面对象模式。虽然我们 当然可以用HtmlUnit做到这一点,WebDriver提供了一些我们在 以下各节使此模式更易于实现。

模拟Mvc和WebDriver设置

要将Selenium WebDriver与Spring MVC测试框架一起使用,请确保您的项目 包括测试依赖项。​​org.seleniumhq.selenium:selenium-htmlunit-driver​

我们可以通过以下示例轻松创建与 MockMvc 集成的 Selenium WebDriver:​​MockMvcHtmlUnitDriverBuilder​

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}

前面的示例确保引用服务器的任何 URL 都是 定向到我们的实例,而无需真正的 HTTP 连接。任何其他 像往常一样,使用网络连接请求 URL。这使我们能够轻松测试 使用CDN。​​localhost​​​​MockMvc​

MockMvc 和 WebDriver 使用

现在我们可以像往常一样使用 WebDriver,但不需要部署我们的 应用程序到 Servlet 容器。例如,我们可以请求视图创建一个 包含以下内容的消息:

CreateMessagePage page = CreateMessagePage.to(driver);

​CreateMessagePage​​​扩展。我们不会详细介绍,但总而言之,它包含我们所有页面的通用功能。 例如,如果我们的应用程序具有导航栏、全局错误消息和其他 功能,我们可以将此逻辑放置在共享位置。AbstractPageAbstractPage

对于我们所在的 HTML 页面的每个部分,我们都有一个成员变量 感兴趣。这些是类型。WebDriver的PageFactory​让我们删除一个 来自 HtmlUnit 版本的大量代码通过自动解析 每。PageFactory#initElements(WebDriver,Class<T>)​方法通过使用字段名称并查找它来自动解析每个 通过 HTML 页面中的元素的理论。WebElementCreateMessagePageWebElementWebElementidname

我们可以使用@FindBy注释​来覆盖默认的查找行为。我们的示例展示了如何使用注释来查找带有选择器的提交按钮(input[type=submit])。@FindBycss

然后,我们可以填写表单并提交以创建消息,如下所示:

ViewMessagePage viewMessagePage =
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);

​CreateMessagePage​​​扩展。我们不会详细介绍,但总而言之,它包含我们所有页面的通用功能。 例如,如果我们的应用程序具有导航栏、全局错误消息和其他 功能,我们可以将此逻辑放置在共享位置。​​AbstractPage​​​​AbstractPage​

对于我们所在的 HTML 页面的每个部分,我们都有一个成员变量 感兴趣。这些是类型。WebDriver的PageFactory​让我们删除一个 来自 HtmlUnit 版本的大量代码通过自动解析 每。PageFactory#initElements(WebDriver,Class<T>)​方法通过使用字段名称并查找它来自动解析每个 通过 HTML 页面中的元素的理论。​​WebElement​​​​CreateMessagePage​​​​WebElement​​​​WebElement​​​​id​​​​name​

我们可以使用@FindBy注释来覆盖默认的查找行为。我们的示例展示了如何使用注释来查找带有选择器的提交按钮(input[type=submit])。@FindBycss

这通过利用页面对象模式改进了HtmlUnit 测试的设计。正如我们在为什么使用WebDriver和MockMvc?中提到的,我们可以使用页面对象模式。 使用HtmlUnit,但使用WebDriver要容易得多。请考虑以下实现:​​CreateMessagePage​

public class CreateMessagePage
extends AbstractPage {


private WebElement summary;
private WebElement text;


@FindBy(css = "input[type=submit]")
private WebElement submit;

public CreateMessagePage(WebDriver driver) {
super(driver);
}

public <T> T createMessage(Class<T> resultPage, String summary, String details) {
this.summary.sendKeys(summary);
this.text.sendKeys(details);
this.submit.click();
return PageFactory.initElements(driver, resultPage);
}

public static CreateMessagePage to(WebDriver driver) {
driver.get("http://localhost:9990/mail/messages/form");
return PageFactory.initElements(driver, CreateMessagePage.class);
}
}

最后,我们可以验证是否已成功创建新消息。以下 断言使用AssertJ断言库:

assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");

我们可以看到,我们允许我们与自定义域模型进行交互。为 例如,它公开了一个返回 aObject 的方法:​​ViewMessagePage​​​​Message​

public Message getMessage() throws ParseException {
Message message = new Message();
message.setId(getId());
message.setCreated(getCreated());
message.setSummary(getSummary());
message.setText(getText());
return message;
}

然后,我们可以在断言中使用丰富的域对象。

最后,我们不能忘记在测试完成后关闭实例, 如下:​​WebDriver​

@AfterEach
void destroy() {
if (driver != null) {
driver.close();
}
}

有关使用 WebDriver 的其他信息,请参阅 SeleniumWebDriver 文档。

高深​​MockMvcHtmlUnitDriverBuilder​

在到目前为止的示例中,我们以最简单的方式使用 可能,通过构建基于加载为我们 Spring TestContext Framework。此处重复此方法,如下所示:​​MockMvcHtmlUnitDriverBuilder​​​​WebDriver​​​​WebApplicationContext​

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}

我们还可以指定其他配置选项,如下所示:

WebDriver driver;

@BeforeEach
void setup() {
driver = MockMvcHtmlUnitDriverBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
}

作为替代方案,我们可以通过单独配置实例并将其提供给实例来执行完全相同的设置,如下所示:​​MockMvc​​​​MockMvcHtmlUnitDriverBuilder​

MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();

driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();

这更冗长,但是,通过构建实例,我们有 MockMvc的全部力量触手可及。​​WebDriver​​​​MockMvc​

MockMvc 和 Geb

在上一节中,我们看到了如何将MockMvc与WebDriver一起使用。在本节中,我们 使用Geb使我们的测试甚至 Groovy-er。

为什么选择Geb和MockMvc?

Geb 由 WebDriver 提供支持,因此它提供了许多与我们从中获得的相同好处。 网络驱动程序。但是,Geb 通过照顾一些 我们的样板代码。

模拟Mvc和Geb设置

我们可以轻松地使用使用MockMvc的Selenium WebDriver初始化Geb,因为 遵循:​​Browser​

def setup() {
browser.driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}

这确保了服务器的任何URL引用都被定向到我们的实例,而无需真正的HTTP连接。任何其他网址都是 正常使用网络连接请求。这使我们能够轻松测试 CDN。​​localhost​​​​MockMvc​

MockMvc 和 Geb 用法

现在我们可以像往常一样使用 Geb,但不需要将我们的应用程序部署到 一个 Servlet 容器。例如,我们可以请求视图创建带有 以后:

to CreateMessagePage

然后,我们可以填写表单并提交以创建消息,如下所示:

when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)

任何未找到的无法识别的方法调用或属性访问或引用都是 转发到当前页面对象。这删除了很多样板代码,我们 直接使用网络驱动程序时需要。

与直接使用WebDriver一样,这通过使用页面对象改进了HtmlUnit测试的设计。 模式。如前所述,我们可以将页面对象模式与 HtmlUnit 一起使用,并且 WebDriver,但使用Geb更容易。考虑我们新的基于 Groovy 的实现:​​CreateMessagePage​

class CreateMessagePage extends Page {
static url = 'messages/form'
static at = { assert title == 'Messages : Create'; true }
static content = {
submit { $('input[type=submit]') }
form { $('form') }
errors(required:false) { $('label.error, .alert-error')?.text() }
}
}

我们的扩展。我们不会详细介绍,但是,在 总之,它包含我们所有页面的通用功能。我们定义一个网址,其中 可以找到此页面。这让我们导航到该页面,如下所示:​​CreateMessagePage​​​​Page​​​​Page​

to CreateMessagePage

我们还有一个闭包来确定我们是否在指定的页面。它应该 返回如果我们在正确的页面上。这就是为什么我们可以断言我们在 正确的页面,如下所示:​​at​​​​true​

then:
at CreateMessagePage
errors.contains('This field is required.')

接下来,我们创建一个闭包,指定 页。我们可以使用jQuery-ish Navigator 用于​选择我们感兴趣的内容的 API。​​content​

最后,我们可以验证新消息是否已成功创建,如下所示:

then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

有关如何充分利用 Geb 的更多详细信息,请参阅The Book of Geb用户手册。

3.8. 测试客户端应用程序

可以使用客户端测试来测试内部使用的代码。这 想法是声明预期的请求并提供“存根”响应,以便您可以 专注于隔离测试代码(即,不运行服务器)。以下 示例显示了如何执行此操作:​​RestTemplate​

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());

// Test code that uses the above RestTemplate ...

mockServer.verify();

在前面的示例中,(客户端 REST 的中心类 测试)配置与自定义 根据预期断言实际请求并返回“存根”响应。在此 在这种情况下,我们期望一个请求并希望返回一个包含内容的 200 响应。我们可以将其他预期请求和存根响应定义为 需要。当我们定义预期请求和存根响应时,可以是 像往常一样在客户端代码中使用。在测试结束时,可以 用于验证是否已满足所有期望。​​MockRestServiceServer​​​​RestTemplate​​​​ClientHttpRequestFactory​​​​/greeting​​​​text/plain​​​​RestTemplate​​​​mockServer.verify()​

默认情况下,请求按声明期望的顺序进行。你 可以在构建服务器时设置该选项,在这种情况下,所有 检查期望(按顺序)以查找给定请求的匹配项。这意味着 允许按任何顺序提出请求。以下示例使用:​​ignoreExpectOrder​​​​ignoreExpectOrder​

server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();

即使默认情况下有无序请求,每个请求也只允许运行一次。 该方法提供了一个重载的变体,该变体接受指定计数范围的参数(例如,,,,,,等)。以下示例使用:​​expect​​​​ExpectedCount​​​​once​​​​manyTimes​​​​max​​​​min​​​​between​​​​times​

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());

// ...

mockServer.verify();

请注意,whenis 未设置(默认值),因此,请求 按声明顺序预期,则该顺序仅适用于任何 预期请求。例如,如果“/something”需要两次,后跟 “/somewhere”三次,那么应该有一个请求“/something”,然后才有 对“/某处”的请求,但是,除了随后的“/某物”和“/某处”, 请求可以随时提出。​​ignoreExpectOrder​

作为上述所有功能的替代方法,客户端测试支持还提供了可以配置到 ato 中的实现 将其绑定到实例。这允许使用实际的服务器端处理请求 逻辑,但不运行服务器。以下示例演示如何执行此操作:​​ClientHttpRequestFactory​​​​RestTemplate​​​​MockMvc​

MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...

3.8.1. 静态导入

与服务器端测试一样,用于客户端测试的流畅 API 需要一些静态 进口。这些很容易通过搜索找到。Eclipse 用户应添加 Java → Editor → 内容下 Eclipse 首选项中的“收藏夹静态成员” 协助→收藏夹。这允许在键入第一个字符后使用内容辅助 静态方法名称。其他 IDE(如 IntelliJ)可能不需要任何额外的 配置。检查对静态成员的代码完成的支持。​​MockRest*​​​​MockRestRequestMatchers.*​​​​MockRestResponseCreators.*​

3.8.2. 客户端 REST 测试的更多示例

Spring MVC 测试自己的测试包括示例 客户端 REST 测试的测试。

4. 更多资源

有关测试的详细信息,请参阅以下资源:

  • JUnit:“一个程序员友好的Java测试框架”。 由 Spring 框架在其测试套件中使用,并在Spring TestContext Framework 中受支持。
  • TestNG:受JUnit启发的测试框架,增加了支持 适用于测试组、数据驱动测试、分布式测试和其他功能。支持 在Spring TestContext Framework中
  • AssertJ: “Fluent assertions for Java”, 包括对 Java 8 lambda、流和其他功能的支持。
  • 模拟对象:维基百科中的文章。
  • MockObjects.com:专用于模拟对象的网站,一个 在测试驱动开发中改进代码设计的技术。
  • Mockito:基于Test Spy模式的Java模拟库。由 Spring 框架使用 在其测试套件中。
  • EasyMock:Java库“提供模拟对象 接口(和对象通过类扩展)通过使用动态生成它们 Java的代理机制。
  • JMock:支持Java代码测试驱动开发的库 与模拟对象。
  • DbUnit:JUnit扩展(也可以与Ant和Maven一起使用) 面向数据库驱动的项目,并将您的数据库放入 测试运行之间的已知状态。
  • Testcontainers:支持 JUnit 的 Java 库 tests, 提供通用数据库的轻量级、一次性实例, Selenium web 浏览器,或可以在 Docker 容器中运行的任何其他内容。
  • The Grinder:Java负载测试框架。
  • SpringMockK:支持 Spring Boot 用 Kotlin 编写的集成测试使用MockK 而不是 Mockito。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK