Java进击框架:Spring-Test(六)

前言

Spring团队提倡测试驱动开发(TDD)。Spring团队发现,控制反转(IoC)的正确使用确实使单元和集成测试变得更容易(因为类上setter方法和适当构造函数的存在使它们更容易在测试中连接在一起,而不必设置服务定位器注册中心和类似的结构)。

Spring TestContext框架(位于org.springframework.test.context包中)提供了通用的、注释驱动的单元测试和集成测试支持,与所使用的测试框架无关。TestContext框架还非常重视约定而不是配置,使用合理的默认值,您可以通过基于注释的配置来覆盖这些默认值。

该框架的核心由TestContextManager类和TestContextTestExecutionListenerSmartContextLoader接口组成。

  • TestContextManagerSpring TestContext框架的主要入口点,负责管理单个TestContext并且向每个注册的TestExecutionListener在明确定义的测试执行点,比如:执行测试方法之前、测试方法之后。

  • TestContext封装测试运行的上下文(不知道实际使用的测试框架),并为它所负责的测试实例提供上下文管理和缓存支持。

  • TestExecutionListener定义API,用于对TestContextManager侦听器向其注册。

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class MyServiceTest {
    @Test
    public void test() throws Exception {
        TestContextManager testContextManager = new TestContextManager(MyServiceTest.class);
        testContextManager.prepareTestInstance(this);

        TestContext testContext = testContextManager.getTestContext();
    }
}

@RunWithJUnit 框架提供的一个注解,用于指定测试类运行时使用的运行器(Runner)。在 Spring 中,通常会使用 SpringRunner 运行器来执行基于 Spring 的测试。它是 JUnit 4 的默认运行器,在 JUnit 5 中称为 SpringJUnit4ClassRunner

上述示例代码中,我们创建了一个 TestContextManager 对象,并调用 prepareTestInstance(this) 方法来准备测试实例和测试上下文。通过 getTestContext() 方法获取当前测试的 TestContext 对象,我们可以使用它来操作测试上下文。

以下内容中会用的一些依赖:

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.23</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.23</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.23</version>
        </dependency>
        <!--junit 4-->
		<dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>compile</scope>
        </dependency>
        <!--junit 5-->
		<dependency>
		    <groupId>org.junit.jupiter</groupId>
		    <artifactId>junit-jupiter</artifactId>
		    <version>5.8.2</version>
		    <scope>test</scope>
		</dependency>
        <dependency>
            <groupId>org.easymock</groupId>
            <artifactId>easymock</artifactId>
            <version>4.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
            <version>2.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.4.0</version>
        </dependency>

单元测试

与传统的J2EE / Java EE开发相比,依赖注入应该使您的代码对容器的依赖性更小。组成应用程序的POJOsJUnitTestNG测试中应该是可测试的,使用new操作器,不带Spring或任何其他容器。如果您遵循Spring的架构建议,那么您的代码库的干净分层和组件化有助于更容易的单元测试。例如,您可以通过存根或模仿DAO或存储库接口来测试服务层对象,而无需在运行单元测试时访问持久数据。

模拟对象

Spring包含许多专门用于mock的包。

  • 环境

org.springframework.mock.env包包含了EnvironmentPropertySource抽象的模拟实现。MockEnvironmentMockPropertySource对于开发依赖于环境特定属性的代码的容器外测试非常有用。

下面讲解这两种的用法,示例代码如下:

public class MyServiceTest {

    @Test
    public void testGetValue() {
        ConfigurableEnvironment mockEnv = new MockEnvironment()
                .withProperty("my.property", "mocked value");
        MyService myService = new MyService();
        myService.setConfigurableEnvironment(mockEnv);
        System.out.println(myService.getValue());
        //断言返回的值与预期值相匹配
        assertEquals("mocked value2", myService.getValue());
    }
}
public class MyService {
    private String value;

    private ConfigurableEnvironment configurableEnvironment;

    public void setConfigurableEnvironment(ConfigurableEnvironment configurableEnvironment) {
        this.configurableEnvironment = configurableEnvironment;
    }

    public String getValue() {
        return configurableEnvironment.getProperty("my.property");
    }
}

创建了一个名为 MyService 的简单服务类,它依赖于环境变量或属性值。MyService 类中的 getValue() 方法使用了 SpringConfigurableEnvironment 对象来获取属性值。在测试中,我们使用 MockEnvironment 来模拟环境配置,并通过 setProperty() 方法设置了属性 "my.property" 的值为 "mocked value"

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
@TestPropertySource(properties = "my.property=mocked value")
public class MyServiceTest {
    @Autowired
    private MyService myService;

    @Test
    public void testGetValue() {
        String value = myService.getValue();
        //断言返回的值与预期值相匹配
        assertEquals("mocked value", value);
    }
}
public class MyService {
    @Value("${my.property}")
    private String myProperty;

    public String getValue() {
        return myProperty;
    }
}
@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyService();
    }
}

通过 @TestPropertySource 注解将 MockPropertySource 添加到测试环境中,从而完成测试。需要注意的是,@TestPropertySource 注解会覆盖项目中已存在的属性源,因此在设置虚拟属性值时要确保不会意外地覆盖了其他属性。

  • JNDI

JNDIJava 平台提供的一种标准接口,用于访问命名和目录服务。在开发应用程序时,有时需要通过 JNDI 来获取外部资源,例如数据库连接池、消息队列等。为了方便测试这些依赖于 JNDI 的代码,可以使用 org.springframework.mock.jndi 工具包提供的模拟对象。从Spring Framework 5.2开始,软件包被正式弃用,取而代之的是来自第三方的完整解决方案,例如Simple-JNDI

  • Servlet API

org.springframework.mock.web包包含一套全面的Servlet API模拟对象,这些对象对于测试web上下文、控制器和过滤器非常有用。这些模拟对象旨在与SpringWeb MVC框架一起使用,通常比动态模拟对象(例如EasyMock)或替代的Servlet API模拟对象(例如模拟对象)。

示例代码如下:

public class MyServiceTest {
    @Test
    public void testAdd() {
        // 创建模拟对象
        CalculatorService mockService = createMock(CalculatorService.class);

        // 设置模拟对象的期望行为
        expect(mockService.add(2, 3)).andReturn(5);

        // 将模拟对象注入到被测试类中
        Calculator calculator = new Calculator(mockService);

        // 对受测方法进行测试
        replay(mockService);
        int result = calculator.add(2, 3);

        // 验证期望行为是否正确响应
        verify(mockService);
        assertEquals(5, result);
    }
}
public interface CalculatorService {
    int add(int a, int b);
}
public class Calculator {
    private CalculatorService service;

    public Calculator(CalculatorService service) {
        this.service = service;
    }

    public int add(int a, int b) {
        return service.add(a, b);
    }
}

Spring MVC测试框架建立在模拟Servlet API对象的基础上,为Spring MVC提供了一个集成测试框架。参考MockMvc

  • Spring Web响应式

org.springframework.mock.http.server.reactive包包含了WebFlux应用中使用的ServerHttpRequestServerHttpResponse的模拟实现。org.springframework.mock.web.server包包含一个模拟ServerWebExchange,它依赖于那些模拟请求和响应对象。

MockServerHttpRequestMockServerHttpResponse都从相同的抽象基类扩展为特定于服务器的实现,并与它们共享行为。

MockServerHttpRequest示例代码如下:

public class MyServiceTest {
    @Test
    public void testAdd() {
        // 创建一个GET请求
        MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
                .header("Content-Type", "application/json")
                .queryParam("page", "1")
                .build();
    }
}

MockServerHttpResponse示例代码如下:

public class MyServiceTest {
    @Test
    public void testAdd() throws UnsupportedEncodingException {
        // 创建一个MockHttpServletResponse对象
        MockHttpServletResponse response = new MockHttpServletResponse();
        // 设置响应状态码
        response.setStatus(HttpServletResponse.SC_OK);
        // 设置响应头
        response.setHeader("Content-Type", "application/json");
        // 设置响应体内容
        String responseBody = "{\"message\": \"Hello, world!\"}";
        response.setContentLength(responseBody.length());
        response.getWriter().write(responseBody);
        // 获取响应信息
        int statusCode = response.getStatus();
        String contentType = response.getHeader("Content-Type");
        String responseBody2 = response.getContentAsString();
    }
}

Spring包含了许多有助于单元测试的类。它们分为两类:

  • 通用测试工具

org.springframework.test.util软件包包含几个通用的工具,用于单元和集成测试。

AopTestUtilsAOP相关实用方法的集合。您可以使用这些方法获得对隐藏在一个或多个Spring代理后面的底层目标对象的引用。

public class MyServiceTest {
    @Test
    public void testAdd() {
        //获取代理对象背后的最终目标对象,即被代理的真实对象。
        Object target = AopTestUtils.getUltimateTargetObject(MyAspect.class);
        System.out.println(target);
        // 获取代理对象的目标对象,即被代理的对象(可能是最终目标对象或者另一个代理对象)。
        Object target2 = AopTestUtils.getTargetObject(MyAspect.class);
        System.out.println(target2);
    }
}

ReflectionTestUtils是基于反射的实用方法的集合。您可以在需要更改常数值、设置private字段,调用一个privatesetter()方法,或者调用一个private测试应用程序代码的用例时的配置或生命周期回调方法。

public class MyServiceTest {
    @Test
    public void testAdd() {
        MyService myService = new MyService();
        // 使用ReflectionTestUtils设置私有字段的值
        ReflectionTestUtils.setField(myService, "value", "hello world");
        // 使用ReflectionTestUtils调用私有方法并检查返回值
        Object actualValue = ReflectionTestUtils.invokeMethod(myService, "getValue");
        System.out.println(actualValue);

    }
}
public class MyService {
    private String value;

    private String getValue() { return value; }

    public void setValue(String value) { this.value = value; }
}

TestSocketUtils可用于在可用的随机端口上启动外部服务器的集成测试。然而,这些实用程序不能保证给定端口的后续可用性,因此是不可靠的。而不是使用TestSocketUtils要为服务器找到可用的本地端口,建议您依靠服务器的能力,在它选择或由操作系统分配的随机临时端口上启动。要与该服务器交互,您应该查询服务器当前使用的端口。

  • Spring MVC测试实用程序

org.springframework.test.web包装包含ModelAndViewAssert,您可以将它与JUnitTestNG或任何其他处理Spring MVC的单元测试框架结合使用ModelAndView对象。

public class MyServiceTest {
    @Test
    public void test() {
        ModelAndView modelAndView = new ModelAndView("myView");
        //使用assertViewName方法断言视图名称是否为"myView"。
        assertViewName(modelAndView, "myView");
    }
}

集成测试

能够在不需要部署到应用服务器或连接到其他企业基础设施的情况下执行一些集成测试是很重要的。这样做可以让您测试以下内容:

  • Spring IoC容器上下文的正确连接

  • 使用JDBC或ORM工具进行数据访问。这包括SQL语句的正确性、Hibernate查询、JPA实体映射等等

Spring的集成测试支持有以下主要目标:

  • 管理测试之间的Spring IoC容器缓存
  • 提供测试fixture实例的依赖注入
  • 提供适合集成测试的事务管理
  • 提供特定于spring的基类,帮助开发人员编写集成测试

上下文管理和缓存

Spring TestContext框架提供了一致的Spring加载ApplicationContext实例和WebApplicationContext实例以及这些上下文的缓存。支持加载上下文的缓存很重要,因为启动时间会成为一个问题——不是因为Spring本身的开销,而是因为Spring容器实例化的对象需要时间来实例化。例如,一个包含50到100个Hibernate映射文件的项目可能需要10到20秒来加载映射文件,在每个测试设备中运行每个测试之前产生的成本会导致整个测试运行变慢,从而降低开发人员的工作效率。

默认情况下,一旦加载,配置的ApplicationContext将为每个测试重用。因此,每个测试套件只产生一次设置成本,并且随后的测试执行速度要快得多。在这种情况下,“测试套件”一词意味着所有测试都在同一个JVM中运行——例如,所有测试都是从给定项目或模块的Ant、MavenGradle构建中运行的。在不太可能的情况下,测试破坏了应用程序上下文并需要重新加载(例如,通过修改bean定义或应用程序对象的状态),TestContext框架可以配置为在执行下一个测试之前重新加载配置并重新构建应用程序上下文。

  • 使用XML资源的上下文配置

使用TestContext框架的测试类不需要扩展任何特定的类或实现特定的接口来配置它们的应用程序上下文。相反,通过声明@ContextConfiguration类级别的注释。默认情况下,则配置的ContextLoader确定如何从默认位置或默认配置类加载上下文。除了上下文资源位置和组件类,应用程序上下文也可以通过应用程序上下文初始化器来配置。

<?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">
    <bean id="myService" class="com.example.MyService"></bean>
</beans>
public class MyService {
}
@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:dao.xml")
public class MyServiceTest {

    @Autowired
    MyService myService;

    @Test
    public void test(){
        System.out.println(myService);
    }

}

如果您@ContextConfiguration省略了注释中的locationsvalue这两个属性,TestContext框架试图检测一个默认的XML资源位置。如果您的类被命名为com.example.MyServiceTest GenericXmlContextLoader加载应用程序上下文"classpath:com/example/MyServiceTest-context.xml"

在这里插入图片描述

  • 使用组件类进行上下文配置

你可以用@ContextConfiguration注释加载组件类,并用一个包含对组件类引用的数组来配置classes属性。

术语“组件类”可以指以下任何一种:

  • 用注释的类@Configuration
  • 一个组件(即一个用@Component, @Service, @Repository或者其他原型注释)。
  • 一个符合JSR 330标准的类,用jakarta.inject注释。
  • 任何包含以下内容的类@Bean-方法。
  • 任何其他打算注册为Spring组件的类(例如ApplicationContext),潜在地利用了单个构造函数的自动装配,而不使用Spring注释。
public class MyService {}
@Configuration
public class AppConfig {

    @Bean
    public MyService myService(){
        return new MyService();
    }
}
@RunWith(value = SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class MyServiceTest {

    @Autowired
    MyService myService;

    @Test
    public void test(){
        System.out.println(myService);
    }
}
  • 带有上下文初始化器的上下文配置

@ContextConfiguration 注解中的 initializers 属性用于指定要在测试上下文加载之前执行的初始化回调对象(ApplicationContextInitializer),该数组包含对实现ApplicationContextInitializer

public class MyApplicationContextInitializer 
        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        System.out.println("init");
    }
}
@RunWith(value = SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class,initializers = MyApplicationContextInitializer.class)
public class MyServiceTest {

    @Autowired
    MyService myService;

    @Test
    public void test(){
        System.out.println(myService);
    }
    /** Output:
     *  init
     *  com.example.MyService@76f2bbc1
     */
}
  • 上下文配置继承

@ContextConfiguration支持布尔值inheritLocationsinheritInitializers指示是否应该继承由超类声明的资源位置或组件类和上下文初始值设定项的属性。两个标志的默认值都是true;设置为false时,子类必须自行定义属性值,否则会抛出 IllegalStateException 异常。

@ContextConfiguration(locations = "classpath:MyServiceTest-context.xml")
public class BaseTest {
}
@RunWith(value = SpringRunner.class)
@ContextConfiguration(inheritLocations = true)
public class MyServiceTest extends BaseTest{

    @Autowired
    MyService myService;

    @Test
    public void test(){
        System.out.println(myService);
    }
    /** Output:
     *  com.example.MyService@76f2bbc1
     */
}
  • 环境配置文件的上下文配置

Spring框架对环境和概要文件(又名“bean定义概要文件”)的概念具有一流的支持,并且可以配置集成测试来为各种测试场景激活特定的bean定义概要文件。这是通过用@ActiveProfiles注释一个测试类,并提供一个应该在为测试加载ApplicationContext时激活的配置文件列表来实现的。

<?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">

    <beans profile="dev">
        <bean id="myService" class="com.example.MyService">
            <property name="value" value="dev"></property>
        </bean>
    </beans>
    <beans profile="prod">
        <bean id="myService" class="com.example.MyService">
            <property name="value" value="prod"></property>
        </bean>
    </beans>
</beans>
public class MyService {
    private String value;

    public String getValue() { return value; }

    public void setValue(String value) { this.value = value; }
}
@RunWith(value = SpringRunner.class)
@ContextConfiguration
@ActiveProfiles(value = "prod")
public class MyServiceTest{

    @Autowired
    MyService myService;

    @Test
    public void test(){
        System.out.println(myService.getValue());
    }
    /** Output:
     *  prod
     */
}

也可以使用注解的形式,示例代码如下:

@Configuration
public class AppConfig {
    @Bean
    @Profile("dev")
    public MyService myServiceDev(){
        MyService myService = new MyService();
        myService.setValue("dev");
        return myService;
    }
    @Bean
    @Profile("prod")
    public MyService myServiceProd(){
        MyService myService = new MyService();
        myService.setValue("prod");
        return myService;
    }
}
@RunWith(value = SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
@ActiveProfiles(value = "prod")
public class MyServiceTest{

    @Autowired
    MyService myService;

    @Test
    public void test(){
        System.out.println(myService.getValue());
    }
    /** Output:
     *  prod
     */
}
  • 带有测试属性源的上下文配置

您可以声明@TestPropertySource注释来声明测试属性文件或内联属性的资源位置。

name=hello

@RunWith(value = SpringRunner.class)
@TestPropertySource(locations = "/test.properties")
public class MyServiceTest{

    @Value("${name}")
    String name;

    @Test
    public void test(){
        System.out.println(name);
    }
    /** Output:
     *  hello
     */
}

通过使用@TestPropertySourceproperties属性,可以以键值对的形式配置内联属性,如下面的示例所示:

@RunWith(value = SpringRunner.class)
@TestPropertySource(locations = "/test.properties",properties = {"name:world"})
public class MyServiceTest{

    @Value("${name}")
    String name;

    @Test
    public void test(){
        System.out.println(name);
    }
    /** Output:
     *  world
     */
}

支持的语法结构有:key=valuekey:valuekey value

如果@TestPropertySource被声明为空批注(也就是说,没有locations或者properties属性),如果带注释的测试类是com.example.MyTest,对应的默认属性文件是classpath:com/example/MyTest.properties。如果无法检测到默认值,则引发IllegalStateException被抛出。

  • 具有动态属性源的上下文配置

Spring Framework 5.2.5开始,TestContext框架支持动态的属性通过@DynamicPropertySource注释。

@RunWith(value = SpringRunner.class)
@TestPropertySource(locations = "/test.properties",properties = {"name:world","name=world2"})
public class MyServiceTest{

    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        registry.add("name", ()->"张山");
    }

    @Value("${name}")
    String name;

    @Test
    public void test(){
        System.out.println(name);
    }
    /** Output:
     *  张山
     */
}

动态属性的优先级高于从@TestPropertySource操作系统的环境、Java系统属性或由应用程序通过使用@PropertySource或者以编程方式。

  • 上下文缓存

一旦TestContext框架为一个测试加载了一个ApplicationContext(或WebApplicationContext),这个context就会被缓存并重用给所有在同一个测试套件中声明了相同唯一的context配置的后续测试。

Spring TestContext框架将应用程序上下文存储在静态缓存中。这意味着上下文实际上存储在一个静态变量中。换句话说,如果测试在单独的进程中运行,则在每次测试执行之间清除静态缓存,这将有效地禁用缓存机制。

为了从缓存机制中获益,所有测试必须在相同的进程或测试套件中运行。

  • 上下文层次结构

当编写依赖加载弹簧的集成测试时ApplicationContext通常,针对单个上下文进行测试就足够了。在测试中,有时您需要创建多个嵌套的应用程序上下文,以便模拟复杂的应用程序结构或依赖关系。@ContextHierarchy 注解允许您按层次结构组织这些应用程序上下文。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextHierarchy({
    @ContextConfiguration("classpath:context1.xml"),
    @ContextConfiguration("classpath:context2.xml")
})
public class MyTest {
    // 测试内容
}

我们使用 @ContextHierarchy 注解来定义两个层次的应用程序上下文。第一个层次的上下文由 context1.xml 文件定义,第二个层次的上下文由 context2.xml 文件定义。这意味着 context2.xml 中的 bean 可以访问 context1.xml 中定义的 bean

通过使用 @ContextHierarchy 注解,您可以在测试中创建多个层次的应用程序上下文,并且它们之间可以相互访问和共享 bean。这在某些复杂的测试场景下非常有用。

事务管理

TestContext框架中,事务由TransactionalTestExecutionListener管理,它是默认配置的,即使您没有在测试类上显式声明@TestExecutionListeners。此外,您必须在类或方法级别为您的测试声明Spring@Transactional注释。

TransactionalTestExecutionListener期望在Spring ApplicationContext中为测试定义一个PlatformTransactionManager bean 。如果在测试的ApplicationContext中有多个PlatformTransactionManager的实例,你可以通过使用@Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr")来声明一个限定符,或者TransactionManagementConfigurer可以通过@Configuration类来实现。

使用 XML 配置:

<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

使用 Java 配置:

@Configuration
public class TransactionConfig {
    @Autowired
    private DataSource dataSource;

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource);
    }
}
  • 启用和禁用事务

@Transactional注释测试方法会导致测试在事务中运行,默认情况下,该事务在测试完成后自动回滚。如果用@Transactional注释了测试类,则该类层次结构中的每个测试方法都在事务中运行。没有使用@Transactional注释的测试方法(在类或方法级别)不会在事务中运行。请注意,@Transactional不支持测试生命周期方法——例如,用JUnit Jupiter@BeforeAll@BeforeEach等注释的方法。此外,带有@Transactional注释但将传播属性设置为NOT_SUPPORTEDNEVER的测试不会在事务中运行。

@Transactional属性支持:

属性支持测试管理的事务
value和transactionManager
propagation仅仅Propagation.NOT_SUPPORTEDPropagation.NEVER受到支持
isolation
timeout
readOnly
rollbackFor和rollbackForClassName否:使用TestTransaction.flagForRollback()代替
noRollbackFor和noRollbackForClassName否:使用TestTransaction.flagForCommit()代替
  • 事务回滚和提交行为

默认情况下,测试事务将在测试完成后自动回滚;但是,事务性提交和回滚行为可以通过@Commit@Rollback注释。

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MyTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    @Commit
    public void testCommit() {
        User user = new User();
        user.setUsername("test");
        user.setPassword("123456");
        userRepository.save(user);
        assertThat(userRepository.findByUsername("test")).isNotNull();
    }

    @Test
    @Rollback
    public void testRollback() {
        User user = new User();
        user.setUsername("test");
        user.setPassword("123456");
        userRepository.save(user);
        assertThat(userRepository.findByUsername("test")).isNotNull();
    }
}

您可以在测试方法中、方法之前和方法之后使用TestTransaction来启动或结束当前测试管理的事务,或者为回滚或提交配置当前测试管理的事务。只要启用了TransactionalTestExecutionListener,对TestTransaction的支持就自动可用。

@RunWith(SpringRunner.class)
public class MyTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    public void testCommit() {
        // 开启事务
        TestTransaction.start();
        try {
            User user = new User();
            user.setUsername("test");
            user.setPassword("123456");
            userRepository.save(user);
            // 标记事务为回滚状态
            TestTransaction.flagForRollback();
            // 断言事务已标记为回滚状态
            assertTrue(TestTransaction.isFlaggedForRollback());
            // 结束事务并验证数据库状态
            TestTransaction.end();
            // 可以进行其他断言或验证操作
        } finally {
            // 重置事务状态,确保下一个测试方法开始时的事务状态正常
            TestTransaction.flagForCommit();
            TestTransaction.end();
        }
    }
}

有时,您可能需要在事务性测试方法之前或之后但在事务性上下文之外运行某些代码,TransactionalTestExecutionListener支持@BeforeTransaction@AfterTransaction针对此类场景的注释。

@RunWith(SpringRunner.class)
@Transactional
public class MyTest {
    @Autowired
    private UserRepository userRepository;

    @BeforeTransaction
    public void beforeTransaction() {
        // 在事务开启前操作
    }

    @AfterTransaction
    public void afterTransaction() {
        // 在事务回滚或提交后操作
    }

    @Test
    public void testSaveUser() {
        // 测试方法中省略具体测试逻辑
    }
}

集成测试的支持类

Spring TestContext框架提供了几个abstract支持简化集成测试编写的类。这些基本测试类提供了测试框架中定义良好的钩子,以及方便的实例变量和方法,让您可以访问:

  • ApplicationContext,用于执行显式bean查找或测试上下文的整体状态。

  • 一个JdbcTemplate,用于执行SQL语句来查询数据库。您可以在执行与数据库相关的应用程序代码之前和之后使用这样的查询来确认数据库状态,并且Spring确保这样的查询与应用程序代码在相同的事务范围内运行。当与ORM工具一起使用时,一定要避免误报。

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

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { AppConfig.class })
public class MyTest {
    @Autowired
    private MyService myService;

    @Test
    public void testMyMethod() {
        // 测试代码
    }
}

执行SQL脚本

Spring提供了以下选项,用于在集成测试方法中以编程方式执行SQL脚本。

  • org.springframework.jdbc.datasource.init.ScriptUtils:提供了一组用于处理SQL脚本的静态实用工具方法,主要供框架内部使用。
  • org.springframework.jdbc.datasource.init.ResourceDatabasePopulator:提供基于对象的API,通过使用外部资源中定义的SQL脚本以编程方式填充、初始化或清理数据库。
  • org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
  • org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
    @Test
    public void test() {
        // 创建 ResourceDatabasePopulator 实例
        ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
        // 添加 SQL 脚本文件资源
        populator.addScript(new ClassPathResource("data.sql"));
        // 执行填充操作
        populator.execute(dataSource);
    }

除了上述以编程方式运行SQL脚本的机制之外,您可以声明@Sql测试类或测试方法上的注释,用于配置单个SQL语句或SQL脚本的资源路径,这些SQL脚本应在集成测试方法之前或之后针对给定数据库运行。

@RunWith(value = SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
@Sql(scripts = "create_table.sql")
public class MyServiceTest {

    @Test
    @Sql(scripts = "data.sql",statements = "需要执行的sql语句",
    		executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    public void test() {
    }
}
  • statements 属性:添加一条新的sql。
  • executionPhase属性:用于指定 SQL 执行的阶段,默认情况下,executionPhase 的值为 BEFORE_TEST_METHOD,即在每个测试方法执行之前执行 SQL 脚本。除此之外还有一些其它值:AFTER_TEST_METHOD(测试方法执行之后执行),BEFORE_TEST_CLASS(测试类的所有测试方法之前执行),AFTER_TEST_CLASS(测试类的所有测试方法之后执行)。

如果没有指定SQL脚本或语句,则尝试检测default脚本,取决于位置@Sql已声明。如果无法检测到默认值,则引发IllegalStateException被抛出。

  • 类级声明:如果带注释的测试类是com.example.MyTest,相应的默认脚本是classpath:com/example/MyTest.sql

  • 方法级声明:如果带注释的测试方法被命名为testMethod()并且在类中定义com.example.MyTest,相应的默认脚本是classpath:com/example/MyTest.testMethod.sql

  • sql分组

@SqlGroup 注解可以用于对多个 @Sql 注解进行分组,从而方便地在测试类或测试方法上进行统一配置。

@RunWith(value = SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
@SqlGroup({@Sql(scripts = "create_table.sql",config = @SqlConfig(commentPrefix = "#")),
            @Sql(scripts = "drop_table.sql")})
public class MyServiceTest {
}
  • 解释sql

@SqlConfig 是一个用于配置 @Sql 注解行为的注解。它可以定义一些与 SQL 脚本执行相关的参数,例如分隔符、错误处理等。

@SqlConfig 注解不限于以下属性:

  • separator:指定 SQL 脚本中的语句分隔符,默认为 ";"。你可以根据自己的需要将其设置为其他分隔符,例如 "$""$$"
  • commentPrefix:指定 SQL 脚本中的注释前缀,默认为 "--"。如果你的 SQL 脚本使用了其他注释前缀,可以在此处进行配置。
  • errorMode:指定 SQL 脚本执行过程中遇到错误时的处理方式,默认为 ErrorMode.FAIL_ON_ERROR。可以选择的处理方式包括 FAIL_ON_ERRORCONTINUE_ON_ERRORHALT_ON_ERROR,分别表示在遇到错误时抛出异常、继续执行脚本但记录错误、或者停止执行脚本并记录错误。
@RunWith(value = SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
@Sql(scripts = "create_table.sql",config = @SqlConfig(commentPrefix = "#"))
public class MyServiceTest {
}
  • 合并sql

Spring Framework 5.2开始,可以合并方法级@Sql具有类级声明的声明,使用@SqlMergeMode 注解。

@RunWith(value = SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
@SqlMergeMode(value = SqlMergeMode.MergeMode.MERGE)
public class MyServiceTest {
}

要启用@Sql合并,请用@SqlMergeMode(MERGE)注释测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,您可以通过@SqlMergeMode(OVERRIDE)切换回默认模式。

WebTestClient

WebTestClient是为测试服务器应用程序而设计的HTTP客户端。它包裹着春天的网络客户端并使用它来执行请求,但公开了用于验证响应的测试外观。WebTestClient可用于执行端到端HTTP测试。它还可以通过模拟服务器请求和响应对象,在没有运行服务器的情况下测试Spring MVCSpring WebFlux应用程序。

  • 绑定到控制器

这种设置允许您通过模拟请求和响应对象测试特定的控制器,而无需运行服务器。

对于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

@RunWith(SpringRunner.class)
@WebAppConfiguration
public class MyServiceTest {
    @Autowired
    private WebApplicationContext webApplicationContext;
    @Test
    public void test() {
        WebTestClient client = MockMvcWebTestClient.bindToApplicationContext(webApplicationContext).build();
    }
}

如果你使用ApplicationContext进行绑定,推荐使用@RunWith(SpringRunner.class)@WebAppConfiguration,启动你的测试用例,防止意外的错误。

绑定到路由器功能:

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

绑定到服务器:

WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
  • 客户端配置
@WebFluxTest
public class MyServiceTest {
    @Test
    public void test() {
        WebTestClient build = WebTestClient.bindToController(new TestController())
                .configureClient()
                .baseUrl("/test")
                .build();
    }
}

我们使用 configureClient() 方法为 WebTestClient 实例进行配置,并通过 baseUrl() 方法设置基本 URL。最后,使用 build() 方法创建 WebTestClient 对象并将其分配给 build 变量。

  • 执行请求

WebTestClient提供了一个与WebClient相同的API,直到使用exchange()执行请求。在调用exchange()之后,WebTestClientWebClient中分离出来,继续使用工作流来验证响应。

@WebFluxTest
public class MyServiceTest {
    @Test
    public void test() {
        WebTestClient build = WebTestClient.bindToController(new TestController())
                .configureClient()
                .baseUrl("/test")
                .build();
        WebTestClient.ResponseSpec response = build
                .get()//请求方式
                .uri("/method")//请求地址
                .exchange();//执行请求
    }
}

除此之外还有其他的方法,设置请求参数、请求头,示例代码如下:

@WebFluxTest
public class MyServiceTest {
    @Test
    public void test() {
        WebTestClient build = WebTestClient.bindToController(new TestController())
                .configureClient()
                .baseUrl("/test")
                .build();
        WebTestClient.ResponseSpec response = build
                .post()//请求方式
                .uri("/method")//请求地址
                .bodyValue(new Object())//设置请求参数
                .accept(MediaType.APPLICATION_JSON)//设置请求头格式
                .exchange()//执行请求
                ;
    }
}
  • 响应处理

我们还可以使用断言来进行响应的判断

@WebFluxTest
public class MyServiceTest {
    @Test
    public void test() {
        WebTestClient build = WebTestClient.bindToController(new TestController())
                .configureClient()
                .baseUrl("/test")
                .build();
        List response = build
                .post()
                .uri("/method")
                .exchange()
                .expectStatus().isOk()//断言方式,用于验证期望的 HTTP 响应状态码是否为 200(OK)。除此之外还有其他比如404、500等
                .expectHeader().contentType(MediaType.APPLICATION_JSON)//设置响应头格式
                .expectBody(List.class).consumeWith(result->{//响应体的内容
                    //如果内置断言不够,您可以使用该对象并执行任何其他断言:
                })//
                .returnResult()//将响应结果转换为 EntityExchangeResult 对象,可以通过该对象获取响应状态码、响应头、响应体等信息
                .getResponseBody()//获取实体对象
                ;
    }
}

expectStatus()还有其他的一些用途,如下:

.isOk():断言状态码是否为 200。
.isNotFound():断言状态码是否为 404。
.is5xxServerError():断言状态码是否为 5xx(服务器错误)。
.value(int expectedStatus):断言状态码是否等于 expectedStatus。

expectBody()方法还有其他的一些用途,如下:

expectBody().isEmpty():验证响应体是否为空。
expectBody().json(String expectedJson):验证响应体的 JSON 内容是否与预期一致。
expectBody().jsonPath(String expression, Object... args):使用 JSONPath 表达式来验证响应体中的字段值。
expectBody().isEqualTo(Object expectedObject):验证响应体是否与预期对象完全相等。

如果您想忽略响应内容,可以使用.expectBody(Void.class)上述代码都是经过测试成功跑通的示例,不同的注解有点区别

MockMvc

Spring MVC测试框架,也称为MockMvc,旨在为Spring MVC控制器提供更完整的测试,而无需运行服务器。Spring -test模块在没有运行服务器的情况下复制了完整的Spring MVC请求处理。

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

要设置MockMvc来测试一个特定的控制器,使用以下代码:

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

MockMvcBuildersSpring MVC Test框架提供的工厂类,用于创建MockMvc实例。

你也可以使用ApplicationContext

@RunWith(SpringRunner.class)
@WebAppConfiguration
public class MyServiceTest {
    @Autowired
    private WebApplicationContext webApplicationContext;
    @Test
    public void test() {
        MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }
}

如果你使用ApplicationContext进行启动,推荐使用@RunWith(SpringRunner.class)@WebAppConfiguration,启动你的测试用例,防止意外的错误。

  • 执行请求
@WebFluxTest
public class MyServiceTest {
    @Test
    public void test() {
        //standaloneSetup是一个静态方法可以直接调用standaloneSetup(new TestController()).build();
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController()).build();
        try {
            mockMvc.perform(post("/test/method/{id}",1)//请求方式:get()、post(),方法中第二参数可以传参
                    .accept(MediaType.APPLICATION_JSON)//请求头:除了accept()方法还有contentType()方法、header()方法都可以设置请求头
                    .param("name","test")//传参指定字段
                    .content("{\"name\": \"test\", \"age\": 18}")//传参,json格式
            )
            ;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

通过上述示例,我们可以知道,传参方式有三种,可以在请求方法中添加第二个参数、也可以调用.param()方法或者.content()方法。

  • 响应处理

我们可以对响应进行一些处理,andExpect()方法来验证模型中是否存在错误属性。

@WebFluxTest
public class MyServiceTest {
    @Test
    public void test() {
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController()).build();
        try {
            MvcResult person = mockMvc.perform(get("/test/method"))
                    .andExpect(status().isOk())//用于验证期望的 HTTP 响应状态码是否为 200(OK)。除此之外还有其他比如404、500等
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))//设置响应头格式
                    .andExpect(model().attributeHasErrors("person"))//验证模型中是否存在错误属性
                    .andReturn();//获取执行请求后的返回结果
            String contentAsString = person.getResponse().getContentAsString();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

除了使用 .getContentAsString() 方法获取响应体以字符串形式,MvcResult 还提供了其他方法来获取不同类型的响应数据,比如 .getResponse().getContentAsByteArray() 可以获取响应体以字节数组形式,.getResponse().getHeader() 可以获取响应头信息等。

  • 筛选注册

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

@WebFluxTest
public class MyServiceTest {
    @Test
    public void test() {
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController()).addFilter(new CharacterEncodingFilter()).build();
    }
}
  • html单元集成

Spring提供了MockMvcHtmlUnit。当使用基于HTML的视图时,这简化了端到端测试的执行(现在大多数项目都是前后分离项目,所以并不建议增加后端测试用例的工作量,如果有需要可以自行了解)。

MockMvc适用于不依赖于Servlet容器的模板技术(例如,Thymeleaf、FreeMarker等),但不适用于JSP,因为它们依赖于Servlet容器。

JDBC测试支持

org.springframework.test.jdbc包装包含JdbcTestUtils,这是一个与JDBC相关的实用函数集合,旨在简化标准数据库测试场景。具体来说,JdbcTestUtils提供下列静态实用工具方法。

  • countRowsInTable(..):计算给定表中的行数。
  • countRowsInTableWhere(..):使用提供的计算给定表中的行数WHERE条件。
  • deleteFromTables(..):删除指定表中的所有行。
  • deleteFromTableWhere(..):使用提供的从给定的表中删除行WHERE条件。
  • dropTables(..):删除指定的表。

其它注释

除了前面内容中介绍过的注解,再来讲解下没用到的一些注解(你不必全都记住,只要会一两种能解决你的问题即可)。

  • @ContextConfiguration注解

@ContextConfiguration定义类级元数据,该元数据用于确定如何加载和配置ApplicationContext用于集成测试。具体来说,@ContextConfiguration声明应用程序上下文资源locations或者组件classes用于加载上下文。

//@ContextConfiguration(locations = { "classpath:/applicationContext.xml" })使用 XML 文件配置
//@ContextConfiguration(locations = { "classpath:/com/example/app/" })使用组件扫描
@ContextConfiguration(classes = { AppConfig.class })//使用 Java 配置类
public class MyServiceTest {
}
  • @WebAppConfiguration注解

@WebAppConfiguration是一个类级别的注释,你可以用它来声明为集成测试加载的ApplicationContext应该是一个WebApplicationContext

@WebAppConfiguration
@ContextConfiguration
public class MyServiceTest {
}
  • @ContextHierarchy注解

@ContextHierarchy是一个类级注释,用于定义ApplicationContext集成测试的实例。@ContextHierarchy应该用一个或多个@ContextConfiguration实例,每个实例定义上下文层次结构中的一个级别。

@Configuration
@ContextHierarchy({
    @ContextConfiguration(classes = AppConfig.class),
    @ContextConfiguration(classes = DatabaseConfig.class)
})
public class RootConfig {
    // ...
}
  • @DirtiesContext注解

@DirtiesContext 注解用于标记测试方法或类会导致应用程序上下文被"脏化"(dirty)。当一个测试方法或类被标记为 @DirtiesContext 时,在执行该方法或类后,Spring 将会重置相应的应用程序上下文,以确保下一个测试方法或类在一个干净的环境中运行。

@RunWith(SpringRunner.class)
public class MyServiceTest {
    @Test
    @DirtiesContext
    public void testMethod() {
        // ...
    }

    @Test
    public void anotherTestMethod() {
        // ...
    }
}
  • @TestExecutionListeners注解

@TestExecutionListeners用于自定义测试执行时的监听器。通过使用 @TestExecutionListeners 注解,我们可以指定在测试方法或类执行期间调用的监听器,以扩展测试框架的功能或添加额外的行为。

测试执行监听器可以实现 TestExecutionListener 接口或继承 TestExecutionListenerAdapter 类,并覆盖其中的方法来定义自定义的测试执行行为。

public class MyTestListener implements TestExecutionListener {
    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        // 在测试方法执行之前执行的逻辑
        System.out.println("after");
    }
}
@RunWith(SpringRunner.class)
@TestExecutionListeners({ MyTestListener.class })
public class MyServiceTest {

    @Test
    public void test() {
        System.out.println("test");
    }
}
  • @Timed注解

JUnit 4中,通过使用 @Timed 注解,指示带注释的测试方法必须在指定的时间段(以毫秒为单位)内完成执行。如果文本执行时间超过指定的时间段,测试失败。

@RunWith(SpringRunner.class)
public class MyServiceTest {
    @Test
    @Timed(millis = 10)
    public void test() {
        System.out.println("qwe");
    }
}
  • @Repeat注解

JUnit 4中,指示带注释的测试方法必须重复运行。注释中指定了测试方法的运行次数。

@RunWith(SpringRunner.class)
public class MyServiceTest {
    @Test
    @Repeat(5)
    public void test() {
        System.out.println("qwe");
    }
}
  • @SpringJUnitConfig注解

@SpringJUnitConfigJUnit 5 中的一个组合注释,它结合了@ExtendWith(SpringExtension.class)Spring TestContext框架中的@ContextConfiguration,我们可以方便地将 Spring 的功能集成到JUnit测试中,并使用 Spring 上下文来管理测试环境和依赖注入。


@SpringJUnitConfig(AppConfig.class)
public class MyServiceTest {
}
  • @SpringJUnitWebConfig注解

@SpringJUnitWebConfigJUnit 5 中的一个组合注释,它结合了JUnit Jupiter中的@ExtendWith(SpringExtension.class)Spring TestContext框架中的@ContextConfiguration@WebAppConfiguration

@SpringJUnitWebConfig(TestController.class)
public class MyServiceTest {
}

上面介绍的一些*Config注解在实际测试中有很多问题,可能是依赖版本导致不支持,已经可以跑通的还是JUnit 4版本,示例代码:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class MyServiceTest {
	@Autowired
    private ApplicationContext applicationContext;
    @Autowired
    private MyService myService;

    @Test
    public void test() {
        System.out.println(myService);
        System.out.println(applicationContext);
    }
}

如果你想使用WebApplicationContext 做一些处理,可以使用@WebAppConfiguration注解,示例代码如下:

@RunWith(SpringRunner.class)
@WebAppConfiguration
public class MyServiceTest {
    @Autowired
    private WebApplicationContext applicationContext;

    @Autowired
    private MyService myService;

    @Test
    public void test() {
        System.out.println(myService);
        System.out.println(applicationContext);
    }
}

如果你有更好的使用方法,欢迎评论区讨论交流。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值