SpringBoot Test实战:从单元测试到集成测试的完整指南
2026/9/9 19:54:43 网站建设 项目流程

SpringBoot Test一直是个神奇的话题,你在社区里搜一下,能看到两类极端的帖子:一类是"测试覆盖率达到90%的团队怎么搭建测试体系",另一类是"救命,@SpringBootTest又启动失败了"。这两类帖子之间,隔着的是大量只有真正踩过坑才懂的经验。

这篇文章就是想把SpringBoot Test这个主题彻底讲透。从最基础的测试分层思路,到核心注解的实际用法和版本差异,再到一套可以直接抄作业的完整实操流程,以及我整理的高频问题排查手册。不管是刚接触SpringBoot测试的新手,还是想把自己的测试代码写得更规范的老手,这篇文章都能给你一些参考。

1. 测试到底该怎么分层:为什么别一上来就@SpringBootTest

1.1 先想清楚一个问题:测试是为了什么

很多人在SpringBoot项目里写测试,心态是"领导要求覆盖率",于是把测试当成任务应付,每个Service方法都写一个@SpringBootTest包起来的测试类,跑一次要十几秒,改个测试数据依赖一堆前置条件。这种测试写多了,不但没有增强信心,反而成了CI里的定时炸弹,动不动就红,红了你还没时间去修,最后干脆把测试跳过。

我在实际项目中得到的体会是,测试不是为了覆盖率数字,而是为了让你在改代码的时候敢动手。重构一个方法、升级一个依赖、改一个SQL,跑一遍相关测试,三分钟内告诉你有没有改挂,这就是测试最大的价值。所以测试的分层和选型,第一原则应该是"快"和"稳"。

1.2 测试分层的核心思路:单元测试、切片测试、集成测试

SpringBoot官方文档其实已经把测试分层的思路讲得很清楚了,就是三个层次:

  • 单元测试:只测一个类或一个方法,不启动Spring容器,其他依赖全部Mock掉。执行速度毫秒级。
  • 切片测试(Slice Test):启动Spring容器的一小部分,比如只加载Controller层或者只加载Repository层,用@WebMvcTest@DataJpaTest这类注解搞定。启动速度几秒级。
  • 集成测试:完整启动Spring容器,用真实数据库、Redis、消息队列等外部依赖。执行速度从几秒到几十秒不等,但最接近真实运行环境。

这三层测试的使用比例,理论上应该是金字塔形,单元测试最多,集成测试最少。但在真实项目里,我发现很多团队的测试比例是倒过来的,这通常是因为大家太依赖@SpringBootTest,把单元测试当集成测试来写,时间全耗在启动容器上了。

1.3 为什么"SpringBoot单元测试最佳实战"成了高频搜索词

从最近的热搜词来看,"SpringBoot 单元测试最佳实战"和"springboot test"的搜索量一直不低。这背后反映出两个问题:一是测试确实是SpringBoot项目的硬需求,几乎每个项目都要写;二是很多人写出来的测试不够"实战",要么测试方法命名不清不楚,要么断言写得跟没写一样,要么测试之间互相依赖,跑一个全挂。

真正好的单元测试,应该具备三个特征:独立、快速、有明确的断言目标。每个测试方法只测一个行为,不依赖测试执行顺序,不依赖外部环境,断言的是"结果对不对",而不是"代码跑没跑通"。

2. SpringBoot Test核心注解盘点:功能、代价和版本陷阱

2.1 @SpringBootTest到底做了什么

@SpringBootTest是集成测试的入口注解,它的核心作用是启动完整的Spring应用上下文。它会去扫描配置类、加载application.yml、创建所有Bean、执行自动装配逻辑,整个流程跟你把应用跑起来几乎一模一样。

这个注解有几个关键参数需要重点看一下:

参数可选值作用
webEnvironmentMOCKRANDOM_PORTDEFINED_PORTNONE决定测试环境的Web服务器类型
properties形如"key=value"的字符串数组临时指定配置属性,覆盖配置文件
classes配置类Class数组指定加载哪些配置类,控制上下文范围

默认的webEnvironment = MOCK会创建一个模拟的Servlet环境,不启动真实的Tomcat,配合MockMvc可以模拟HTTP请求。如果你需要测试真实的HTTP调用,比如测RestTemplate或WebClient的远程调用,就需要用RANDOM_PORT,让它随机选一个可用端口真实启动服务。

很多人忽略的一点是,@SpringBootTest启动的上下文是会被缓存的。也就是说,同一个测试类里多个测试方法共用同一个上下文,多个测试类如果配置相同也会复用同一个上下文。这个机制在Spring Test Framework里叫ApplicationContext缓存,合理利用它,是优化测试速度的关键。

2.2 切片测试:不需要完整容器的时候别硬撑

@SpringBootTest的代价是完整的自动装配和Bean创建,如果你的电脑配置一般,跑一个完整测试类可能要等几十秒。更麻烦的是,完整上下文意味着所有外部依赖都要健康,数据库要能连上,Redis要能连上,任何一个down掉,整个测试类就废了。

切片测试就是为了解决这种窘境。它的思路是"我只测这一层,其他层我都不关心":

切片注解擅长测试的对象自动加载的内容
@WebMvcTestController层、拦截器、ControllerAdviceSpring MVC相关组件,不会加载Service和Repository
@DataJpaTestRepository层、JPA映射、SQLJPA/Hibernate相关组件,默认用内嵌数据库
@JsonTestJSON序列化与反序列化Jackson等JSON转换器
@RestClientTestRestTemplate/WebClient的Mock调用RestTemplateBuilder和MockRestServiceServer

我特别推荐在项目里多用切片测试。写Controller层测试,就用@WebMvcTest,把Service层Mock掉,这样不用连接数据库,速度飞快。写Repository层测试,就用@DataJpaTest,它会自动用内嵌数据库(如H2),只加载JPA相关配置,十几秒就能跑完。

2.3 @MockBean和@MockitoBean:Spring Boot 3.4之后的重大变化

聊到测试,@MockBean是绕不开的注解。它的作用很简单,把容器里的某个Bean替换成Mockito的Mock对象,让你可以不依赖真实实现来测试上层逻辑。

但如果你用的是Spring Boot 3.4及以上版本,需要特别注意:@MockBean被标记为废弃了,官方推荐使用@MockitoBean,对应的@SpyBean也被@SpyBean(注意是新的org.springframework.test.context.bean.override.mockito.MockitoBean)替代。原因是在3.4版本里引入了一套新的Bean override机制,更加灵活、性能更好。

如果你的项目还在用3.3或更低版本,那继续用@MockBean没问题。但如果你的Spring Boot版本已经升到3.4以上,启动时候会看到警告日志,建议尽早迁移到@MockitoBean,因为后续版本大概率会移除旧的实现。

2.4 配置文件加载顺序和@ActiveProfiles的配合

SpringBoot测试里经常遇到的一个场景是,测试环境要用独立的数据库或配置,不能动开发环境的配置。这时候@ActiveProfiles("test")就是标配。

它背后的逻辑是Spring的Profile机制:配置文件里的spring.profiles.active决定默认激活哪个Profile,而@ActiveProfiles注解可以在测试启动时覆盖这个值,让测试走独立的配置。

需要注意的坑是配置的加载优先级。在SpringBoot里,配置来源的优先级从高到低大概是:命令行参数、Java系统属性、环境变量、application-{profile}.ymlapplication.yml。测试里的@TestPropertySource注解优先级高于application.yml但低于@SpringBootTest(properties = ...)。如果你想在某个测试类里临时覆盖配置,最省事的方式是用properties参数,比如@SpringBootTest(properties = "spring.datasource.url=jdbc:h2:mem:testdb"),效果立竿见影,也不会污染其他测试。

3. 实操过程:从依赖到一套完整的可运行测试

3.1 环境准备:引入必要的测试依赖

开始之前,先看下SpringBoot项目里基础测试依赖应该怎么加。如果你用的是Spring Initializr创建的项目,spring-boot-starter-test通常已经加好了,这个依赖里内置了JUnit 5、Mockito、AssertJ、Hamcrest、JSONAssert等一系列测试库,足够应付绝大多数场景。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

如果你的测试要操作真实数据库,一般还会引入Testcontainers相关依赖。Testcontainers用Docker容器启动真实的MySQL、PostgreSQL、Redis等中间件,测试结束自动销毁容器,不会在本地留脏数据。

<dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mysql</artifactId> <scope>test</scope> </dependency>

如果你比较保守,只想用内存数据库H2来跑Repository测试,那加一个com.h2database:h2依赖就够了。H2的优势是轻量、无需额外环境,劣势是它跟真实MySQL/PostgreSQL在某些SQL语法上不完全一致,可能出现"测试跑得好好的,上线却报SQL错误"的情况。所以我建议Repository层面的集成测试尽量用Testcontainers,具体怎么选,后面会展开。

3.2 编写第一个Service层单元测试

假设你有一个UserService,依赖UserRepository和一个PasswordEncoder,要写单元测试。核心思路是:只测UserService的逻辑,其他依赖全部Mock掉。代码如下:

class UserServiceTest { @Mock private UserRepository userRepository; @Mock private PasswordEncoder passwordEncoder; @InjectMocks private UserService userService; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); } @Test void createUser_shouldReturnSavedUser_whenUsernameNotExist() { // 准备 String username = "alice"; String rawPassword = "123456"; when(userRepository.existsByUsername(username)).thenReturn(false); when(passwordEncoder.encode(rawPassword)).thenReturn("encrypted-password"); when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0)); // 执行 User result = userService.createUser(username, rawPassword); // 断言 assertThat(result).isNotNull(); assertThat(result.getUsername()).isEqualTo(username); verify(userRepository, times(1)).save(any(User.class)); } @Test void createUser_shouldThrowException_whenUsernameAlreadyExist() { String username = "alice"; when(userRepository.existsByUsername(username)).thenReturn(true); assertThatThrownBy(() -> userService.createUser(username, "123456")) .isInstanceOf(BusinessException.class) .hasMessageContaining("already exists"); } }

这段代码里有几个细节值得讲一下。

第一,@InjectMocks会帮你把@Mock创建的对象注入到UserService的构造器或字段中。如果UserService用的是构造器注入,那就更加简单清晰,这也是我推荐的方式。

第二,thenAnswer(invocation -> invocation.getArgument(0))这个写法很常用。当你不关心save方法的具体返回值,但又需要它返回一个有效的对象时,直接返回入参是最方便的做法。

第三,断言的时候用verify来验证依赖的调用次数。这个习惯很多人没有,但测试的本质是验证行为,只要方法返回了正确结果还不够,还要确认你Mock的依赖真的被正确调用了。比如创建用户场景,如果业务代码漏掉了save调用,返回值依然是null,断言就能发现;但有些时候业务代码多调了一次save,返回值一样,不复用verify就查不出来。

3.3 Controller层测试:用@WebMvcTest和MockMvc模拟请求

Controller层的测试重点,是验证接口的URL映射、参数校验、返回值结构和状态码。这一层不需要启动完整容器,用@WebMvcTestMockMvc就够了。

@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockitoBean private UserService userService; @Test void getUser_shouldReturnUserDto_whenUserExists() throws Exception { Long userId = 1L; UserResponseDto dto = new UserResponseDto(userId, "alice"); when(userService.getUserById(userId)).thenReturn(dto); mockMvc.perform(get("/api/users/{id}", userId)) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("alice")) .andExpect(jsonPath("$.id").value(1)); } @Test void createUser_shouldReturn400_whenUsernameBlank() throws Exception { String requestBody = """ { "username": "", "password": "123456" } """; mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isBadRequest()); } }

这里用到了@MockitoBean(如果你用的是Spring Boot 3.4以下,就写@MockBean),把UserService直接替换成Mock,这样Controller测试就不会走到真实Service逻辑,不连数据库,很快。

几个容易踩的坑说一下。

  • @WebMvcTest默认会加载所有@ControllerAdviceFilterWebMvcConfigurer。如果项目里有全局异常处理器,它会生效,比如校验参数失败时返回400,这个行为就在异常处理器里控制,测试时要留意是否符合预期。
  • 如果项目里有Spring Security,@WebMvcTest会加载安全配置。很多项目里Controller方法都有权限校验,测试的时候需要对MockMvc做安全设置,或者把安全配置排除掉。最简单的做法是在测试类上加@AutoConfigureMockMvc(addFilters = false),跳过过滤器。
  • jsonPath的用法要熟悉,这是AssertJ之外另一个常用的断言库。$.username代表JSON根路径下的username字段,取值后跟期望值对比。

3.4 Repository层测试:@DataJpaTest和Testcontainers的取舍

Repository层测试主要验证SQL是否正确、返回的实体字段映射是否正确。这里有两种主流方案。

方案一,@DataJpaTest加H2内存数据库。启动速度最快,不需要额外环境。适合简单的CRUD场景。

@DataJpaTest class UserRepositoryTest { @Autowired private UserRepository userRepository; @Test void findByUsername_shouldReturnUser_whenUserExists() { User user = new User(); user.setUsername("alice"); user.setPassword("encrypted"); userRepository.save(user); Optional<User> result = userRepository.findByUsername("alice"); assertThat(result).isPresent(); assertThat(result.get().getUsername()).isEqualTo("alice"); } }

方案二,@DataJpaTest加Testcontainers。如果一个项目里的SQL逻辑复杂,用到了MySQL特有的函数、JSON字段、全文索引,H2很可能跟MySQL行为不一致,这时候就必须用Testcontainers跑真实的MySQL容器。

@DataJpaTest @Testcontainers @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class UserRepositoryTest { @Container static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", mysql::getJdbcUrl); registry.add("spring.datasource.username", mysql::getUsername); registry.add("spring.datasource.password", mysql::getPassword); } @Autowired private UserRepository userRepository; // 测试方法 }

这里有几个关键点。

  • @AutoConfigureTestDatabase(replace = NONE)非常关键。默认情况下@DataJpaTest会尝试用内嵌数据库替换数据源,如果不加这一行,Testcontainers启动的MySQL容器配置会被覆盖掉,白启动了。
  • @DynamicPropertySource是在Spring容器启动前动态注册配置属性的机制,这里把容器的JDBC信息动态注入到spring.datasource.*配置中。
  • 用Testcontainers跑真实数据库,测试速度肯定比H2慢,但换来的是跟生产环境一致的SQL行为。我的建议是:简单的CRUD和关联查询用H2就够,复杂的SQL、原生查询、数据库特性相关的测试,用Testcontainers。

3.5 完整的集成测试:@SpringBootTest + Testcontainers + MockMvc

如果你需要把Controller层、Service层、Repository层全部串起来,测一个完整的业务链路,那就要上@SpringBootTest了。比如用户注册的流程,从接收HTTP请求开始,到参数校验、业务逻辑、数据落库、返回响应,全链路都要测一遍。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers class UserIntegrationTest { @Container static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", mysql::getJdbcUrl); registry.add("spring.datasource.username", mysql::getUsername); registry.add("spring.datasource.password", mysql::getPassword); } @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void registerUser_shouldPersistAndReturnCreated() { String url = "http://localhost:" + port + "/api/users"; UserCreateRequest request = new UserCreateRequest("alice", "123456"); ResponseEntity<UserResponseDto> response = restTemplate.postForEntity( url, request, UserResponseDto.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().getUsername()).isEqualTo("alice"); // 再查一次数据库确认数据真的落库了 String queryUrl = "http://localhost:" + port + "/api/users/" + response.getBody().getId(); ResponseEntity<UserResponseDto> queryResponse = restTemplate.getForEntity(queryUrl, UserResponseDto.class); assertThat(queryResponse.getBody().getUsername()).isEqualTo("alice"); } }

RANDOM_PORT配合TestRestTemplate是集成测试里非常舒服的组合。TestRestTemplate会自动带上测试环境的相关配置,用法跟RestTemplate几乎一样,但更贴近SpringBoot集成的写法。

集成测试写起来爽,但要注意别滥用。如果你每个测试类都用完整的@SpringBootTest,几十个测试类加起来,CI时间会非常可观。我的建议是:核心链路写集成测试,其他逻辑用单元测试和切片测试覆盖。

3.6 测试命名规范和断言技巧

最后分享几个测试代码的规范建议,这部分对团队协作的价值可能超过技术本身。

测试方法命名,我推荐使用方法名_场景_预期结果的格式,比如createUser_shouldReturnSavedUser_whenUsernameNotExist。这种命名读起来跟英语句子一样,测试失败的时候,看方法名就知道哪里出了问题,不用再去翻代码。

断言方面,优先使用AssertJ的流式断言,而不是JUnit自带的assertEquals那一套。AssertJ的链式API可读性更强,还能做集合字段的精确断言。

assertThat(result) .extracting(User::getUsername, User::getStatus) .containsExactly("alice", UserStatus.ACTIVE); assertThat(userList) .hasSize(3) .extracting(User::getUsername) .containsExactly("alice", "bob", "carol");

一个测试方法只测一个行为,不要在一个方法里堆十几个断言。如果某个行为需要多种断言来验证,那就说明这个行为本身太复杂,该考虑把逻辑拆出来。

4. 常见问题与排查技巧实录

4.1 数据库连接失败:H2替换了MySQL数据源怎么办

这是@DataJpaTest最常见的坑之一。你配置的是MySQL连接串,但@DataJpaTest默认会用内嵌数据库H2把数据源替换掉,然后启动时报错:Failed to replace DataSource with an embedded database

解决方式很明确,加一行配置:

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

这行的意思是"不要替换我的数据源"。如果你确实想用内存数据库,但项目里用的是MySQL语法,那H2需要开启MySQL兼容模式,在application-test.yml里设置连接串jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_LOWER=TRUE

4.2 @MockBean注入不生效

有时候你在测试类上加了@MockBean,但运行时发现被Mock的Bean还是实际逻辑,或者干脆报NoSuchBeanDefinitionException。这通常是因为你Mock的类没有被Spring容器管理。

排查思路从三个角度走:第一,确认目标Bean是否真的在容器里,比如你Mock的是UserService,那UserService必须是在@Service@Component注解下注册过的;第二,确认切面是否生效,如果项目用了AOP切面切在Service层上,@MockBean替换的是目标对象,但切面代理可能还保留着对原有目标对象的引用;第三,如果你用的是Spring Boot 3.4以下版本,检查是不是误用了@MockitoBean,这个注解在这些版本里还不存在,会直接编译报错。

4.3 测试方法之间数据互相污染

这是Repository测试和集成测试里特别容易遇到的问题。第一个测试方法往数据库插了一条记录,第二个测试方法查询时发现数据比预期多,于是断言失败。

解决方案是@Transactional。在测试方法或测试类上加@Transactional,Spring会在每个测试方法结束后自动回滚事务,不会真的落库。@DataJpaTest本身就是事务性的,所以不需要额外加。但@SpringBootTest默认不是事务性的,需要自己加:

@SpringBootTest @Transactional class UserIntegrationTest { // 每个测试方法结束后自动回滚 }

注意,加@Transactional的测试,回滚是默认行为。如果你某个测试方法确实需要验证数据落库后能被查询到,或者需要外部的另一个服务读到这条数据(比如异步消息队列),就不适合加事务。这时候应该用Testcontainers或者每轮测试前清库的策略。

4.4 测试启动时Redis、消息队列连不上

SpringBoot项目里基本都用了Redis,一些项目还接入了MQ。@SpringBootTest启动时会自动创建这些Bean,如果本地的Redis没启动,测试直接报连接超时。

有两种处理方式。

第一种,Mock掉外部依赖相关的Bean。比如你测试用户注册链路,里面用到了Redis缓存用户Session,那直接@MockitoBeanRedisTemplateStringRedisTemplate,让Redis这部分逻辑走Mock,不真的去连Redis。

第二种,用@SpringBootTest(properties = ...)禁用相关自动配置。比如:

@SpringBootTest(properties = { "spring.redis.port=6379", "spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration" })

这种方式适合你不关心Redis逻辑的测试。但如果你要测的链路本身依赖Redis的核心能力,那还是要启动Testcontainers的Redis容器,这个跟MySQL的用法一样,GenericContainer指定redis:7-alpine镜像即可。

4.5 IDEA和Maven环境下配置文件差异导致的测试失败

从热搜词里能看到"idea maven发布时的prod test配置文件",这个问题不少人都遇到过。现象通常是:本地IDEA里跑测试一切正常,但是在Maven命令行执行mvn test时就报错,或者反之。

根源在于,IDEA和Maven设置spring.profiles.active的方式不一样。IDEA里你可以在Run Configuration的Environment variables或者VM options里指定-Dspring.profiles.active=test,但Maven的mvn test不会自动读取这个设置,它只会读取application.ymlapplication-{profile}.yml

所以最稳妥的做法,是不要依赖IDE或者命令行来指定Profile,而是在测试代码里显式声明:

@ActiveProfiles("test")

这样无论是IDEA还是Maven执行,都会统一激活test环境配置。如果遇到IDEA里能跑、Maven里跑不了的情况,优先检查是不是项目里用了src/test/resources下的配置但Maven没有把它打进测试Classpath,这类问题可以查看target/test-classes目录确认。

4.6 测试跑得慢:上下文缓存和并行执行的优化策略

测试太慢是团队推行测试文化的巨大阻力。一次构建十分钟,谁都不想等。优化测试速度,核心思路有三个方向。

第一个方向,尽量多用单元测试和切片测试。@DataJpaTest@WebMvcTest都比@SpringBootTest快一个数量级。能用Mock解决的问题就不要启动完整容器。

第二个方向,善用SpringTest的上下文缓存机制。Spring会缓存ApplicationContext,相同配置的测试类复用同一个上下文。为了让缓存命中率更高,尽量不要在不同测试类里通过@SpringBootTest(properties = ...)写不同的配置,每个不同的配置都会生成一个独立的上下文,缓存就失效了。

第三个方向,开启JUnit的并行测试。JUnit 5支持并行执行,配置一下junit-platform.properties

junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent junit.jupiter.execution.parallel.mode.classes.default=concurrent

但要注意,并行执行测试时,多个测试类共享数据库会产生数据竞争。对于依赖数据库的测试类,还是建议保持串行,或者用Testcontainers隔离。

最后再分享一个小技巧

我在项目里踩过的最大一个坑,是把spring-boot-starter-test里的依赖用exclusion排除掉了。当时是为了解决一个依赖冲突,结果把AssertJ排掉了,导致一大片测试的assertThat全部编译报错。排查了半天才发现是依赖被排除导致的。

所以如果你在测试里遇到离奇的编译错误,先别急着改代码,看一眼mvn dependency:tree,确认一下SpringBoot测试相关的依赖是不是完整。尤其是JUnit 5、Mockito、AssertJ这三个核心库,缺一不可。另外一个容易被忽略的依赖是spring-boot-test-autoconfigure,切片测试注解都依赖它,如果你在项目里手动整理过依赖,一定要保证它没有被误删。

测试这个东西,写的时候确实比写业务代码费劲,但它给你带来的安全感和自信,是线上出问题时才发现没测试可跑的懊恼所无法比拟的。希望你也能在自己的项目里把测试跑起来,跑得又快又稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询