掌握Spring Boot的测试效率:优化策略和最佳实践

973 篇文章 0 订阅
556 篇文章 1 订阅

2024软件测试面试刷题,这个小程序(永久刷题),靠它快速找到工作了!(刷题APP的天花板)-CSDN博客文章浏览阅读846次,点赞38次,收藏6次。你知不知道有这么一个软件测试面试的刷题小程序。里面包含了面试常问的软件测试基础题,web自动化测试、app自动化测试、接口测试、性能测试、自动化测试、安全测试及一些常问到的人力资源题目。最主要的是他还收集了像阿里、华为这样的大厂面试真题,还有互动交流板块……https://blog.csdn.net/AI_Green/article/details/134931243?spm=1001.2014.3001.5501揭开增强Spring Boot测试能力的秘密!探索我们如何利用特定技术,将测试运行时间缩短60%!

以下为作者观点:

嘿,朋友们!让我们一起进入使用 JUnit 进行 Spring Boot 测试的精彩世界。它的功能非常强大,为我们的代码测试提供了一个真实的环境。但是,如果我们不对测试进行优化,它们可能会很慢,并对团队的变更时间产生负面影响。

本文将分享如何优化 Spring Boot 测试,使其更快、更高效、更可靠。想象一下,一个APP的测试需要10分钟才能执行,这个时间也不少!让我们看看如何在短时间内快速完成这些测试吧!

了解Spring中的测试分片(Test Slicing)

Spring 中的测试分片(Test Slicing)允许测试应用程序的特定部分,只关注相关组件,而不是加载整个上下文。它是通过 @WebMvcTest、@DataJpaTest 或 @JsonTest 等注解实现的。这些注解是一种有针对性的方法,可将上下文加载限制在特定层或技术上。例如,@WebMvcTest 主要加载 Web 层,而 @DataJpaTest 则初始化数据 JPA 层,以实现更简洁高效的测试。这种选择性加载方法是优化测试效率的基石。

还有更多注解可用于切分上下文。请参阅有关测试切片的 Spring 官方文档。(https://docs.spring.io/spring-boot/docs/current/reference/html/test-auto-configuration.html#appendix.test-auto-configuration.slices)

测试切片:使用 @DataJpaTest 代替 @SpringBootTest

让我们来看一个示例(代码如下)。测试首先删除目标表中的所有数据(货物和集装箱,每个货物可以有多个集装箱),然后保存新的货物。然后,创建一个包含 50 个线程的线程池,每个线程调用 svc.createOrUpdateContainer 方法。

测试将等待所有线程结束,然后检查数据库是否只有一个容器。

该测试主要是检查并发问题,涉及大量线程,在我的机器上耗时约 16 秒--对于单个服务检查来说,这可是一大块时间,不是吗?

@ActiveProfiles("test")@SpringBootTestabstractclassBaseIT {    @Autowired    privatelateinitvarshipmentRepo: ShipmentRepository    @Autowired    privatelateinitvarcontainerRepo: ContainerRepository}classContainerServiceTest : BaseIT() {    @Autowired    privatelateinitvarsvc: ContainerService    @BeforeEach    funsetup() {        shipmentRepo.deleteAll()        containerRepo.deleteAll()        shipmentRepo.save(shipment)    }    @Test    funtestConcurrentUpdatesForContainer() {        valexecutor= Executors.newFixedThreadPool(50)        repeat(50) {            executor.execute {                containerService.createOrUpdateContainer("${shipment.id}${svc.DEFAULT_CONTAINER}", Patch("NEW_LABEL"))            }        }        executor.shutdown()        while (!executor.awaitTermination(100, TimeUnit.MILLISECONDS)) {            // busy waiting for executor to terminate        }        assertThat(containerRepo.find(shipment)).hasSize(1)    }}

我们遇到的第一个问题是类声明:

class ContainerServiceTest : BaseIT()

问题的起因是 BaseIT 类使用了 @SpringBootTest。这会导致整个应用程序的 Spring 上下文被加载(每次我们都会使用上下文缓存机制,这个我们稍后再谈!)。当应用程序足够大时,大量的 Bean 就会被加载--对于具有特定目标的测试来说,这是一项代价高昂的操作。

但是,我们并不想加载所有内容。我们只需要加载 ContainerService Bean 和 JPA 资源库。我们可以改用 @DataJpaTest。该注解只加载应用程序的 JPA 部分,而这正是我们本次测试所需要的。让我们试试看!

@DataJpaTestclass ContainerServiceTest {    @Autowired    private lateinit var svc: ContainerService    @Autowired    private lateinit var shipmentRepo: ShipmentRepository    @Autowired    private lateinit var containerRepo: ContainerRepository}

执行时会出现异常:

org.springframework.beans.factory.BeanCreationException: Failed to replace DataSource with an embedded database for tests. If you want an embedded database please put a supported one on the classpath or tune the replace attribute of @AutoConfigureTestDatabase.

@DataJpaTest 有一个注解 @AutoConfigureTestDatabase,默认情况下,它会为测试设置一个 H2 内存数据库,并配置 DataSource 以使用它。然而,在这种情况下,classpath 中找不到 H2 依赖项。

实际上,我们并不想在测试中使用 H2,所以我们可以告诉 @AutoConfigureTestDatabase 不要用 H2 替换我们配置的数据库。此外,我们还必须配置和加载我们自己的数据库,这里通过导入名为 EmbeddedDataSourceConfig 的 @Configuration 类来实现(它只是创建了一个 DataSource 类型的 @Bean)。

@DataJpaTest@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)@Import(EmbeddedDataSourceConfig::class) // Import the embedded database configuration if needed.@ActiveProfiles("test") // Use the test profile to load a different configuration for tests.class ContainerServiceTest {    // test code}

让我们再次尝试运行测试。现在,它失败了,出现了以下错误:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'ContainerServiceTest': Unsatisfied dependency expressed through field 'containerService'

你已经掌握了诀窍,需要在 Spring 上下文中加载 ContainerService Bean!

@DataJpaTest@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)@Import(ContainerService::class, EmbeddedDataSourceConfig::class)@ActiveProfiles("test")class ContainerServiceTest {    // test code}

Uh-oh! Spring 上下文加载成功,但测试却失败了,错误如下:

java.lang.AssertionError:
Expected size:<1> but was:<0> in:
<[]>

如果查看 @DataJpaTest,你会发现它使用了 @Transactional 注解。这意味着,默认情况下,从目标表中删除数据和创建新容器只会在测试方法结束时提交,因此线程创建的事务看不到这些更改。

由于我们希望在主事务(@DataJpaTest 使用的事务)中提交事务,因此需要使用 Propagation.REQUIRES_NEW:

@DataJpaTest@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)@Import(ContainerService::class, EmbeddedDataSourceConfig::class)@ActiveProfiles("test")class ContainerServiceTest {    @Autowired    private lateinit var transactionTemplate: TransactionTemplate    @Autowired    private lateinit var svc: ContainerService    @Autowired    private lateinit var shipmentRepo: ShipmentRepository    @Autowired    private lateinit var containerRepo: ContainerRepository    @BeforeEach    fun setup() {        transactionTemplate.propagationBehavior = TransactionTemplate.PROPAGATION_REQUIRES_NEW        transactionTemplate.execute {            shipmentRepo.deleteAll()            containerRepo.deleteAll()            shipmentRepo.save(shipment)        }    }}

测试通过,仅用 8 秒(加载上下文 + 运行)就完成了,比以前快了一倍!

测试切片:@JsonTest 精确验证 JSON 序列化/解序列化 

请看这个测试片段:

 public class EventDeserializationIT extends BaseIT {    private static final String RESOURCE_PATH = "event-example.json";    @Autowired    private ObjectMapper objectMapper;    private Event dto;    @Test    public void testDeserialization() throws Exception {        String json = Resources.toString(Resources.getResource(RESOURCE_PATH), UTF_8);        dto = objectMapper.reader().forType(Event.class).readValue(json);        assertThat(dto.getData().getNewTour().getFromLocation()).isNotNull();        assertThat(dto.getData().getNewTour().getToLocation()).isNotNull();    }}

该测试的目的是确保正确的反序列化。我们可以使用 @JsonTest 注解导入测试中所需的 Bean。我们只需要对象映射器,无需扩展任何其他类!使用此注解只会应用与 JSON 测试相关的配置(即 @JsonComponent、Jackson Module)。

@JsonTestpublic class EventDeserializationTest {    @Autowired    private ObjectMapper objectMapper;    // Test implementation}

测试切片:用于 REST API 的 @WebMvcTest

使用 @WebMvcTest,我们可以在不启动服务器(如嵌入式 Tomcat)或加载整个应用程序上下文的情况下测试 REST API。这一切都以特定控制器为目标。快速高效,就是这么简单!

@WebMvcTest(ShipmentServiceController.class)public class ShipmentServiceControllerTests {    @Autowired    private MockMvc mvc;    @MockBean    private ShipmentService service;    @Test    public void getShipmentShouldReturnShipmentDetails() {        given(this.service.schedule(any())).willReturn(new LocalDate());        this.mvc.perform(                get("/shipments/12345")                        .accept(MediaType.APPLICATION_JSON)                        .andExpect(status().isOk())                        .andExpect(jsonPath("$.number").value("12345"))                // ...        );    }}

驯服 Mock/Spy Beans 和上下文缓存难题 

让我们深入了解 Spring Test 上下文缓存机制的复杂性!

当你的测试涉及 Spring Test 功能(如 @SpringBootTest、@WebMvcTest、@DataJpaTest)时,它们需要一个正在运行的 Spring 上下文。为测试启动 Spring 上下文需要大量时间,尤其是在使用 @SpringBootTest 填充整个上下文的情况下,如果每个测试都启动自己的上下文,就会导致测试执行开销增加,构建时间延长。

幸运的是,Spring Test 提供了一种机制来缓存已启动的应用程序上下文,并将其重新用于具有类似上下文需求的后续测试。

缓存就像一个地图,有一定的容量。映射键由几个参数计算得出,其中包括加载到上下文中的 Bean。

包括:

  • 位置 (from @ContextConfiguration)

  • 类 (from @ContextConfiguration)

  • contextInitializerClasses (from @ContextConfiguration)

  • contextCustomizers (from ContextCustomizerFactory) – 这包括 @DynamicPropertySource 方法以及 Spring Boot 测试支持的各种功能,如 @MockBean 和 @SpyBean

  • contextLoader (from @ContextConfiguration)

  • parent (from @ContextHierarchy)

  • activeProfiles (from @ActiveProfiles)

  • propertySourceLocations (from @TestPropertySource)

  • propertySourceProperties (from @TestPropertySource)

  • resourceBasePath (from @WebAppConfiguration)

例如,如果 TestClassA 为 @ContextConfiguration 的 locations(或值)属性指定了 {"app-config.xml"、"test-config.xml"},TestContext 框架就会加载相应的 ApplicationContext,并将其存储在静态上下文缓存中,该缓存的键完全基于这些位置。因此,如果 TestClassB 也为其位置定义了 {"app-config.xml"、"test-config.xml"}(通过显式继承或隐式继承),并且没有为上述任何其他属性定义不同的属性,那么两个测试类将共享同一个 ApplicationContext。这意味着加载应用程序上下文的设置成本只需一次(每个测试套件),而且后续测试执行速度会更快。

如果在不同的测试中使用不同的属性,例如在测试中使用不同的(ContextConfiguration、TestPropertySource、@MockBean 或 @SpyBean),缓存键就会发生变化。对于每个新上下文(缓存中不存在),都必须从头开始加载。

如果有许多不同的上下文,缓存中的旧键就会被移除,因此下一个运行的测试可能会使用这些缓存上下文,就需要重新加载它们。这将导致额外的测试时间。

一种效率优化方法是将 mock Bean 合并到父类中。这可以确保上下文保持不变,从而提高效率并避免多次重新加载上下文。

前后示例: 

@SpringBootTestpublic class TestClass1 {    @MockBean    private DependencyA dependencyA;    // Test implementation}@SpringBootTestpublic class TestClass2 {    @MockBean    private DependencyB dependencyB;    // Test implementation}@SpringBootTestpublic class TestClass3 {    @MockBean    private DependencyC dependencyC;    // Test implementation}

如果我们尝试运行上述示例,上下文将被重新加载3次,这一点也不高效。让我们试着优化一下。

@SpringBootTestpublic abstract class BaseTestClass {    @MockBean    private DependencyA dependencyA;    @MockBean    private DependencyB dependencyB;    @MockBean    private DependencyC dependencyC;}// Extend the BaseTestClass for each test classpublic class TestClass1 extends BaseTestClass {    @Test    public void testSomething1() {        // Test implementation    }}public class TestClass2 extends BaseTestClass {    @Test    public void testSomething2() {        // Test implementation    }}public class TestClass3 extends BaseTestClass {    @Test    public void testSomething3() {        // Test implementation    }}

现在,上下文只需重新加载一次,效率更高!

或者更好:使用 @Import 注解导入包含 mock Bean 的配置类,可以避免类继承。

@TestConfigurationclass Config {    @MockBean    private DependencyA dependencyA;    @MockBean    private DependencyB dependencyB;    @MockBean    private DependencyC dependencyC;}@Import(Config::class)@ActiveProfiles("test")class TestClass1 {    // Test code}

使用 @DirtiesContext 之前请三思

在测试类中应用 @DirtiesContext 会在测试执行后删除应用程序上下文。这会将 Spring 上下文标记为脏上下文,从而阻止 Spring Test 重用它。谨慎考虑使用此注解很重要。

虽然有些人使用它来重置数据库中的 ID,但还有更好的替代方法。例如,@Transactional 注解可用于在执行测试后回滚事务。

并行执行测试

默认情况下,JUnit Jupiter 测试在单线程中顺序运行。然而,JUnit 5.3 中引入了一项可选功能,即允许测试并行运行,以加快执行速度。🚀

要启动并行测试执行,请按以下步骤操作:

1.在 test/resources 中创建一个 junit-platform.properties 文件。

2.在该文件中添加以下配置:junit.jupiter.execution.parallel.enabled = true

3.在要并行运行的每个类中添加以下内容。@Execution(CONCURRENT)

请记住,某些测试可能因其性质而与并行执行不兼容。在这种情况下,不应添加 @Execution(CONCURRENT)。有关不同执行模式的更多解释,请参阅 JUnit: writing tests - parallel execution。(https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-parallel-execution)

结果

应用上述所有优化措施后,我们的 CI/CD 管道有了很大改观。我们的测试速度更快了,现在只需要4分15秒,而以前需要10分7秒,提高了60%!

结论 

在这次优化 Spring Boot 测试的冒险中,我们利用了一系列策略来提高测试效率和速度。让我们总结一下我们实施的策略:

  • 测试切片:利用 @WebMvcTest、@DataJpaTest 和 @JsonTest 将测试重点放在特定层或组件上。你可以查看更多信息(测试 Spring Boot 应用程序)。(https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing.spring-boot-applications)

  • 上下文缓存困境:通过优化mock和spy Bean的使用,克服与脏 ApplicationContext 缓存相关的难题。请参见Spring测试上下文缓存。(https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/caching.html)

  • 并行测试执行:启用并行测试执行以大幅缩短测试套件的执行时间。请参阅《JUnit 5并行执行用户指南》。(https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution)

这些策略共同将测试转化为一个更快、更可靠和更高效的过程。每种策略无论是单独使用还是结合使用,都能极大地促进测试实践的优化,使工程师能以更高的效率交付更高质量的软件。 

行动吧,在路上总比一直观望的要好,未来的你肯定会感谢现在拼搏的自己!如果想学习提升找不到资料,没人答疑解惑时,请及时加入群: 786229024,里面有各种测试开发资料和技术可以一起交流哦。

最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】在这里插入图片描述
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值