JUnit 单元测试详细使用指南

JUnit 单元测试详细使用指南

1. JUnit 概述

JUnit 是 Java 最流行的单元测试框架,用于编写和运行可重复的测试。

1.1 JUnit 版本

  • JUnit 4:使用注解,需要手动导入
  • JUnit 5:模块化设计,功能更强大(推荐)

2. 环境配置

2.1 Maven 依赖

xml 复制代码
<!-- JUnit 5 -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>
</dependency>

<!-- JUnit 4 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

2.2 项目结构

复制代码
src/
├── main/
│   └── java/
│       └── com/example/
│           └── UserService.java
└── test/
    └── java/
        └── com/example/
            └── UserServiceTest.java

3. JUnit 5 核心用法

3.1 基本测试类

java 复制代码
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    
    private final Calculator calculator = new Calculator();
    
    @Test
    void testAddition() {
        // 给定条件
        int a = 5;
        int b = 3;
        
        // 执行操作
        int result = calculator.add(a, b);
        
        // 验证结果
        assertEquals(8, result, "5 + 3 应该等于 8");
    }
    
    @Test
    void testDivision() {
        assertEquals(2, calculator.divide(6, 3));
    }
    
    @Test
    void testDivisionByZero() {
        // 测试异常情况
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(5, 0);
        });
    }
}

3.2 生命周期注解

java 复制代码
import org.junit.jupiter.api.*;

class LifecycleTest {
    
    @BeforeAll
    static void setUpBeforeAll() {
        System.out.println("在所有测试方法之前执行一次 - 用于初始化静态资源");
    }
    
    @AfterAll
    static void tearDownAfterAll() {
        System.out.println("在所有测试方法之后执行一次 - 用于清理静态资源");
    }
    
    @BeforeEach
    void setUp() {
        System.out.println("在每个测试方法之前执行 - 用于初始化测试数据");
    }
    
    @AfterEach
    void tearDown() {
        System.out.println("在每个测试方法之后执行 - 用于清理资源");
    }
    
    @Test
    void testOne() {
        System.out.println("执行测试一");
        assertTrue(true);
    }
    
    @Test
    void testTwo() {
        System.out.println("执行测试二");
        assertFalse(false);
    }
}

3.3 断言方法

java 复制代码
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class AssertionsTest {
    
    @Test
    void testVariousAssertions() {
        String nullString = null;
        String actualString = "Hello JUnit";
        
        // 基本断言
        assertEquals(4, 2 + 2);
        assertNotEquals(3, 1 + 1);
        
        // 布尔断言
        assertTrue(5 > 3);
        assertFalse(5 < 3);
        
        // 空值断言
        assertNull(nullString);
        assertNotNull(actualString);
        
        // 数组断言
        assertArrayEquals(new int[]{1, 2, 3}, new int[]{1, 2, 3});
        
        // 异常断言
        Exception exception = assertThrows(ArithmeticException.class, () -> {
            int result = 1 / 0;
        });
        
        // 超时断言
        assertTimeout(Duration.ofSeconds(1), () -> {
            Thread.sleep(500);
        });
        
        // 组合断言 - 所有断言都必须通过
        assertAll("多个断言",
            () -> assertEquals(4, 2 + 2),
            () -> assertTrue(5 > 3),
            () -> assertNotNull(actualString)
        );
    }
}

4. 高级特性

4.1 参数化测试

java 复制代码
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

class ParameterizedTestExample {
    
    @ParameterizedTest
    @ValueSource(ints = {1, 3, 5, -3, 15})
    void testIsOdd(int number) {
        assertTrue(number % 2 != 0);
    }
    
    @ParameterizedTest
    @CsvSource({
        "2, 3, 5",
        "0, 0, 0",
        "-1, 2, 1",
        "10, -5, 5"
    })
    void testAddition(int a, int b, int expected) {
        Calculator calculator = new Calculator();
        assertEquals(expected, calculator.add(a, b));
    }
    
    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithMethodSource(String argument) {
        assertNotNull(argument);
    }
    
    static Stream<String> stringProvider() {
        return Stream.of("apple", "banana", "orange");
    }
}

4.2 测试套件

java 复制代码
import org.junit.platform.suite.api.*;

@Suite
@SelectPackages("com.example.service")
@IncludeClassNamePatterns(".*Test")
@ExcludeTags("slow")
class TestSuite {
    // 运行 com.example.service 包下所有以 Test 结尾的测试类,排除标记为 slow 的测试
}

4.3 条件测试

java 复制代码
import org.junit.jupiter.api.condition.*;

class ConditionalTest {
    
    @Test
    @EnabledOnOs(OS.WINDOWS)
    void onlyOnWindows() {
        // 只在 Windows 系统上运行
    }
    
    @Test
    @DisabledOnOs(OS.MAC)
    void notOnMac() {
        // 不在 Mac 系统上运行
    }
    
    @Test
    @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
    void onlyOn64BitArchitecture() {
        // 只在 64 位架构上运行
    }
    
    @Test
    @EnabledIf("customCondition")
    void onlyIfCustomCondition() {
        // 只在自定义条件满足时运行
    }
    
    boolean customCondition() {
        return System.getProperty("custom.flag") != null;
    }
}

5. 实际开发中的测试示例

5.1 Service 层测试

java 复制代码
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testCreateUser() {
        // 准备测试数据
        User user = new User("john@example.com", "John Doe");
        
        // 设置 Mock 行为
        when(userRepository.save(any(User.class))).thenReturn(user);
        doNothing().when(emailService).sendWelcomeEmail(anyString());
        
        // 执行测试
        User createdUser = userService.createUser(user);
        
        // 验证结果
        assertNotNull(createdUser);
        assertEquals("john@example.com", createdUser.getEmail());
        
        // 验证 Mock 交互
        verify(userRepository, times(1)).save(user);
        verify(emailService, times(1)).sendWelcomeEmail("john@example.com");
    }
    
    @Test
    void testCreateUserWithDuplicateEmail() {
        User user = new User("existing@example.com", "Existing User");
        
        when(userRepository.findByEmail("existing@example.com"))
            .thenReturn(Optional.of(user));
        
        // 测试异常情况
        assertThrows(DuplicateEmailException.class, () -> {
            userService.createUser(user);
        });
        
        verify(userRepository, never()).save(any(User.class));
    }
}

5.2 MyBatis Mapper 测试

java 复制代码
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;

@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserMapperTest {
    
    @Autowired
    private UserMapper userMapper;
    
    @Test
    void testFindById() {
        // 假设测试数据已通过 @Sql 或数据库迁移工具准备
        User user = userMapper.findById(1L);
        
        assertNotNull(user);
        assertEquals("john@example.com", user.getEmail());
    }
    
    @Test
    void testInsertUser() {
        User newUser = new User();
        newUser.setEmail("test@example.com");
        newUser.setName("Test User");
        
        int result = userMapper.insert(newUser);
        
        assertEquals(1, result);
        assertNotNull(newUser.getId()); // 测试自增ID
        
        User savedUser = userMapper.findById(newUser.getId());
        assertEquals("test@example.com", savedUser.getEmail());
    }
}

5.3 Spring Boot 集成测试

java 复制代码
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testGetUser() throws Exception {
        // 准备测试数据
        User user = new User("test@example.com", "Test User");
        userRepository.save(user);
        
        // 执行并验证 HTTP 请求
        mockMvc.perform(get("/api/users/{id}", user.getId()))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.email").value("test@example.com"))
               .andExpect(jsonPath("$.name").value("Test User"));
    }
}

6. 测试最佳实践

6.1 测试命名规范

java 复制代码
class UserServiceTest {
    
    // 方式1:方法名描述行为
    @Test
    void shouldReturnUserWhenValidIdProvided() {
        // 测试逻辑
    }
    
    // 方式2:Given-When-Then 模式
    @Test
    void givenValidUserId_whenFindById_thenReturnUser() {
        // given - 准备数据
        Long userId = 1L;
        User expectedUser = new User(userId, "test@example.com");
        
        // when - 执行操作
        User actualUser = userService.findById(userId);
        
        // then - 验证结果
        assertEquals(expectedUser, actualUser);
    }
    
    // 方式3:简单描述
    @Test
    void testFindUserById() {
        // 测试逻辑
    }
}

6.2 测试组织结构

java 复制代码
class ComplexServiceTest {
    
    @Nested
    @DisplayName("创建用户场景")
    class CreateUserScenarios {
        
        @Test
        @DisplayName("当提供有效用户信息时,应该成功创建用户")
        void createUserWithValidData() {
            // 测试正常流程
        }
        
        @Test
        @DisplayName("当提供重复邮箱时,应该抛出重复邮箱异常")
        void createUserWithDuplicateEmail() {
            // 测试异常流程
        }
    }
    
    @Nested
    @DisplayName("更新用户场景")
    class UpdateUserScenarios {
        
        @Test
        void updateUserWithValidData() {
            // 测试更新逻辑
        }
    }
}

7. 常见易错点

7.1 测试独立性

java 复制代码
// 错误:测试之间有依赖
class DependentTest {
    private static int counter = 0;
    
    @Test
    void testOne() {
        counter++;
        assertEquals(1, counter); // 依赖于执行顺序
    }
    
    @Test
    void testTwo() {
        counter++;
        assertEquals(2, counter); // 可能失败
    }
}

// 正确:每个测试独立
class IndependentTest {
    
    @Test
    void testOne() {
        int result = calculate(1);
        assertEquals(2, result);
    }
    
    @Test
    void testTwo() {
        int result = calculate(2);
        assertEquals(3, result);
    }
    
    private int calculate(int input) {
        return input + 1;
    }
}

7.2 避免过于复杂的测试

java 复制代码
// 错误:一个测试验证太多功能
@Test
void testUserRegistration() {
    // 验证用户创建
    // 验证邮件发送
    // 验证日志记录
    // 验证数据库状态
    // 验证缓存更新
    // ... 过于复杂
}

// 正确:拆分关注点
@Test
void testUserCreation() {
    // 只关注用户创建
}

@Test 
void testEmailSending() {
    // 只关注邮件发送
}

@Test
void testLogging() {
    // 只关注日志记录
}

7.3 正确的异常测试

java 复制代码
// 错误:在 try-catch 中手动处理异常
@Test
void testExceptionWrong() {
    try {
        userService.divide(1, 0);
        fail("应该抛出异常");
    } catch (ArithmeticException e) {
        // 测试通过
    }
}

// 正确:使用 assertThrows
@Test
void testExceptionCorrect() {
    assertThrows(ArithmeticException.class, () -> {
        userService.divide(1, 0);
    });
}

8. 常用测试模式

8.1 测试数据构建器

java 复制代码
class UserTestBuilder {
    
    private Long id;
    private String email = "default@example.com";
    private String name = "Default User";
    
    public static UserTestBuilder aUser() {
        return new UserTestBuilder();
    }
    
    public UserTestBuilder withEmail(String email) {
        this.email = email;
        return this;
    }
    
    public UserTestBuilder withName(String name) {
        this.name = name;
        return this;
    }
    
    public User build() {
        User user = new User();
        user.setId(id);
        user.setEmail(email);
        user.setName(name);
        return user;
    }
}

// 使用构建器
@Test
void testWithBuilder() {
    User user = UserTestBuilder.aUser()
            .withEmail("test@example.com")
            .withName("Test User")
            .build();
    
    User savedUser = userRepository.save(user);
    assertNotNull(savedUser.getId());
}

通过掌握这些 JUnit 使用方法和最佳实践,你可以编写出高质量、可维护的单元测试,显著提升代码质量和开发效率。

相关推荐
软件检测小牛玛17 小时前
具备软件功能测试资质的机构哪家更权威?山东软件测评机构 中承信安
功能测试·单元测试·软件测试报告·软件测评机构
Traced back18 小时前
SQL Server 核心语法+进阶知识点大全(小白版)
数据库·sqlserver
山岚的运维笔记18 小时前
SQL Server笔记 -- 第14章:CASE语句
数据库·笔记·sql·microsoft·sqlserver
闻哥20 小时前
从测试坏味道到优雅实践:打造高质量单元测试
java·面试·单元测试·log4j·springboot
松涛和鸣1 天前
70、IMX6ULL LED驱动实战
linux·数据库·驱动开发·postgresql·sqlserver
Warren981 天前
Pytest Fixture 作用域与接口测试 Token 污染问题实战解析
功能测试·面试·单元测试·集成测试·pytest·postman·模块测试
知行合一。。。1 天前
程序中的log4j、stderr、stdout日志
python·单元测试·log4j
UpYoung!1 天前
【SQL Server 2019】企业级数据库系统—数据库SQL Server 2019保姆级详细图文下载安装完全指南
运维·数据库·sqlserver·运维开发·数据库管理·开发工具·sqlserver2019
知识分享小能手1 天前
SQL Server 2019入门学习教程,从入门到精通,SQL Server 2019 数据表的操作 —语法详解与实战案例(3)
数据库·学习·sqlserver