1. 什么是单元测试

  1.1 基本定义

  单元测试(Unit Test) 是对软件开发中最小可测单位(例如一个方法或者一个类)进行验证的一种测试方式。

  在 Java 后端的 Spring Boot 项目中,单元测试通常会借助 JUnit、Mockito 等框架对代码中核心逻辑进行快速且隔离的验证,保证功能正确性。

  目的:及早发现并修复 BUG,使后续迭代功能或重构时能迅速验证不会破坏已实现的功能。

  1.2 单元测试在 Spring Boot 中的地位

  Spring Boot 提供了非常方便的测试支持,如 @SpringBootTest、@TestConfiguration 等注解,让开发者可以快速地在带有 Spring 容器上下文的环境中执行测试。

  Spring Boot 本身也对 JUnit、Mockito、AssertJ 等常用测试框架或库提供了开箱即用的整合或依赖。

  1.3 单元测试与其他测试的区别

  单元测试:聚焦在一个方法或者一个类层面,不涉及过多外部依赖,能极快地发现逻辑错误。

  集成测试:多个模块或组件交互时的测试,通常依赖真实数据库、消息队列等外部资源。

  端到端测试(E2E):关注的是整个系统的完整流程,包括前端、后端、数据库、外部接口等。

  在 Spring Boot 环境中,可以使用 @SpringBootTest 搭配 Mock 或者内存数据库来实现集成测试,但这通常已经不只是“单元”级别了。

  2. 为什么要写单元测试?

  快速发现 Bug:写完代码马上测,不用等到上线才被发现问题。

  减少回归成本:以后代码改动或升级,只要一键跑测试,就能知道改动有没有影响其他功能。

  保证代码质量:养成单元测试的习惯,会促使你把代码设计得更简洁和更容易测试。

  简单说:花小时间写单元测试,能为你省下大时间修 Bug。

  3. 环境准备

  3.1 依赖

  在一个常规的 Spring Boot 项目中,只要在 pom.xml(Maven)或 build.gradle(Gradle) 里加上:

<!-- 如果是 Maven -->
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
  </dependency>

 

·JUnit 5:最常用的Java测试框架(写 @Test 方法)

  · Mockito:常用的“模拟”库(用来Mock其他依赖)

  · AssertJ / Hamcrest:更好用的断言库

  · Spring Test / Spring Boot Test:Spring官方提供的测试辅助

  这也就够了,一般不需要额外安装别的。

  3.2 项目结构

  Spring Boot常见的目录结构(Maven示例):

  src
   ├─ main
   │   └─ java
   │       └─ com.example.demo
   │           ├─ DemoApplication.java
   │           └─ service
   │               └─ MyService.java
   └─ test
       └─ java
           └─ com.example.demo
               ├─ DemoApplicationTests.java
               └─ service
                   └─ MyServiceTest.java

 

 src/main/java 放你的业务代码。

  src/test/java 放你的测试代码。

  通常测试类的包路径要和被测类一致,这样在IDE里能很快对上号,也方便管理。

  4. 最最简单的单元测试示例(不依赖Spring)

  先从“纯JUnit”说起,最简单的情况就是:

  我有一个普通的工具类/方法

  我就想测试它的输入输出对不对

  不用装载Spring,也不用什么复杂注解

  代码示例

  假设我们有一个简单的工具类:

public class MathUtil {
      public static int add(int a, int b) {
          return a + b;
      }
  }

那我们写一个测试类(路径:src/test/java/.../MathUtilTest.java):

 import org.junit.jupiter.api.Assertions;
  import org.junit.jupiter.api.Test;
   
  public class MathUtilTest {
   
      @Test
      void testAdd() {
          int result = MathUtil.add(2, 3);
          Assertions.assertEquals(5, result, "2 + 3 应该等于 5");
      }
  }

@Test 表示这是一个测试方法。

  Assertions.assertEquals(期望值, 实际值, "提示信息") 用来断言。

  如果断言不通过,测试就失败;通过则测试成功。

  运行方法:

  在 IDE(如 IntelliJ/ Eclipse)里,右键这个 MathUtilTest 类 -> Run 'MathUtilTest'

  或者在命令行里运行 mvn test(Maven) / gradle test(Gradle)。

  这就是最最基础的单元测试。

  5. 在 Spring Boot 里测试 - Service层

  当你要测试一个 Service(业务逻辑类) 时,它可能依赖其他Bean(例如 Repository、Dao 等)或者需要 Autowired。在 Spring Boot 里,有两种主要方法:

  方法1:纯Mock(不启动Spring Context)

  适合只想测试这个Service逻辑本身,不需要真的连数据库,也不需要整个Spring环境。速度最快。

  用 Mockito 来创建一个假的(Mock)依赖。

  注入到要测的Service里,这样你可以控制依赖的行为。

  示例

  UserRepository.java (假设它是个接口,用来访问数据库):

 public interface UserRepository {
      User findByName(String name);
      // ... 其他方法
  }

UserService.java (我们要测这个类):

public class UserService {
   
      private UserRepository userRepository;
   
      // 通过构造注入依赖
      public UserService(UserRepository userRepository) {
          this.userRepository = userRepository;
      }
   
      public String getUserNickname(String name) {
          User user = userRepository.findByName(name);
          if (user == null) {
              return "UNKNOWN";
          }
          return user.getNickname();
      }
  }

UserServiceTest.java (测试类,不依赖 Spring):

import org.junit.jupiter.api.Test;
  import org.junit.jupiter.api.Assertions;
  import org.mockito.Mockito;
  import org.mockito.Mock;
  import org.mockito.InjectMocks;
  import org.mockito.junit.jupiter.MockitoExtension;
  import org.junit.jupiter.api.extension.ExtendWith;
   
  @ExtendWith(MockitoExtension.class) // JUnit5 启用Mockito
  public class UserServiceTest {
   
      @Mock
      private UserRepository userRepository; // Mock出来的依赖
   
      @InjectMocks
      private UserService userService;       // 要测试的对象,会把上面这个Mock自动注入进来
   
      @Test
      void testGetUserNickname_found() {
          // 1. 假设我们模拟一个“数据库中查到的用户”:
          User mockUser = new User();
          mockUser.setName("alice");
          mockUser.setNickname("AliceWonder");
   
          // 2. 定义假数据的返回行为
          Mockito.when(userRepository.findByName("alice")).thenReturn(mockUser);
   
          // 3. 调用被测方法
          String nickname = userService.getUserNickname("alice");
   
          // 4. 断言结果
          Assertions.assertEquals("AliceWonder", nickname);
      }
   
      @Test
      void testGetUserNickname_notFound() {
          // 没有设置when,则默认返回null
          String nickname = userService.getUserNickname("bob");
          Assertions.assertEquals("UNKNOWN", nickname);
      }
  }

使用了 @Mock 注解声明要模拟的依赖 userRepository。

  使用了 @InjectMocks 注解告诉 Mockito,要把所有标记 @Mock 的对象注入进 UserService。

  这样就能让 UserService 这个对象在执行时使用模拟过的 userRepository 而不访问真实数据库。

  然后通过 Mockito.when(...) 来定义依赖方法的返回值,用于测试用例的前提条件设置。

  通过 Assertions 来验证执行结果是否符合预期。

  这样就只测 UserService 的逻辑,不会真的访问数据库,也不需要启动Spring,执行很快。

  方法2:使用 @SpringBootTest (集成上下文)

  适合你想在测试时使用Spring管理Bean,比如自动注入 @Autowired,或想测试和别的Bean的连接配置是否正常。

  在测试类上加 @SpringBootTest。

  这样Spring容器会启动,你也能 @Autowired 你的Service或者别的Bean。

  示例

  UserService.java (类似前面,只不过换成了 Spring注入):

@Service
  public class UserService {
      @Autowired
      private UserRepository userRepository;
   
      public String getUserNickname(String name) {
          User user = userRepository.findByName(name);
          if (user == null) {
              return "UNKNOWN";
          }
          return user.getNickname();
      }
  }

UserServiceSpringTest.java (测试类,使用Spring上下文):

@SpringBootTest
  public class UserServiceSpringTest {
   
      @Autowired
      private UserService userService;
   
      @MockBean
      private UserRepository userRepository; 
      // @MockBean的意思:Spring 启动时,
      // 把真正的UserRepository替换成一个Mock对象,
      // 我们就可以定义它的返回值,而不会真的连数据库
   
      @Test
      void testGetUserNickname_found() {
          User mockUser = new User();
          mockUser.setName("alice");
          mockUser.setNickname("AliceWonder");
   
          Mockito.when(userRepository.findByName("alice")).thenReturn(mockUser);
   
          String result = userService.getUserNickname("alice");
          Assertions.assertEquals("AliceWonder", result);
      }
   
      @Test
      void testGetUserNickname_notFound() {
          // 不设置when就会返回null
          String result = userService.getUserNickname("unknown");
          Assertions.assertEquals("UNKNOWN", result);
      }
  }

 @SpringBootTest会启动一个小型Spring环境,让 @Autowired 能起作用。

  @MockBean 可以让你把某个Bean(比如 UserRepository)变成一个模拟对象。

  整体执行依然比较快,但比纯Mock稍微慢一点,因为要先启动Spring容器。

  6. 测试 Controller 层

  在 Spring Boot 里,Controller 是对外的 HTTP 接口。最常见的两种测试方式:

  用 @WebMvcTest + MockMvc:不启动整个应用,只启动Web层,速度较快;

  用 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + TestRestTemplate:会真正启动一个内嵌服务器,发起真实HTTP请求,更贴近实际环境。

  6.1 @WebMvcTest 示例

  @WebMvcTest(UserController.class) // 表示只测 UserController 相关

public class UserControllerTest {
   
      @Autowired
      private MockMvc mockMvc; // 用来模拟HTTP请求
   
      @MockBean
      private UserService userService; // Mock掉Service层
   
      @Test
      void testGetUser() throws Exception {
          // 假设Service返回一个User对象
          User mockUser = new User();
          mockUser.setName("test");
          mockUser.setNickname("TestNick");
   
          // 定义service行为
          Mockito.when(userService.getUserNickname("test")).thenReturn("TestNick");
   
          // 用MockMvc发起GET请求,对应Controller的 /user/{name} 路径
          mockMvc.perform(MockMvcRequestBuilders.get("/user/test"))
              .andExpect(MockMvcResultMatchers.status().isOk())
              .andExpect(MockMvcResultMatchers.content().string("TestNick"));
      }
  }

@WebMvcTest 只会扫描和加载 Web 层相关的组件,不会启动整个 Spring Boot 应用,测试速度更快。

  mockMvc.perform(get("/users/1")) 可以模拟一次 GET 请求到 /users/1,并断言返回的 JSON 结构和内容。

  6.2 @SpringBootTest + TestRestTemplate

  如果你想做一个更真实的集成测试(包括 Controller、Service、Repository 等所有层),可以使用 @SpringBootTest 并设置 webEnvironment = RANDOM_PORT 或 DEFINED_PORT 来启动内嵌服务器,然后注入 TestRestTemplate 来请求:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  public class UserControllerIntegrationTest {
   
      @Autowired
      private TestRestTemplate restTemplate; // 可以真的发请求
   
      @Test
      void testGetUser() {
          // 假设数据库里已经有对应数据,或者你用 @MockBean 替换依赖
          String result = restTemplate.getForObject("/user/test", String.class);
          Assertions.assertEquals("TestNick", result);
      }
  }

 这里会真正启动一个随机端口的Tomcat,然后 TestRestTemplate 真的去请求本地这个 /user/test 接口。

  非常贴近真实部署,只是适合做集成测试,比前面的MockMvc测试稍慢一点。

  7. 常见的断言与技巧

  7.1 断言

  Assertions.assertEquals(期望, 实际):断言二者相等。

  Assertions.assertTrue(条件):断言条件为真。

  Assertions.assertThrows(异常类型, 代码块):断言执行代码块会抛出指定异常。

  例如:

 @Test
  void testThrowException() {
      Assertions.assertThrows(IllegalArgumentException.class, () -> {
          // 假设调用了一个会抛出异常的方法
          someMethod(null);
      });
  }

7.2 Mock时常用的 Mockito 方法

  Mockito.when( mockObj.方法(...) ).thenReturn(返回值);

  Mockito.when( mockObj.方法(...) ).thenThrow(异常);

  Mockito.verify( mockObj, Mockito.times(1) ).某方法(...); // 验证是否调用了某方法

  8. 测试运行与整合

  8.1 在本地IDE里运行

  右键单个测试类或测试方法 -> Run

  或者在项目主目录运行 mvn test / gradle test

  8.2 与持续集成(CI)整合

  在 Jenkins、GitLab CI、GitHub Actions 等环境里,一般只要执行 mvn test 或 gradle test 就可以跑所有测试用例。

  如果测试全部通过,就说明代码基本没问题;如果测试挂了,说明你这次提交的改动有Bug或者破坏了原有逻辑。

  9. 流程小结(简版“使用指南”)

  新手首次写单元测试:

  在 src/test/java 下创建和源代码同包路径的测试类:XXXTest.java。

  在类里加 @Test 注解的方法,里面写 Assertions.assertXXX(...)。

  右键运行,看输出是否通过。

  要测Service逻辑,但不想连数据库:

  在测试类上写:

 @ExtendWith(MockitoExtension.class)
  public class MyServiceTest {
      @Mock
      private MyRepository myRepository;
   
      @InjectMocks
      private MyService myService;
      ...
  }

用 Mockito.when(...) 来模拟依赖。

  用 assertEquals(...) 来判断结果。

  要测Service逻辑,并用Spring上下文:

  在测试类上加 @SpringBootTest。

  注入 Service:@Autowired private MyService myService;

  如果你不想真的连数据库,那就用 @MockBean MyRepository myRepository;

  要测Controller:

  用 @WebMvcTest(MyController.class) + @MockBean MyService myService; + MockMvc 做单元测试,速度较快;

  或者用 @SpringBootTest(webEnvironment = ... ) + TestRestTemplate 做近似真实的集成测试。

  10. 其他常见问题

  测试和生产环境的配置冲突了怎么办?

  可以在 application-test.yml 里放测试专用配置,然后在测试时用 spring.profiles.active=test。

  需要数据库的测试怎么办?

  可以用@DataJpaTest+内存数据库(比如 H2),只测JPA相关逻辑,不影响真数据库。

  想看覆盖率怎么办?

  可以集成 Jacoco 插件,跑 mvn test 后生成覆盖率报告,看你的测试是不是覆盖到了主要逻辑。

  测试很慢怎么办?

  如果你的逻辑不是必须要Spring,就尽量用纯Mock,不用 @SpringBootTest。

  如果只是测Controller,就用 @WebMvcTest,不要启动全部。

 

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取   

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐