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 使用方法和最佳实践,你可以编写出高质量、可维护的单元测试,显著提升代码质量和开发效率。

相关推荐
Knight_AL3 小时前
Java 单元测试全攻略:JUnit 生命周期、覆盖率提升、自动化框架与 Mock 技术
java·junit·单元测试
cgsthtm5 小时前
SQL Server自动定时备份还原到另一台服务器
sqlserver·定时备份还原·任务计划程序·映射网络驱动器·代理作业
韩立学长21 小时前
【开题答辩实录分享】以《制造型企业供应商档案管理系统设计与开发》为例进行答辩实录分享
sqlserver·c#
yunmi_1 天前
安全框架 SpringSecurity 入门(超详细,IDEA2024)
java·spring boot·spring·junit·maven·mybatis·spring security
许长安2 天前
Redis(二)——Redis协议与异步方式
数据库·redis·junit
小熊出擊2 天前
【pytest】fixture 内省(Introspection)测试上下文
python·单元测试·pytest
杨云龙UP2 天前
小工具大体验:rlwrap加持下的Oracle/MySQL/SQL Server命令行交互
运维·服务器·数据库·sql·mysql·oracle·sqlserver
小熊出擊3 天前
【pytest】finalizer 执行顺序:FILO 原则
python·测试工具·单元测试·pytest
xjf77113 天前
Nx项目中使用Vitest对原生JS组件进行单元测试
javascript·单元测试·前端框架·nx·vitest·前端测试