JUnit 5的 Assertions

JUnit Jupiter Assertions 是 JUnit 5 测试框架的核心断言库,用于在测试中验证预期结果与实际结果是否一致。它提供了一组丰富、灵活的静态方法,使得测试断言更加直观和易读。

主要特点

  1. 静态导入:推荐静态导入断言方法

  2. 丰富的断言类型:支持多种数据类型和条件

  3. 失败消息定制:支持自定义失败消息

  4. 流式API:部分断言支持链式调用

  5. Lambda支持:延迟消息评估

与 JUnit 4 的对比

特性 JUnit 4 (org.junit.Assert) JUnit 5 (org.junit.jupiter.api.Assertions)
包名 org.junit.Assert org.junit.jupiter.api.Assertions
方法命名 驼峰命名 (assertEquals) 同样驼峰命名,但更多方法
超时测试 @Test(timeout = ...) assertTimeout()
异常测试 @Test(expected = ...) assertThrows()
分组断言 assertAll()
Lambda支持 支持延迟消息
实例检查 assertInstanceOf()

断言方法

基本断言

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

// 相等性断言
assertEquals(expected, actual);
assertEquals(expected, actual, "自定义失败消息");
assertEquals(expected, actual, () -> "延迟评估的消息");

// 不相等断言
assertNotEquals(unexpected, actual);

// 对象引用断言
assertSame(expected, actual);      // 同一对象
assertNotSame(unexpected, actual); // 不同对象

// 空值检查
assertNull(actual);
assertNotNull(actual);

布尔和条件断言

java 复制代码
// 条件断言
assertTrue(condition);
assertFalse(condition);

// 基于Supplier的条件断言
assertTrue(() -> condition);
assertTrue(condition, () -> "失败时显示的消息");

异常断言

java 复制代码
// 验证是否抛出指定异常
Exception exception = assertThrows(
    ExpectedExceptionType.class,
    () -> codeThatShouldThrowException()
);

// 获取异常详细信息
assertEquals("Expected message", exception.getMessage());

// 验证没有抛出异常
assertDoesNotThrow(() -> codeThatShouldNotThrowException());

超时断言

java 复制代码
// 验证执行时间不超过指定时间
assertTimeout(Duration.ofSeconds(1), () -> {
    // 测试代码
});

// 可中断的超时断言(超时后继续执行)
assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
    // 测试代码
});

数组和可迭代对象断言

java 复制代码
// 数组断言
assertArrayEquals(expectedArray, actualArray);

// 可迭代对象断言
Iterable<String> expected = Arrays.asList("a", "b", "c");
Iterable<String> actual = Arrays.asList("a", "b", "c");
assertIterableEquals(expected, actual);

// 行迭代器断言(忽略空白和排序)
assertLinesMatch(List<String> expectedLines, List<String> actualLines);

复合断言(assertAll)

java 复制代码
@Test
void multipleAssertions() {
    Person person = new Person("zhangsan", "lisi");
    
    assertAll("person",
        () -> assertEquals("zhangsan", person.getFirstName()),
        () -> assertEquals("lisi", person.getLastName()),
        () -> assertNotNull(person.getId())
    );
}
// 所有断言都会执行,即使某些失败

失败断言

java 复制代码
// 直接使测试失败
fail("测试应该失败");
fail(() -> "延迟评估的失败消息");

assertInstanceOf

java 复制代码
@Test
void testInstanceOf() {
    Object obj = "Hello";
    
    // 验证对象是指定类型的实例
    String result = assertInstanceOf(String.class, obj);
    assertEquals("Hello", result);
    
    // 带自定义消息
    assertInstanceOf(String.class, obj, "应该是字符串类型");
}

assertNotEquals 的 Delta 支持

java 复制代码
@Test
void testNotEqualsWithDelta() {
    // 浮点数不相等断言,带误差范围
    assertNotEquals(1.0, 1.1, 0.01);
    // 这行会失败,因为 1.0 和 1.1 的差 > 0.01
}

使用 Lambda 表达式延迟消息评估

java 复制代码
@Test
void testWithExpensiveMessage() {
    String actual = expensiveOperation();
    
    // 好的做法:消息延迟评估
    assertEquals("expected", actual,
        () -> "失败消息,只在实际失败时计算: " + expensiveMessage());
    
    // 避免的做法:总是计算消息
    assertEquals("expected", actual,
        "失败消息: " + expensiveMessage()); // 即使测试通过也会计算
}

使用 assertAll 进行分组断言

java 复制代码
@Test
void testPerson() {
    Person person = service.findPerson(1);
    
    // 所有断言都会执行,提供完整的问题视图
    assertAll("person properties",
        () -> assertNotNull(person.getId(), "id should not be null"),
        () -> assertEquals("John", person.getFirstName()),
        () -> assertEquals(30, person.getAge()),
        () -> assertTrue(person.isActive())
    );
}

异常测试

java 复制代码
@Test
void testException() {
    // 验证异常类型和消息
    IllegalArgumentException exception = assertThrows(
        IllegalArgumentException.class,
        () -> validator.validate(null),
        "Expected validate() to throw"
    );
    
    // 进一步验证异常详情
    assertEquals("Value cannot be null", exception.getMessage());
    assertEquals("VALIDATION_ERROR", exception.getErrorCode());
}
java 复制代码
public class CustomAssertions {
    
    public static void assertValidEmail(String email) {
        assertNotNull(email, "Email should not be null");
        assertTrue(email.contains("@"), 
            () -> "Email should contain '@': " + email);
        assertTrue(email.length() >= 5,
            () -> "Email should be at least 5 chars: " + email);
    }
    
    public static void assertBetween(int value, int min, int max) {
        assertAll(
            () -> assertTrue(value >= min, 
                () -> String.format("%d should be >= %d", value, min)),
            () -> assertTrue(value <= max,
                () -> String.format("%d should be <= %d", value, max))
        );
    }
}

// 使用自定义断言
@Test
void testCustomAssertions() {
    CustomAssertions.assertValidEmail("zhangsan@demo.com");
    CustomAssertions.assertBetween(age, 18, 65);
}

实际示例

User

java 复制代码
import java.time.LocalDateTime;
import java.util.UUID;

/**
 * 用户认证系统 - 功能级测试
 * 测试用户注册、登录、密码重置等核心功能
 */

// 用户实体类
public class User {
    private String id;
    private String username;
    private String email;
    private String hashedPassword;
    private boolean isActive;
    private boolean isLocked;
    private int failedLoginAttempts;
    private LocalDateTime createdAt;
    private LocalDateTime lastLoginAt;

    public User(String username, String email, String hashedPassword) {
        this.id = UUID.randomUUID().toString();
        this.username = username;
        this.email = email;
        this.hashedPassword = hashedPassword;
        this.isActive = true;
        this.isLocked = false;
        this.failedLoginAttempts = 0;
        this.createdAt = LocalDateTime.now();
    }

    public boolean verifyPassword(String password) {
        // 简化的密码验证(实际应使用BCrypt等)
        return this.hashedPassword.equals(hashPassword(password));
    }

    public void recordSuccessfulLogin() {
        this.lastLoginAt = LocalDateTime.now();
        this.failedLoginAttempts = 0;
        this.isLocked = false;
    }

    public void recordFailedLogin() {
        this.failedLoginAttempts++;
        if (this.failedLoginAttempts >= 5) {
            this.isLocked = true;
        }
    }

    public void resetPassword(String newHashedPassword) {
        this.hashedPassword = newHashedPassword;
        this.failedLoginAttempts = 0;
        this.isLocked = false;
    }

    public void deactivate() {
        this.isActive = false;
    }

    public void activate() {
        this.isActive = true;
    }

    private String hashPassword(String password) {
        // 简化版本 - 实际应使用安全的哈希算法
        return "HASHED_" + password;
    }

    // Getters
    public String getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public String getEmail() {
        return email;
    }

    public boolean isActive() {
        return isActive;
    }

    public boolean isLocked() {
        return isLocked;
    }

    public int getFailedLoginAttempts() {
        return failedLoginAttempts;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public LocalDateTime getLastLoginAt() {
        return lastLoginAt;
    }
}

UserRepository

java 复制代码
import java.util.HashMap;
import java.util.Map;

// 用户仓库(模拟数据库)
public class UserRepository {
    private Map<String, User> usersById = new HashMap<>();
    private Map<String, User> usersByUsername = new HashMap<>();
    private Map<String, User> usersByEmail = new HashMap<>();

    public User save(User user) {
        usersById.put(user.getId(), user);
        usersByUsername.put(user.getUsername(), user);
        usersByEmail.put(user.getEmail(), user);
        return user;
    }

    public User findById(String id) {
        return usersById.get(id);
    }

    public User findByUsername(String username) {
        return usersByUsername.get(username);
    }

    public User findByEmail(String email) {
        return usersByEmail.get(email);
    }

    public boolean existsByUsername(String username) {
        return usersByUsername.containsKey(username);
    }

    public boolean existsByEmail(String email) {
        return usersByEmail.containsKey(email);
    }

    public void delete(String id) {
        User user = usersById.remove(id);
        if (user != null) {
            usersByUsername.remove(user.getUsername());
            usersByEmail.remove(user.getEmail());
        }
    }
}

UserService

java 复制代码
// 用户服务 - 核心业务逻辑
public class UserService {
    private UserRepository userRepository;
    private EmailValidator emailValidator;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
        this.emailValidator = new EmailValidator();
    }

    public User register(String username, String email, String password) {
        // 验证输入
        validateRegistrationInput(username, email, password);

        // 检查唯一性
        if (userRepository.existsByUsername(username)) {
            throw new IllegalArgumentException("用户名已存在: " + username);
        }

        if (userRepository.existsByEmail(email)) {
            throw new IllegalArgumentException("邮箱已被注册: " + email);
        }

        // 创建用户
        String hashedPassword = hashPassword(password);
        User user = new User(username, email, hashedPassword);

        return userRepository.save(user);
    }

    public User login(String username, String password) {
        User user = userRepository.findByUsername(username);

        if (user == null) {
            throw new SecurityException("用户不存在或密码错误");
        }

        if (!user.isActive()) {
            throw new SecurityException("账户已被禁用");
        }

        if (user.isLocked()) {
            throw new SecurityException("账户已被锁定,请重置密码");
        }

        if (user.verifyPassword(password)) {
            user.recordSuccessfulLogin();
            return user;
        } else {
            user.recordFailedLogin();
            throw new SecurityException("用户名或密码错误");
        }
    }

    public void resetPassword(String email, String newPassword) {
        User user = userRepository.findByEmail(email);

        if (user == null) {
            throw new IllegalArgumentException("邮箱未注册: " + email);
        }

        validatePassword(newPassword);

        String hashedPassword = hashPassword(newPassword);
        user.resetPassword(hashedPassword);
    }

    public void updateEmail(String userId, String newEmail) {
        User user = userRepository.findById(userId);

        if (user == null) {
            throw new IllegalArgumentException("用户不存在");
        }

        if (!emailValidator.isValid(newEmail)) {
            throw new IllegalArgumentException("无效的邮箱格式: " + newEmail);
        }

        if (userRepository.existsByEmail(newEmail) &&
                !user.getEmail().equals(newEmail)) {
            throw new IllegalArgumentException("邮箱已被使用: " + newEmail);
        }

        userRepository.delete(user.getId());
        user = new User(user.getUsername(), newEmail, user.getUsername());
        userRepository.save(user);
    }

    private void validateRegistrationInput(String username, String email, String password) {
        if (username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("用户名不能为空");
        }

        if (username.length() < 3 || username.length() > 20) {
            throw new IllegalArgumentException("用户名长度必须在3-20个字符之间");
        }

        if (!username.matches("^[a-zA-Z0-9_]+$")) {
            throw new IllegalArgumentException("用户名只能包含字母、数字和下划线");
        }

        if (!emailValidator.isValid(email)) {
            throw new IllegalArgumentException("无效的邮箱地址: " + email);
        }

        validatePassword(password);
    }

    private void validatePassword(String password) {
        if (password == null || password.length() < 8) {
            throw new IllegalArgumentException("密码长度至少8位");
        }

        // 检查密码强度
        boolean hasUpperCase = !password.equals(password.toLowerCase());
        boolean hasLowerCase = !password.equals(password.toUpperCase());
        boolean hasDigit = password.matches(".*\\d.*");

        if (!hasUpperCase || !hasLowerCase || !hasDigit) {
            throw new IllegalArgumentException(
                    "密码必须包含大小写字母和数字"
            );
        }
    }

    private String hashPassword(String password) {
        // 简化版本 - 实际应使用BCrypt
        return "HASHED_" + password;
    }

    public void deactivateUser(String userId) {
        User user = userRepository.findById(userId);
        if (user != null) {
            user.deactivate();
        }
    }

    public void activateUser(String userId) {
        User user = userRepository.findById(userId);
        if (user != null) {
            user.activate();
        }
    }
}

UserServiceFunctionalTest

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

import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

// 功能级测试类
@DisplayName("用户认证系统功能测试")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserServiceFunctionalTest {

    private UserRepository userRepository;
    private UserService userService;
    private User testUser;

    @BeforeAll
    void initializeServices() {
        userRepository = new UserRepository();
        userService = new UserService(userRepository);
    }

    @BeforeEach
    void setUpTestUser() {
        // 清理之前的测试数据
        userRepository = new UserRepository();
        userService = new UserService(userRepository);

        // 创建一个测试用户
        testUser = userService.register("testuser", "test@example.com", "Password123");
    }

    @Test
    @DisplayName("TC001: 用户成功注册")
    void testUserRegistrationSuccess() {
        // Given
        String username = "newuser";
        String email = "newuser@example.com";
        String password = "StrongPass123";

        // When
        User registeredUser = userService.register(username, email, password);

        // Then
        assertAll("注册用户验证",
                () -> assertNotNull(registeredUser, "用户应该成功创建"),
                () -> assertEquals(username, registeredUser.getUsername(), "用户名应该匹配"),
                () -> assertEquals(email, registeredUser.getEmail(), "邮箱应该匹配"),
                () -> assertTrue(registeredUser.isActive(), "新用户应该处于激活状态"),
                () -> assertFalse(registeredUser.isLocked(), "新用户不应该被锁定"),
                () -> assertEquals(0, registeredUser.getFailedLoginAttempts(), "失败登录次数应该为0"),
                () -> assertNotNull(registeredUser.getCreatedAt(), "创建时间不应该为空")
        );

        // 验证用户已保存到仓库
        User foundUser = userRepository.findByUsername(username);
        assertNotNull(foundUser, "用户应该能从仓库中找到");
        assertEquals(registeredUser.getId(), foundUser.getId(), "用户ID应该匹配");
    }

    @Test
    @DisplayName("TC002: 注册时用户名重复")
    void testDuplicateUsernameRegistration() {
        // Given
        String existingUsername = testUser.getUsername();
        String newEmail = "another@example.com";
        String password = "Password123";

        // When & Then
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> userService.register(existingUsername, newEmail, password)
        );

        assertEquals("用户名已存在: " + existingUsername, exception.getMessage());
    }

    @Test
    @DisplayName("TC003: 注册时邮箱重复")
    void testDuplicateEmailRegistration() {
        // Given
        String newUsername = "differentuser";
        String existingEmail = testUser.getEmail();
        String password = "Password123";

        // When & Then
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> userService.register(newUsername, existingEmail, password)
        );

        assertEquals("邮箱已被注册: " + existingEmail, exception.getMessage());
    }

    @ParameterizedTest(name = "无效用户名: {0}")
    @ValueSource(strings = {"ab", "", "   ", "verylongusernamethatiswaytoolong", "user@name"})
    @DisplayName("TC004: 注册时用户名格式无效")
    void testInvalidUsernameRegistration(String invalidUsername) {
        // When & Then
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> userService.register(invalidUsername, "test@example.com", "Password123")
        );

        assertNotNull(exception.getMessage());
        assertTrue(
                exception.getMessage().contains("用户名") ||
                        exception.getMessage().contains("空") ||
                        exception.getMessage().contains("长度") ||
                        exception.getMessage().contains("字符"),
                "异常消息应该描述用户名问题: " + exception.getMessage()
        );
    }

    @ParameterizedTest(name = "无效密码: {0}")
    @ValueSource(strings = {"short", "12345678", "abcdefgh", "ABCDEFGH", "noDigit", "NODIGIT"})
    @DisplayName("TC005: 注册时密码强度不足")
    void testWeakPasswordRegistration(String weakPassword) {
        // When & Then
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> userService.register("newuser", "test@example.com", weakPassword)
        );

        assertNotNull(exception.getMessage());
        assertTrue(
                exception.getMessage().contains("密码") ||
                        exception.getMessage().contains("长度") ||
                        exception.getMessage().contains("字母") ||
                        exception.getMessage().contains("数字"),
                "异常消息应该描述密码问题: " + exception.getMessage()
        );
    }

    @Test
    @DisplayName("TC006: 用户成功登录")
    void testSuccessfulLogin() {
        // Given
        String username = testUser.getUsername();
        String correctPassword = "Password123";

        // 记录登录前的状态
        LocalDateTime beforeLogin = LocalDateTime.now();

        // When
        User loggedInUser = userService.login(username, correctPassword);

        // Then
        assertAll("登录验证",
                () -> assertNotNull(loggedInUser, "登录应该返回用户对象"),
                () -> assertEquals(testUser.getId(), loggedInUser.getId(), "用户ID应该匹配"),
                () -> assertEquals(0, loggedInUser.getFailedLoginAttempts(), "失败登录次数应该重置为0"),
                () -> assertFalse(loggedInUser.isLocked(), "用户不应该被锁定"),
                () -> assertNotNull(loggedInUser.getLastLoginAt(), "最后登录时间应该被设置"),
                () -> assertTrue(
                        loggedInUser.getLastLoginAt().isAfter(beforeLogin) ||
                                loggedInUser.getLastLoginAt().equals(beforeLogin),
                        "最后登录时间应该在登录之后或同时"
                )
        );
    }

    @Test
    @DisplayName("TC007: 登录时密码错误")
    void testLoginWithWrongPassword() {
        // Given
        String username = testUser.getUsername();
        String wrongPassword = "WrongPassword123";

        // When & Then
        SecurityException exception = assertThrows(
                SecurityException.class,
                () -> userService.login(username, wrongPassword)
        );

        assertEquals("用户名或密码错误", exception.getMessage());

        // 验证失败登录被记录
        User userAfterFailedLogin = userRepository.findByUsername(username);
        assertEquals(1, userAfterFailedLogin.getFailedLoginAttempts(), "失败登录次数应该增加");
    }

    @Test
    @DisplayName("TC008: 连续5次密码错误导致账户锁定")
    void testAccountLockAfterMultipleFailedAttempts() {
        // Given
        String username = testUser.getUsername();
        String wrongPassword = "WrongPassword123";

        // When - 连续5次错误登录
        for (int i = 1; i <= 5; i++) {
            SecurityException exception = assertThrows(
                    SecurityException.class,
                    () -> userService.login(username, wrongPassword),
                    "第" + i + "次登录应该失败"
            );

            User user = userRepository.findByUsername(username);

            if (i < 5) {
                assertEquals("用户名或密码错误", exception.getMessage());
                assertFalse(user.isLocked(), "前4次失败不应该锁定账户");
                assertEquals(i, user.getFailedLoginAttempts(), "失败次数应该是" + i);
            } else {
                assertEquals("用户名或密码错误", exception.getMessage());
                assertTrue(user.isLocked(), "第5次失败应该锁定账户");
                assertEquals(5, user.getFailedLoginAttempts());
            }
        }

        // Then - 第6次尝试应该显示账户锁定
        SecurityException lockedException = assertThrows(
                SecurityException.class,
                () -> userService.login(username, "Password123")
        );

        assertEquals("账户已被锁定,请重置密码", lockedException.getMessage());
    }

    @Test
    @DisplayName("TC009: 登录已禁用账户")
    void testLoginDeactivatedAccount() {
        // Given
        userService.deactivateUser(testUser.getId());

        // When & Then
        SecurityException exception = assertThrows(
                SecurityException.class,
                () -> userService.login(testUser.getUsername(), "Password123")
        );

        assertEquals("账户已被禁用", exception.getMessage());
    }

    @Test
    @DisplayName("TC010: 成功重置密码")
    void testSuccessfulPasswordReset() {
        // Given
        String email = testUser.getEmail();
        String newPassword = "NewPass123!";

        // 假设账户因多次失败被锁定
        for (int i = 0; i < 5; i++) {
            try {
                userService.login(testUser.getUsername(), "WrongPass");
            } catch (SecurityException e) {
                // 预期中的失败
            }
        }

        User userBeforeReset = userRepository.findByEmail(email);
        assumeTrue(userBeforeReset.isLocked(), "账户应该被锁定");
        assertEquals(5, userBeforeReset.getFailedLoginAttempts());

        // When
        userService.resetPassword(email, newPassword);

        // Then
        User userAfterReset = userRepository.findByEmail(email);
        assertAll("密码重置验证",
                () -> assertFalse(userAfterReset.isLocked(), "重置后应该解锁账户"),
                () -> assertEquals(0, userAfterReset.getFailedLoginAttempts(), "失败次数应该重置"),
                () -> assertTrue(userAfterReset.verifyPassword(newPassword), "新密码应该生效")
        );

        // 验证可以用新密码登录
        User loggedInUser = userService.login(testUser.getUsername(), newPassword);
        assertNotNull(loggedInUser);
    }

    @Test
    @DisplayName("TC011: 重置不存在的邮箱密码")
    void testResetPasswordForNonExistentEmail() {
        // When & Then
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> userService.resetPassword("nonexistent@example.com", "NewPass123")
        );

        assertEquals("邮箱未注册: nonexistent@example.com", exception.getMessage());
    }

    @Test
    @DisplayName("TC012: 成功更新邮箱")
    void testUpdateEmailSuccess() {
        // Given
        String userId = testUser.getId();
        String newEmail = "updated@example.com";

        // When
        userService.updateEmail(userId, newEmail);

        // Then
        User updatedUser = userRepository.findById(userId);
        assertAll("邮箱更新验证",
                () -> assertNotNull(updatedUser, "用户应该存在"),
                () -> assertEquals(newEmail, updatedUser.getEmail(), "邮箱应该被更新"),
                () -> assertEquals(testUser.getUsername(), updatedUser.getUsername(), "用户名应该不变")
        );

        // 验证可以通过新邮箱找到用户
        User userByNewEmail = userRepository.findByEmail(newEmail);
        assertNotNull(userByNewEmail);
        assertEquals(userId, userByNewEmail.getId());
    }

    @Test
    @DisplayName("TC013: 更新为已存在的邮箱")
    void testUpdateToExistingEmail() {
        // Given - 创建第二个用户
        User anotherUser = userService.register("anotheruser", "another@example.com", "Password123");
        String existingEmail = anotherUser.getEmail();

        // When & Then
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> userService.updateEmail(testUser.getId(), existingEmail)
        );

        assertEquals("邮箱已被使用: " + existingEmail, exception.getMessage());
    }

    @ParameterizedTest(name = "无效邮箱: {0}")
    @CsvSource({
            "invalid-email",
            "user@",
            "@domain.com",
            "user@.com",
            "user@domain.",
            "user name@domain.com"
    })
    @DisplayName("TC014: 更新为无效邮箱格式")
    void testUpdateToInvalidEmail(String invalidEmail) {
        // When & Then
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> userService.updateEmail(testUser.getId(), invalidEmail)
        );

        assertEquals("无效的邮箱格式: " + invalidEmail, exception.getMessage());
    }

    @Test
    @DisplayName("TC015: 激活和禁用账户")
    void testActivateAndDeactivateAccount() {
        // Given
        String userId = testUser.getId();

        // When - 禁用账户
        userService.deactivateUser(userId);

        // Then
        User deactivatedUser = userRepository.findById(userId);
        assertFalse(deactivatedUser.isActive(), "账户应该被禁用");

        // When - 尝试登录已禁用账户
        SecurityException loginException = assertThrows(
                SecurityException.class,
                () -> userService.login(testUser.getUsername(), "Password123")
        );
        assertEquals("账户已被禁用", loginException.getMessage());

        // When - 重新激活账户
        userService.activateUser(userId);

        // Then
        User reactivatedUser = userRepository.findById(userId);
        assertTrue(reactivatedUser.isActive(), "账户应该被重新激活");

        // When - 登录应该成功
        User loggedInUser = userService.login(testUser.getUsername(), "Password123");
        assertNotNull(loggedInUser);
    }

    @Nested
    @DisplayName("邮箱验证器测试")
    class EmailValidatorTest {

        @ParameterizedTest(name = "有效邮箱: {0}")
        @ValueSource(strings = {
                "user@example.com",
                "first.last@domain.co.uk",
                "user123@sub.domain.com",
                "user+tag@example.com",
                "u@example.com"
        })
        void testValidEmails(String validEmail) {
            EmailValidator validator = new EmailValidator();
            assertTrue(validator.isValid(validEmail),
                    validEmail + " 应该是有效的邮箱");
        }

        @ParameterizedTest(name = "无效邮箱: {0}")
        @ValueSource(strings = {
                "invalid-email",
                "user@",
                "@domain.com",
                "user@.com",
                "user@domain.",
                "",
                "   ",
        })
        void testInvalidEmails(String invalidEmail) {
            EmailValidator validator = new EmailValidator();
            assertFalse(validator.isValid(invalidEmail),
                    invalidEmail + " 应该是无效的邮箱");
        }
    }

    @Test
    @DisplayName("TC016: 端到端用户旅程测试")
    void testEndToEndUserJourney() {
        // 1. 用户注册
        User newUser = userService.register(
                "journeyuser",
                "journey@example.com",
                "JourneyPass123"
        );
        assertNotNull(newUser);
        assertEquals("journeyuser", newUser.getUsername());

        // 2. 用户登录
        User loggedInUser = userService.login("journeyuser", "JourneyPass123");
        assertNotNull(loggedInUser);
        assertEquals(newUser.getId(), loggedInUser.getId());

        // 3. 用户故意输错密码3次
        for (int i = 0; i < 3; i++) {
            assertThrows(SecurityException.class,
                    () -> userService.login("journeyuser", "WrongPass"));
        }

        // 4. 验证账户未锁定
        User userAfterThreeFails = userRepository.findByUsername("journeyuser");
        assertEquals(3, userAfterThreeFails.getFailedLoginAttempts());
        assertFalse(userAfterThreeFails.isLocked());

        // 5. 用正确密码登录,重置失败计数
        User successfulLogin = userService.login("journeyuser", "JourneyPass123");
        assertEquals(0, successfulLogin.getFailedLoginAttempts());

        // 6. 更新邮箱
        userService.updateEmail(newUser.getId(), "newjourney@example.com");
        User userWithNewEmail = userRepository.findById(newUser.getId());
        assertEquals("newjourney@example.com", userWithNewEmail.getEmail());

        // 7. 禁用账户
        userService.deactivateUser(newUser.getId());
        assertThrows(SecurityException.class,
                () -> userService.login("journeyuser", "JourneyPass123"));

        // 8. 重新激活并登录
        userService.activateUser(newUser.getId());
        User finalLogin = userService.login("journeyuser", "JourneyPass123");
        assertNotNull(finalLogin);

        System.out.println("✓ 端到端用户旅程测试完成");
    }

    @Test
    @DisplayName("TC017: 并发用户注册测试")
    void testConcurrentUserRegistrations() throws InterruptedException {
        final int numberOfThreads = 10;
        Thread[] threads = new Thread[numberOfThreads];
        final boolean[] registrationResults = new boolean[numberOfThreads];

        for (int i = 0; i < numberOfThreads; i++) {
            final int index = i;
            threads[i] = new Thread(() -> {
                try {
                    String username = "concurrentuser" + index;
                    String email = "user" + index + "@test.com";
                    userService.register(username, email, "Password123");
                    registrationResults[index] = true;
                } catch (Exception e) {
                    registrationResults[index] = false;
                }
            });
        }

        // 启动所有线程
        for (Thread thread : threads) {
            thread.start();
        }

        // 等待所有线程完成
        for (Thread thread : threads) {
            thread.join();
        }

        // 验证所有注册都成功
        int successfulRegistrations = 0;
        for (boolean result : registrationResults) {
            if (result) successfulRegistrations++;
        }

        assertEquals(numberOfThreads, successfulRegistrations,
                "所有并发注册都应该成功");

        // 验证所有用户都被创建
        for (int i = 0; i < numberOfThreads; i++) {
            User user = userRepository.findByUsername("concurrentuser" + i);
            assertNotNull(user, "用户 " + i + " 应该存在");
        }
    }
}

EmailValidator

java 复制代码
import java.util.regex.Pattern;

// 邮箱验证器
public class EmailValidator {
    private static final String EMAIL_REGEX =
            "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$";
    private static final Pattern pattern = Pattern.compile(EMAIL_REGEX);

    public boolean isValid(String email) {
        if (email == null) return false;
        return pattern.matcher(email).matches();
    }
}

AuthSystemTestRunner 运行所有测试的主类

java 复制代码
// 运行所有测试的主类
class AuthSystemTestRunner {
    public static void main(String[] args) {
        System.out.println("=== 用户认证系统功能测试开始 ===");

        // 可以在这里添加一些手动测试
        UserRepository repo = new UserRepository();
        UserService service = new UserService(repo);

        try {
            // 手动测试1: 注册用户
            User user = service.register("manualuser", "manual@test.com", "Manual123");
            System.out.println("✓ 手动测试1: 用户注册成功 - " + user.getUsername());

            // 手动测试2: 登录用户
            User loggedIn = service.login("manualuser", "Manual123");
            System.out.println("✓ 手动测试2: 用户登录成功");

            // 手动测试3: 错误密码
            try {
                service.login("manualuser", "WrongPassword");
                System.out.println("✗ 手动测试3: 应该抛出异常");
            } catch (SecurityException e) {
                System.out.println("✓ 手动测试3: 密码错误被正确拒绝");
            }

            System.out.println("\n=== 所有手动测试通过 ===");

        } catch (Exception e) {
            System.out.println("✗ 手动测试失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}
复制代码



相关推荐
杀死那个蝈坦3 小时前
OpenResty
junit·openresty
李绍熹2 天前
Lua 语言基础教程
开发语言·junit·lua
李绍熹2 天前
Lua 错误处理详解
开发语言·junit·lua
weixin_462446232 天前
【原创实践】安装与配置 lua-cjson 在宝塔 Nginx 上
nginx·junit·lua
红石榴花生油2 天前
Lua语句与Redis方法的区别及实战笔记
junit
IMPYLH2 天前
Lua 的 select 函数
java·开发语言·笔记·后端·junit·游戏引擎·lua
旷野说2 天前
用 Redis + Lua 守住打赏原子性:我在单体系统中的微观实践(续)
redis·junit·lua
想做后端的前端2 天前
Lua基本数据类型
java·junit·lua
IMPYLH4 天前
Lua 的 require 函数
java·开发语言·笔记·后端·junit·lua