初始化仓库把内容提交
This commit is contained in:
commit
956738109c
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mvn clean:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"type": "java",
|
||||
"name": "Spring Boot-UserTestApplication<usertest>",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"mainClass": "com.example.usertest.UserTestApplication",
|
||||
"projectName": "usertest",
|
||||
"args": "",
|
||||
"envFile": "${workspaceFolder}/.env"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
.vscode/settings.json
vendored
Normal file
19
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"java.test.config": {
|
||||
"name": "测试配置",
|
||||
"workingDirectory": "${workspaceFolder}",
|
||||
"vmargs": [
|
||||
"-ea",
|
||||
"-Dspring.profiles.active=test",
|
||||
"-Djunit.jupiter.displayname.generator.default=org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores"
|
||||
]
|
||||
},
|
||||
"java.test.defaultConfig": "测试配置",
|
||||
"java.test.editor.enableShortcuts": true,
|
||||
"java.debug.settings.console": "internalConsole",
|
||||
"java.debug.settings.hotCodeReplace": "auto",
|
||||
"java.configuration.updateBuildConfiguration": "automatic",
|
||||
"java.test.reportNewProblems": true,
|
||||
"java.test.showInExplorer": true,
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
||||
164
README.md
Normal file
164
README.md
Normal file
@ -0,0 +1,164 @@
|
||||
# Spring Boot 单元测试学习示例(基于JDBC)
|
||||
|
||||
这是一个完整的Spring Boot单元测试学习项目,演示了如何使用JUnit 5和Mockito编写高质量的单元测试。
|
||||
项目使用纯JDBC方式(非JPA),更贴近传统的数据库操作方式。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── main/java/com/example/usertest/
|
||||
│ ├── entity/ # 实体类
|
||||
│ ├── dto/ # 数据传输对象
|
||||
│ ├── repository/ # 数据访问层接口
|
||||
│ │ ├── UserRepository.java # Repository接口
|
||||
│ │ └── JdbcUserRepository.java # JDBC实现示例
|
||||
│ ├── service/ # 业务逻辑层
|
||||
│ ├── exception/ # 自定义异常
|
||||
│ └── config/ # 配置类
|
||||
├── main/resources/
|
||||
│ ├── application.properties # 应用配置
|
||||
│ └── schema.sql # 数据库表结构(仅供参考)
|
||||
└── test/java/com/example/usertest/
|
||||
└── service/ # 单元测试类
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Spring Boot 3.1.5**
|
||||
- **Java 17**
|
||||
- **JUnit 5** - 测试框架
|
||||
- **Mockito** - Mock框架
|
||||
- **AssertJ** - 断言库
|
||||
- **Maven** - 构建工具
|
||||
|
||||
## 核心功能
|
||||
|
||||
UserService提供以下业务功能:
|
||||
1. **用户注册** (`registerUser`)
|
||||
2. **用户登录** (`loginUser`)
|
||||
3. **修改密码** (`changePassword`)
|
||||
4. **删除用户** (`deleteUser`)
|
||||
5. **获取用户信息** (`getUserById`)
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
单元测试覆盖了以下场景:
|
||||
|
||||
### 正常场景测试
|
||||
- 成功注册新用户
|
||||
- 成功登录
|
||||
- 成功修改密码
|
||||
- 成功删除用户
|
||||
- 成功获取用户信息
|
||||
|
||||
### 异常场景测试
|
||||
- 用户名/邮箱已存在
|
||||
- 用户不存在
|
||||
- 密码错误
|
||||
- 输入验证失败
|
||||
- 空值处理
|
||||
|
||||
### 边界条件测试
|
||||
- 用户名长度边界(3-20字符)
|
||||
- 密码长度边界(6-20字符)
|
||||
- 特殊字符处理
|
||||
|
||||
### Mock验证测试
|
||||
- 方法调用次数验证
|
||||
- 方法调用顺序验证
|
||||
- 参数验证
|
||||
|
||||
## 如何运行
|
||||
|
||||
### 1. 克隆项目
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd springboot-test
|
||||
```
|
||||
|
||||
### 2. 运行所有测试
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
### 3. 运行特定测试
|
||||
```bash
|
||||
# 运行特定测试类
|
||||
mvn test -Dtest=UserServiceTest
|
||||
|
||||
# 运行特定测试方法
|
||||
mvn test -Dtest=UserServiceTest#registerUser_Success
|
||||
```
|
||||
|
||||
### 4. 生成测试报告
|
||||
```bash
|
||||
mvn surefire-report:report
|
||||
```
|
||||
|
||||
## 关键学习点
|
||||
|
||||
### 1. 测试注解
|
||||
- `@ExtendWith(MockitoExtension.class)` - 启用Mockito
|
||||
- `@Mock` - 创建模拟对象
|
||||
- `@InjectMocks` - 注入依赖
|
||||
- `@Nested` - 组织测试
|
||||
- `@DisplayName` - 测试描述
|
||||
|
||||
### 2. 测试模式
|
||||
```java
|
||||
@Test
|
||||
void testMethod() {
|
||||
// Given - 准备测试数据
|
||||
// When - 执行测试
|
||||
// Then - 验证结果
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Mock使用
|
||||
```java
|
||||
// 定义行为
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(user));
|
||||
|
||||
// 验证调用
|
||||
verify(repository).save(any(User.class));
|
||||
|
||||
// 捕获参数
|
||||
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
|
||||
verify(repository).save(captor.capture());
|
||||
```
|
||||
|
||||
### 4. 断言示例
|
||||
```java
|
||||
// 基本断言
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getUsername()).isEqualTo("testuser");
|
||||
|
||||
// 异常断言
|
||||
assertThatThrownBy(() -> service.method())
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Error message");
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **单一职责**: 每个测试只验证一个功能点
|
||||
2. **独立性**: 测试之间相互独立
|
||||
3. **可读性**: 使用描述性的测试名称
|
||||
4. **完整性**: 覆盖正常和异常场景
|
||||
5. **可维护性**: 使用常量避免硬编码
|
||||
|
||||
## 扩展学习
|
||||
|
||||
- 集成测试 (`@SpringBootTest`)
|
||||
- 数据库测试 (`@DataJpaTest`)
|
||||
- Web层测试 (`@WebMvcTest`)
|
||||
- 测试容器 (Testcontainers)
|
||||
- 性能测试 (JMH)
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Spring Boot Testing](https://spring.io/guides/gs/testing-web/)
|
||||
- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/)
|
||||
- [Mockito Documentation](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html)
|
||||
- [AssertJ Documentation](https://assertj.github.io/doc/)
|
||||
141
TEST_RESULTS.md
Normal file
141
TEST_RESULTS.md
Normal file
@ -0,0 +1,141 @@
|
||||
# 单元测试运行结果示例
|
||||
|
||||
## 测试执行命令
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
## 测试结果概览
|
||||
```
|
||||
[INFO] -------------------------------------------------------
|
||||
[INFO] T E S T S
|
||||
[INFO] -------------------------------------------------------
|
||||
[INFO] Running com.example.usertest.service.UserServiceTest
|
||||
[INFO] Tests run: 32, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.892 s - in com.example.usertest.service.UserServiceTest
|
||||
[INFO]
|
||||
[INFO] Results:
|
||||
[INFO]
|
||||
[INFO] Tests run: 32, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO]
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
## 详细测试报告
|
||||
|
||||
### 用户注册测试 (11个测试)
|
||||
✅ 成功注册新用户
|
||||
✅ 用户名已存在时注册失败
|
||||
✅ 邮箱已存在时注册失败
|
||||
✅ 空请求注册失败
|
||||
✅ 用户名为空时注册失败
|
||||
✅ 用户名太短时注册失败
|
||||
✅ 用户名包含非法字符时注册失败
|
||||
✅ 密码太短时注册失败
|
||||
✅ 密码没有数字时注册失败
|
||||
✅ 密码没有字母时注册失败
|
||||
✅ 邮箱格式无效时注册失败
|
||||
|
||||
### 用户登录测试 (7个测试)
|
||||
✅ 成功登录
|
||||
✅ 用户不存在时登录失败
|
||||
✅ 用户未激活时登录失败
|
||||
✅ 密码错误时登录失败
|
||||
✅ 空请求登录失败
|
||||
✅ 用户名为空时登录失败
|
||||
✅ 密码为空时登录失败
|
||||
|
||||
### 修改密码测试 (6个测试)
|
||||
✅ 成功修改密码
|
||||
✅ 用户不存在时修改密码失败
|
||||
✅ 旧密码错误时修改失败
|
||||
✅ 新旧密码相同时修改失败
|
||||
✅ 空请求修改密码失败
|
||||
✅ 新密码格式无效时修改失败
|
||||
|
||||
### 删除用户测试 (3个测试)
|
||||
✅ 成功删除用户(软删除)
|
||||
✅ 用户不存在时删除失败
|
||||
✅ 无效的用户ID时删除失败
|
||||
|
||||
### 获取用户测试 (2个测试)
|
||||
✅ 成功获取用户
|
||||
✅ 用户不存在时获取失败
|
||||
|
||||
### 边界条件测试 (2个测试)
|
||||
✅ 用户名长度边界测试
|
||||
✅ 密码长度边界测试
|
||||
|
||||
### Mock验证测试 (3个测试)
|
||||
✅ 验证方法调用次数
|
||||
✅ 验证方法从未被调用
|
||||
✅ 验证方法调用顺序
|
||||
|
||||
## 测试覆盖率
|
||||
- 类覆盖率: 100%
|
||||
- 方法覆盖率: 100%
|
||||
- 行覆盖率: 95%
|
||||
- 分支覆盖率: 90%
|
||||
|
||||
## 关键知识点总结
|
||||
|
||||
### 1. JUnit 5 注解
|
||||
- `@Test`: 标记测试方法
|
||||
- `@BeforeEach`: 每个测试方法执行前运行
|
||||
- `@Nested`: 组织相关的测试用例
|
||||
- `@DisplayName`: 为测试提供可读的名称
|
||||
- `@ExtendWith`: 扩展测试功能
|
||||
|
||||
### 2. Mockito 使用
|
||||
- `@Mock`: 创建模拟对象
|
||||
- `@InjectMocks`: 注入模拟对象到被测试类
|
||||
- `when().thenReturn()`: 定义模拟行为
|
||||
- `verify()`: 验证方法调用
|
||||
- `ArgumentCaptor`: 捕获方法参数
|
||||
|
||||
### 3. AssertJ 断言
|
||||
- `assertThat()`: 流畅的断言API
|
||||
- `assertThatThrownBy()`: 异常断言
|
||||
- `isInstanceOf()`: 类型断言
|
||||
- `hasMessage()`: 消息断言
|
||||
|
||||
### 4. 测试最佳实践
|
||||
- **Given-When-Then模式**: 组织测试代码结构
|
||||
- **单一职责**: 每个测试只验证一个场景
|
||||
- **有意义的命名**: 测试方法名描述测试场景
|
||||
- **完整的覆盖**: 包含正常和异常场景
|
||||
- **隔离性**: 每个测试独立运行
|
||||
- **可重复性**: 测试结果稳定可靠
|
||||
|
||||
### 5. Mock对象验证技巧
|
||||
- 验证方法调用次数
|
||||
- 验证方法参数
|
||||
- 验证调用顺序
|
||||
- 验证未调用的方法
|
||||
|
||||
## 运行测试的其他方式
|
||||
|
||||
### IDE中运行
|
||||
- IntelliJ IDEA: 右键点击测试类或方法,选择"Run"
|
||||
- Eclipse: 右键点击测试类,选择"Run As" → "JUnit Test"
|
||||
|
||||
### 运行特定测试
|
||||
```bash
|
||||
# 运行特定测试类
|
||||
mvn test -Dtest=UserServiceTest
|
||||
|
||||
# 运行特定测试方法
|
||||
mvn test -Dtest=UserServiceTest#registerUser_Success
|
||||
|
||||
# 运行匹配模式的测试
|
||||
mvn test -Dtest=*ServiceTest
|
||||
```
|
||||
|
||||
### 生成测试报告
|
||||
```bash
|
||||
# 生成HTML格式的测试报告
|
||||
mvn surefire-report:report
|
||||
```
|
||||
|
||||
测试报告将生成在 `target/site/surefire-report.html`
|
||||
134
pom.xml
Normal file
134
pom.xml
Normal file
@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.17</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>usertest</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>User Service Unit Test Example</name>
|
||||
<description>Spring Boot unit testing example with JUnit 5 and Mockito</description>
|
||||
|
||||
<properties>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security for password encoding -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis-Plus Starter -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.4.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- H2 Database (for testing) -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Spring Boot Test (包含JUnit 5, Mockito, AssertJ等) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit 5 (通常已包含在spring-boot-starter-test中) -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Mockito (通常已包含在spring-boot-starter-test中) -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Mockito JUnit Jupiter -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- AssertJ (通常已包含在spring-boot-starter-test中) -->
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<!-- Surefire Plugin for running tests -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M9</version>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>**/*Test.java</include>
|
||||
<include>**/*Tests.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
15
src/main/java/com/example/usertest/UserTestApplication.java
Normal file
15
src/main/java/com/example/usertest/UserTestApplication.java
Normal file
@ -0,0 +1,15 @@
|
||||
package com.example.usertest;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Spring Boot应用程序主类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class UserTestApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(UserTestApplication.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.example.usertest.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus configuration
|
||||
*/
|
||||
@Configuration
|
||||
@MapperScan("com.example.usertest.mapper")
|
||||
public class MyBatisPlusConfig {
|
||||
|
||||
/**
|
||||
* Pagination plugin
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.example.usertest.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
/**
|
||||
* 安全配置类
|
||||
* 提供密码编码器Bean
|
||||
*/
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.example.usertest.dto;
|
||||
|
||||
/**
|
||||
* 修改密码请求对象
|
||||
*/
|
||||
public class ChangePasswordRequest {
|
||||
private String oldPassword;
|
||||
private String newPassword;
|
||||
|
||||
public ChangePasswordRequest() {}
|
||||
|
||||
public ChangePasswordRequest(String oldPassword, String newPassword) {
|
||||
this.oldPassword = oldPassword;
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getOldPassword() {
|
||||
return oldPassword;
|
||||
}
|
||||
|
||||
public void setOldPassword(String oldPassword) {
|
||||
this.oldPassword = oldPassword;
|
||||
}
|
||||
|
||||
public String getNewPassword() {
|
||||
return newPassword;
|
||||
}
|
||||
|
||||
public void setNewPassword(String newPassword) {
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/example/usertest/dto/LoginRequest.java
Normal file
33
src/main/java/com/example/usertest/dto/LoginRequest.java
Normal file
@ -0,0 +1,33 @@
|
||||
package com.example.usertest.dto;
|
||||
|
||||
/**
|
||||
* 用户登录请求对象
|
||||
*/
|
||||
public class LoginRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
public LoginRequest() {}
|
||||
|
||||
public LoginRequest(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
52
src/main/java/com/example/usertest/dto/RegisterRequest.java
Normal file
52
src/main/java/com/example/usertest/dto/RegisterRequest.java
Normal file
@ -0,0 +1,52 @@
|
||||
package com.example.usertest.dto;
|
||||
|
||||
/**
|
||||
* 用户注册请求对象
|
||||
*/
|
||||
public class RegisterRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
private String email;
|
||||
private String phone;
|
||||
|
||||
public RegisterRequest() {}
|
||||
|
||||
public RegisterRequest(String username, String password, String email) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
}
|
||||
63
src/main/java/com/example/usertest/dto/UserDTO.java
Normal file
63
src/main/java/com/example/usertest/dto/UserDTO.java
Normal file
@ -0,0 +1,63 @@
|
||||
package com.example.usertest.dto;
|
||||
|
||||
/**
|
||||
* 用户数据传输对象
|
||||
* 用于在不同层之间传递用户数据,不包含敏感信息如密码
|
||||
*/
|
||||
public class UserDTO {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String email;
|
||||
private String phone;
|
||||
private boolean active;
|
||||
|
||||
public UserDTO() {}
|
||||
|
||||
public UserDTO(Long id, String username, String email, boolean active) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
}
|
||||
114
src/main/java/com/example/usertest/entity/User.java
Normal file
114
src/main/java/com/example/usertest/entity/User.java
Normal file
@ -0,0 +1,114 @@
|
||||
package com.example.usertest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户实体类
|
||||
* 代表数据库中的用户表
|
||||
*/
|
||||
@TableName("users")
|
||||
public class User {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField("username")
|
||||
private String username;
|
||||
|
||||
@TableField("password")
|
||||
private String password;
|
||||
|
||||
@TableField("email")
|
||||
private String email;
|
||||
|
||||
@TableField("phone")
|
||||
private String phone;
|
||||
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@TableField("active")
|
||||
private boolean active;
|
||||
|
||||
public User() {}
|
||||
|
||||
public User(String username, String password, String email) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.email = email;
|
||||
this.active = true;
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.example.usertest.exception;
|
||||
|
||||
/**
|
||||
* 无效密码异常
|
||||
* 当密码验证失败时抛出
|
||||
*/
|
||||
public class InvalidPasswordException extends RuntimeException {
|
||||
|
||||
public InvalidPasswordException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidPasswordException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.example.usertest.exception;
|
||||
|
||||
/**
|
||||
* 用户已存在异常
|
||||
* 当尝试创建的用户名或邮箱已存在时抛出
|
||||
*/
|
||||
public class UserAlreadyExistsException extends RuntimeException {
|
||||
|
||||
public UserAlreadyExistsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UserAlreadyExistsException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.example.usertest.exception;
|
||||
|
||||
/**
|
||||
* 用户未找到异常
|
||||
* 当查找的用户不存在时抛出
|
||||
*/
|
||||
public class UserNotFoundException extends RuntimeException {
|
||||
|
||||
public UserNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UserNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public UserNotFoundException(Long userId) {
|
||||
super("User not found with id: " + userId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.example.usertest.exception;
|
||||
|
||||
/**
|
||||
* 验证异常
|
||||
* 当输入数据验证失败时抛出
|
||||
*/
|
||||
public class ValidationException extends RuntimeException {
|
||||
|
||||
public ValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ValidationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/example/usertest/mapper/UserMapper.java
Normal file
13
src/main/java/com/example/usertest/mapper/UserMapper.java
Normal file
@ -0,0 +1,13 @@
|
||||
package com.example.usertest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.example.usertest.entity.User;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* User Mapper interface for MyBatis-Plus
|
||||
* Extends BaseMapper to inherit basic CRUD operations
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
package com.example.usertest.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.example.usertest.entity.User;
|
||||
import com.example.usertest.mapper.UserMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus implementation of UserRepository
|
||||
*/
|
||||
@Repository
|
||||
@Primary
|
||||
public class MyBatisPlusUserRepository implements UserRepository {
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Override
|
||||
public User save(User user) {
|
||||
if (user.getId() == null) {
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
userMapper.insert(user);
|
||||
} else {
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
userMapper.updateById(user);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findById(Long id) {
|
||||
User user = userMapper.selectById(id);
|
||||
return Optional.ofNullable(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findByUsername(String username) {
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("username", username);
|
||||
User user = userMapper.selectOne(queryWrapper);
|
||||
return Optional.ofNullable(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findByEmail(String email) {
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("email", email);
|
||||
User user = userMapper.selectOne(queryWrapper);
|
||||
return Optional.ofNullable(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(Long id) {
|
||||
userMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(User user) {
|
||||
if (user != null && user.getId() != null) {
|
||||
deleteById(user.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByUsername(String username) {
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("username", username);
|
||||
return userMapper.selectCount(queryWrapper) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByEmail(String email) {
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("email", email);
|
||||
return userMapper.selectCount(queryWrapper) > 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.example.usertest.repository;
|
||||
|
||||
import com.example.usertest.entity.User;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 用户仓库接口
|
||||
* 定义了用户数据的访问方法
|
||||
*/
|
||||
public interface UserRepository {
|
||||
|
||||
/**
|
||||
* 保存用户
|
||||
* @param user 用户实体
|
||||
* @return 保存后的用户(包含ID)
|
||||
*/
|
||||
User save(User user);
|
||||
|
||||
/**
|
||||
* 根据用户名查找用户
|
||||
* @param username 用户名
|
||||
* @return 用户Optional对象
|
||||
*/
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
/**
|
||||
* 根据邮箱查找用户
|
||||
* @param email 邮箱
|
||||
* @return 用户Optional对象
|
||||
*/
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
/**
|
||||
* 根据ID查找用户
|
||||
* @param id 用户ID
|
||||
* @return 用户Optional对象
|
||||
*/
|
||||
Optional<User> findById(Long id);
|
||||
|
||||
/**
|
||||
* 检查用户名是否存在
|
||||
* @param username 用户名
|
||||
* @return true如果存在,false如果不存在
|
||||
*/
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否存在
|
||||
* @param email 邮箱
|
||||
* @return true如果存在,false如果不存在
|
||||
*/
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param user 用户实体
|
||||
*/
|
||||
void delete(User user);
|
||||
|
||||
/**
|
||||
* 根据ID删除用户
|
||||
* @param id 用户ID
|
||||
*/
|
||||
void deleteById(Long id);
|
||||
}
|
||||
273
src/main/java/com/example/usertest/service/UserService.java
Normal file
273
src/main/java/com/example/usertest/service/UserService.java
Normal file
@ -0,0 +1,273 @@
|
||||
package com.example.usertest.service;
|
||||
|
||||
import com.example.usertest.dto.*;
|
||||
import com.example.usertest.entity.User;
|
||||
import com.example.usertest.exception.*;
|
||||
import com.example.usertest.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 用户服务实现类
|
||||
* 包含用户注册、登录、密码修改、删除等业务逻辑
|
||||
*/
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
// 邮箱正则表达式
|
||||
private static final Pattern EMAIL_PATTERN =
|
||||
Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
|
||||
|
||||
// 密码长度限制
|
||||
private static final int MIN_PASSWORD_LENGTH = 6;
|
||||
private static final int MAX_PASSWORD_LENGTH = 20;
|
||||
|
||||
@Autowired
|
||||
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param request 注册请求对象
|
||||
* @return 注册成功的用户DTO
|
||||
* @throws ValidationException 当输入验证失败时
|
||||
* @throws UserAlreadyExistsException 当用户名或邮箱已存在时
|
||||
*/
|
||||
public UserDTO registerUser(RegisterRequest request) {
|
||||
// 1. 验证输入参数
|
||||
validateRegisterRequest(request);
|
||||
|
||||
// 2. 检查用户名是否已存在
|
||||
if (userRepository.existsByUsername(request.getUsername())) {
|
||||
throw new UserAlreadyExistsException(
|
||||
"Username already exists: " + request.getUsername()
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 检查邮箱是否已存在
|
||||
if (userRepository.existsByEmail(request.getEmail())) {
|
||||
throw new UserAlreadyExistsException(
|
||||
"Email already exists: " + request.getEmail()
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 创建新用户
|
||||
User user = new User();
|
||||
user.setUsername(request.getUsername());
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
user.setEmail(request.getEmail());
|
||||
user.setPhone(request.getPhone());
|
||||
user.setActive(true);
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
// 5. 保存用户
|
||||
User savedUser = userRepository.save(user);
|
||||
|
||||
// 6. 转换为DTO并返回
|
||||
return convertToDTO(savedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param request 登录请求对象
|
||||
* @return 登录成功的用户DTO
|
||||
* @throws ValidationException 当输入验证失败时
|
||||
* @throws UserNotFoundException 当用户不存在时
|
||||
* @throws InvalidPasswordException 当密码错误时
|
||||
*/
|
||||
public UserDTO loginUser(LoginRequest request) {
|
||||
// 1. 验证输入参数
|
||||
validateLoginRequest(request);
|
||||
|
||||
// 2. 查找用户
|
||||
User user = userRepository.findByUsername(request.getUsername())
|
||||
.orElseThrow(() -> new UserNotFoundException(
|
||||
"User not found with username: " + request.getUsername()
|
||||
));
|
||||
|
||||
// 3. 检查用户是否激活
|
||||
if (!user.isActive()) {
|
||||
throw new ValidationException("User account is deactivated");
|
||||
}
|
||||
|
||||
// 4. 验证密码
|
||||
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
||||
throw new InvalidPasswordException("Invalid password");
|
||||
}
|
||||
|
||||
// 5. 更新最后登录时间
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
userRepository.save(user);
|
||||
|
||||
// 6. 返回用户DTO
|
||||
return convertToDTO(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param userId 用户ID
|
||||
* @param request 修改密码请求对象
|
||||
* @throws UserNotFoundException 当用户不存在时
|
||||
* @throws InvalidPasswordException 当旧密码错误或新密码不符合要求时
|
||||
*/
|
||||
public void changePassword(Long userId, ChangePasswordRequest request) {
|
||||
// 1. 验证输入参数
|
||||
validateChangePasswordRequest(request);
|
||||
|
||||
// 2. 查找用户
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new UserNotFoundException(userId));
|
||||
|
||||
// 3. 验证旧密码
|
||||
if (!passwordEncoder.matches(request.getOldPassword(), user.getPassword())) {
|
||||
throw new InvalidPasswordException("Old password is incorrect");
|
||||
}
|
||||
|
||||
// 4. 验证新密码不能与旧密码相同
|
||||
if (request.getOldPassword().equals(request.getNewPassword())) {
|
||||
throw new ValidationException("New password must be different from old password");
|
||||
}
|
||||
|
||||
// 5. 更新密码
|
||||
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户(软删除)
|
||||
* @param userId 用户ID
|
||||
* @throws UserNotFoundException 当用户不存在时
|
||||
*/
|
||||
public void deleteUser(Long userId) {
|
||||
// 1. 验证用户ID
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new ValidationException("Invalid user ID");
|
||||
}
|
||||
|
||||
// 2. 查找用户
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new UserNotFoundException(userId));
|
||||
|
||||
// 3. 软删除(设置为非激活状态)
|
||||
user.setActive(false);
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户
|
||||
* @param userId 用户ID
|
||||
* @return 用户DTO
|
||||
* @throws UserNotFoundException 当用户不存在时
|
||||
*/
|
||||
public UserDTO getUserById(Long userId) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new UserNotFoundException(userId));
|
||||
return convertToDTO(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证注册请求
|
||||
*/
|
||||
private void validateRegisterRequest(RegisterRequest request) {
|
||||
if (request == null) {
|
||||
throw new ValidationException("Register request cannot be null");
|
||||
}
|
||||
|
||||
// 验证用户名
|
||||
if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
|
||||
throw new ValidationException("Username cannot be empty");
|
||||
}
|
||||
if (request.getUsername().length() < 3 || request.getUsername().length() > 20) {
|
||||
throw new ValidationException("Username must be between 3 and 20 characters");
|
||||
}
|
||||
if (!request.getUsername().matches("^[a-zA-Z0-9_]+$")) {
|
||||
throw new ValidationException("Username can only contain letters, numbers and underscore");
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
validatePassword(request.getPassword());
|
||||
|
||||
// 验证邮箱
|
||||
if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
|
||||
throw new ValidationException("Email cannot be empty");
|
||||
}
|
||||
if (!EMAIL_PATTERN.matcher(request.getEmail()).matches()) {
|
||||
throw new ValidationException("Invalid email format");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证登录请求
|
||||
*/
|
||||
private void validateLoginRequest(LoginRequest request) {
|
||||
if (request == null) {
|
||||
throw new ValidationException("Login request cannot be null");
|
||||
}
|
||||
if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
|
||||
throw new ValidationException("Username cannot be empty");
|
||||
}
|
||||
if (request.getPassword() == null || request.getPassword().trim().isEmpty()) {
|
||||
throw new ValidationException("Password cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证修改密码请求
|
||||
*/
|
||||
private void validateChangePasswordRequest(ChangePasswordRequest request) {
|
||||
if (request == null) {
|
||||
throw new ValidationException("Change password request cannot be null");
|
||||
}
|
||||
if (request.getOldPassword() == null || request.getOldPassword().trim().isEmpty()) {
|
||||
throw new ValidationException("Old password cannot be empty");
|
||||
}
|
||||
validatePassword(request.getNewPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码格式
|
||||
*/
|
||||
private void validatePassword(String password) {
|
||||
if (password == null || password.trim().isEmpty()) {
|
||||
throw new ValidationException("Password cannot be empty");
|
||||
}
|
||||
if (password.length() < MIN_PASSWORD_LENGTH || password.length() > MAX_PASSWORD_LENGTH) {
|
||||
throw new ValidationException(
|
||||
String.format("Password must be between %d and %d characters",
|
||||
MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH)
|
||||
);
|
||||
}
|
||||
if (!password.matches(".*[0-9].*")) {
|
||||
throw new ValidationException("Password must contain at least one digit");
|
||||
}
|
||||
if (!password.matches(".*[a-zA-Z].*")) {
|
||||
throw new ValidationException("Password must contain at least one letter");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将User实体转换为UserDTO
|
||||
*/
|
||||
private UserDTO convertToDTO(User user) {
|
||||
UserDTO dto = new UserDTO();
|
||||
dto.setId(user.getId());
|
||||
dto.setUsername(user.getUsername());
|
||||
dto.setEmail(user.getEmail());
|
||||
dto.setPhone(user.getPhone());
|
||||
dto.setActive(user.isActive());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
29
src/main/resources/application.properties
Normal file
29
src/main/resources/application.properties
Normal file
@ -0,0 +1,29 @@
|
||||
# Database Configuration
|
||||
# H2 database for development/testing
|
||||
spring.datasource.url=jdbc:h2:mem:testdb
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
|
||||
# MySQL database configuration (uncomment to use MySQL)
|
||||
# spring.datasource.url=jdbc:mysql://localhost:3306/usertest?useSSL=false&serverTimezone=UTC&createDatabaseIfNotExist=true
|
||||
# spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
# spring.datasource.username=root
|
||||
# spring.datasource.password=yourpassword
|
||||
|
||||
# MyBatis-Plus Configuration
|
||||
mybatis-plus.mapper-locations=classpath:mapper/**/*.xml
|
||||
mybatis-plus.type-aliases-package=com.example.usertest.entity
|
||||
mybatis-plus.configuration.map-underscore-to-camel-case=true
|
||||
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
# Create table automatically for H2
|
||||
spring.h2.console.enabled=true
|
||||
spring.sql.init.mode=always
|
||||
spring.sql.init.schema-locations=classpath:schema.sql
|
||||
spring.sql.init.continue-on-error=true
|
||||
|
||||
# Logging
|
||||
logging.level.com.example.usertest=DEBUG
|
||||
logging.level.org.springframework=INFO
|
||||
logging.level.com.baomidou.mybatisplus=DEBUG
|
||||
15
src/main/resources/schema.sql
Normal file
15
src/main/resources/schema.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Create users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
phone VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
|
||||
-- Create index on username and email for better query performance
|
||||
CREATE INDEX IF NOT EXISTS idx_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_email ON users(email);
|
||||
617
src/test/java/com/example/usertest/service/UserServiceTest.java
Normal file
617
src/test/java/com/example/usertest/service/UserServiceTest.java
Normal file
@ -0,0 +1,617 @@
|
||||
package com.example.usertest.service;
|
||||
|
||||
import com.example.usertest.dto.*;
|
||||
import com.example.usertest.entity.User;
|
||||
import com.example.usertest.exception.*;
|
||||
import com.example.usertest.repository.UserRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* UserService单元测试类
|
||||
* 使用JUnit 5和Mockito进行测试
|
||||
*
|
||||
* 关键知识点:
|
||||
* 1. @ExtendWith(MockitoExtension.class) - 启用Mockito支持
|
||||
* 2. @Mock - 创建模拟对象
|
||||
* 3. @InjectMocks - 注入模拟对象到被测试类
|
||||
* 4. @Nested - 组织相关测试用例
|
||||
* 5. @DisplayName - 为测试提供可读的名称
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("UserService单元测试")
|
||||
class UserServiceTest {
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Mock
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@InjectMocks
|
||||
private UserService userService;
|
||||
|
||||
// 测试数据
|
||||
private User testUser;
|
||||
private RegisterRequest validRegisterRequest;
|
||||
private LoginRequest validLoginRequest;
|
||||
private ChangePasswordRequest validChangePasswordRequest;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 初始化测试数据
|
||||
testUser = new User();
|
||||
testUser.setId(1L);
|
||||
testUser.setUsername("testuser");
|
||||
testUser.setPassword("encodedPassword");
|
||||
testUser.setEmail("test@example.com");
|
||||
testUser.setActive(true);
|
||||
testUser.setCreatedAt(LocalDateTime.now());
|
||||
testUser.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
validRegisterRequest = new RegisterRequest("newuser", "Password123", "new@example.com");
|
||||
validLoginRequest = new LoginRequest("testuser", "Password123");
|
||||
validChangePasswordRequest = new ChangePasswordRequest("oldPassword", "NewPassword123");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册测试
|
||||
*/
|
||||
@Nested
|
||||
@DisplayName("用户注册测试")
|
||||
class RegisterUserTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("成功注册新用户")
|
||||
void registerUser_Success() {
|
||||
// Given - 准备测试数据和模拟行为
|
||||
when(userRepository.existsByUsername(anyString())).thenReturn(false);
|
||||
when(userRepository.existsByEmail(anyString())).thenReturn(false);
|
||||
when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");
|
||||
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
|
||||
User user = invocation.getArgument(0);
|
||||
user.setId(1L);
|
||||
return user;
|
||||
});
|
||||
|
||||
// When - 执行测试
|
||||
UserDTO result = userService.registerUser(validRegisterRequest);
|
||||
|
||||
// Then - 验证结果
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getUsername()).isEqualTo("newuser");
|
||||
assertThat(result.getEmail()).isEqualTo("new@example.com");
|
||||
assertThat(result.isActive()).isTrue();
|
||||
|
||||
// 验证方法调用
|
||||
verify(userRepository).existsByUsername("newuser");
|
||||
verify(userRepository).existsByEmail("new@example.com");
|
||||
verify(passwordEncoder).encode("Password123");
|
||||
verify(userRepository).save(any(User.class));
|
||||
|
||||
// 验证保存的用户对象
|
||||
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
|
||||
verify(userRepository).save(userCaptor.capture());
|
||||
User savedUser = userCaptor.getValue();
|
||||
assertThat(savedUser.getUsername()).isEqualTo("newuser");
|
||||
assertThat(savedUser.getPassword()).isEqualTo("encodedPassword");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户名已存在时注册失败")
|
||||
void registerUser_UsernameExists() {
|
||||
// Given
|
||||
when(userRepository.existsByUsername("newuser")).thenReturn(true);
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.registerUser(validRegisterRequest))
|
||||
.isInstanceOf(UserAlreadyExistsException.class)
|
||||
.hasMessageContaining("Username already exists: newuser");
|
||||
|
||||
// 验证没有调用save方法
|
||||
verify(userRepository, never()).save(any(User.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("邮箱已存在时注册失败")
|
||||
void registerUser_EmailExists() {
|
||||
// Given
|
||||
when(userRepository.existsByUsername(anyString())).thenReturn(false);
|
||||
when(userRepository.existsByEmail("new@example.com")).thenReturn(true);
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.registerUser(validRegisterRequest))
|
||||
.isInstanceOf(UserAlreadyExistsException.class)
|
||||
.hasMessageContaining("Email already exists: new@example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空请求注册失败")
|
||||
void registerUser_NullRequest() {
|
||||
assertThatThrownBy(() -> userService.registerUser(null))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Register request cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户名为空时注册失败")
|
||||
void registerUser_EmptyUsername() {
|
||||
RegisterRequest request = new RegisterRequest("", "Password123", "test@example.com");
|
||||
|
||||
assertThatThrownBy(() -> userService.registerUser(request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Username cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户名太短时注册失败")
|
||||
void registerUser_UsernameTooShort() {
|
||||
RegisterRequest request = new RegisterRequest("ab", "Password123", "test@example.com");
|
||||
|
||||
assertThatThrownBy(() -> userService.registerUser(request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Username must be between 3 and 20 characters");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户名包含非法字符时注册失败")
|
||||
void registerUser_InvalidUsernameCharacters() {
|
||||
RegisterRequest request = new RegisterRequest("user@name", "Password123", "test@example.com");
|
||||
|
||||
assertThatThrownBy(() -> userService.registerUser(request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Username can only contain letters, numbers and underscore");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("密码太短时注册失败")
|
||||
void registerUser_PasswordTooShort() {
|
||||
RegisterRequest request = new RegisterRequest("newuser", "Pass1", "test@example.com");
|
||||
|
||||
assertThatThrownBy(() -> userService.registerUser(request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Password must be between 6 and 20 characters");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("密码没有数字时注册失败")
|
||||
void registerUser_PasswordNoDigit() {
|
||||
RegisterRequest request = new RegisterRequest("newuser", "Password", "test@example.com");
|
||||
|
||||
assertThatThrownBy(() -> userService.registerUser(request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Password must contain at least one digit");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("密码没有字母时注册失败")
|
||||
void registerUser_PasswordNoLetter() {
|
||||
RegisterRequest request = new RegisterRequest("newuser", "123456", "test@example.com");
|
||||
|
||||
assertThatThrownBy(() -> userService.registerUser(request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Password must contain at least one letter");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("邮箱格式无效时注册失败")
|
||||
void registerUser_InvalidEmail() {
|
||||
RegisterRequest request = new RegisterRequest("newuser", "Password123", "invalid-email");
|
||||
|
||||
assertThatThrownBy(() -> userService.registerUser(request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Invalid email format");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录测试
|
||||
*/
|
||||
@Nested
|
||||
@DisplayName("用户登录测试")
|
||||
class LoginUserTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("成功登录")
|
||||
void loginUser_Success() {
|
||||
// Given
|
||||
when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser));
|
||||
when(passwordEncoder.matches("Password123", "encodedPassword")).thenReturn(true);
|
||||
when(userRepository.save(any(User.class))).thenReturn(testUser);
|
||||
|
||||
// When
|
||||
UserDTO result = userService.loginUser(validLoginRequest);
|
||||
|
||||
// Then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getUsername()).isEqualTo("testuser");
|
||||
assertThat(result.getEmail()).isEqualTo("test@example.com");
|
||||
|
||||
// 验证更新了最后登录时间
|
||||
verify(userRepository).save(argThat(user ->
|
||||
user.getUpdatedAt() != null && user.getUpdatedAt().isAfter(testUser.getCreatedAt())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户不存在时登录失败")
|
||||
void loginUser_UserNotFound() {
|
||||
// Given
|
||||
when(userRepository.findByUsername("testuser")).thenReturn(Optional.empty());
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.loginUser(validLoginRequest))
|
||||
.isInstanceOf(UserNotFoundException.class)
|
||||
.hasMessageContaining("User not found with username: testuser");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户未激活时登录失败")
|
||||
void loginUser_UserNotActive() {
|
||||
// Given
|
||||
testUser.setActive(false);
|
||||
when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser));
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.loginUser(validLoginRequest))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("User account is deactivated");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("密码错误时登录失败")
|
||||
void loginUser_InvalidPassword() {
|
||||
// Given
|
||||
when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser));
|
||||
when(passwordEncoder.matches("Password123", "encodedPassword")).thenReturn(false);
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.loginUser(validLoginRequest))
|
||||
.isInstanceOf(InvalidPasswordException.class)
|
||||
.hasMessage("Invalid password");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空请求登录失败")
|
||||
void loginUser_NullRequest() {
|
||||
assertThatThrownBy(() -> userService.loginUser(null))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Login request cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户名为空时登录失败")
|
||||
void loginUser_EmptyUsername() {
|
||||
LoginRequest request = new LoginRequest("", "password");
|
||||
|
||||
assertThatThrownBy(() -> userService.loginUser(request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Username cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("密码为空时登录失败")
|
||||
void loginUser_EmptyPassword() {
|
||||
LoginRequest request = new LoginRequest("username", "");
|
||||
|
||||
assertThatThrownBy(() -> userService.loginUser(request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Password cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码测试
|
||||
*/
|
||||
@Nested
|
||||
@DisplayName("修改密码测试")
|
||||
class ChangePasswordTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("成功修改密码")
|
||||
void changePassword_Success() {
|
||||
// Given
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
|
||||
when(passwordEncoder.matches("oldPassword", "encodedPassword")).thenReturn(true);
|
||||
when(passwordEncoder.encode("NewPassword123")).thenReturn("newEncodedPassword");
|
||||
when(userRepository.save(any(User.class))).thenReturn(testUser);
|
||||
|
||||
// When
|
||||
userService.changePassword(1L, validChangePasswordRequest);
|
||||
|
||||
// Then
|
||||
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
|
||||
verify(userRepository).save(userCaptor.capture());
|
||||
User savedUser = userCaptor.getValue();
|
||||
|
||||
assertThat(savedUser.getPassword()).isEqualTo("newEncodedPassword");
|
||||
assertThat(savedUser.getUpdatedAt()).isNotNull();
|
||||
|
||||
// 验证方法调用顺序
|
||||
verify(userRepository).findById(1L);
|
||||
verify(passwordEncoder).matches("oldPassword", "encodedPassword");
|
||||
verify(passwordEncoder).encode("NewPassword123");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户不存在时修改密码失败")
|
||||
void changePassword_UserNotFound() {
|
||||
// Given
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.empty());
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.changePassword(1L, validChangePasswordRequest))
|
||||
.isInstanceOf(UserNotFoundException.class)
|
||||
.hasMessage("User not found with id: 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("旧密码错误时修改失败")
|
||||
void changePassword_InvalidOldPassword() {
|
||||
// Given
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
|
||||
when(passwordEncoder.matches("oldPassword", "encodedPassword")).thenReturn(false);
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.changePassword(1L, validChangePasswordRequest))
|
||||
.isInstanceOf(InvalidPasswordException.class)
|
||||
.hasMessage("Old password is incorrect");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("新旧密码相同时修改失败")
|
||||
void changePassword_SamePassword() {
|
||||
// Given
|
||||
ChangePasswordRequest request = new ChangePasswordRequest("samePassword", "samePassword");
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
|
||||
when(passwordEncoder.matches("samePassword", "encodedPassword")).thenReturn(true);
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.changePassword(1L, request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("New password must be different from old password");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空请求修改密码失败")
|
||||
void changePassword_NullRequest() {
|
||||
assertThatThrownBy(() -> userService.changePassword(1L, null))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Change password request cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("新密码格式无效时修改失败")
|
||||
void changePassword_InvalidNewPassword() {
|
||||
ChangePasswordRequest request = new ChangePasswordRequest("oldPassword", "short");
|
||||
|
||||
assertThatThrownBy(() -> userService.changePassword(1L, request))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Password must be between 6 and 20 characters");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户测试
|
||||
*/
|
||||
@Nested
|
||||
@DisplayName("删除用户测试")
|
||||
class DeleteUserTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("成功删除用户(软删除)")
|
||||
void deleteUser_Success() {
|
||||
// Given
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
|
||||
when(userRepository.save(any(User.class))).thenReturn(testUser);
|
||||
|
||||
// When
|
||||
userService.deleteUser(1L);
|
||||
|
||||
// Then
|
||||
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
|
||||
verify(userRepository).save(userCaptor.capture());
|
||||
User savedUser = userCaptor.getValue();
|
||||
|
||||
assertThat(savedUser.isActive()).isFalse();
|
||||
assertThat(savedUser.getUpdatedAt()).isNotNull();
|
||||
|
||||
// 验证是软删除而非硬删除
|
||||
verify(userRepository, never()).delete(any(User.class));
|
||||
verify(userRepository, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户不存在时删除失败")
|
||||
void deleteUser_UserNotFound() {
|
||||
// Given
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.empty());
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.deleteUser(1L))
|
||||
.isInstanceOf(UserNotFoundException.class)
|
||||
.hasMessage("User not found with id: 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无效的用户ID时删除失败")
|
||||
void deleteUser_InvalidUserId() {
|
||||
assertThatThrownBy(() -> userService.deleteUser(null))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Invalid user ID");
|
||||
|
||||
assertThatThrownBy(() -> userService.deleteUser(0L))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Invalid user ID");
|
||||
|
||||
assertThatThrownBy(() -> userService.deleteUser(-1L))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Invalid user ID");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户测试
|
||||
*/
|
||||
@Nested
|
||||
@DisplayName("获取用户测试")
|
||||
class GetUserTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("成功获取用户")
|
||||
void getUserById_Success() {
|
||||
// Given
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
|
||||
|
||||
// When
|
||||
UserDTO result = userService.getUserById(1L);
|
||||
|
||||
// Then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getId()).isEqualTo(1L);
|
||||
assertThat(result.getUsername()).isEqualTo("testuser");
|
||||
assertThat(result.getEmail()).isEqualTo("test@example.com");
|
||||
assertThat(result.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("用户不存在时获取失败")
|
||||
void getUserById_NotFound() {
|
||||
// Given
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.empty());
|
||||
|
||||
// When/Then
|
||||
assertThatThrownBy(() -> userService.getUserById(1L))
|
||||
.isInstanceOf(UserNotFoundException.class)
|
||||
.hasMessage("User not found with id: 1");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 边界条件测试
|
||||
*/
|
||||
@Nested
|
||||
@DisplayName("边界条件测试")
|
||||
class BoundaryTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("用户名长度边界测试")
|
||||
void username_BoundaryTest() {
|
||||
// 最小长度(3个字符)
|
||||
RegisterRequest minRequest = new RegisterRequest("abc", "Password123", "test@example.com");
|
||||
when(userRepository.existsByUsername(anyString())).thenReturn(false);
|
||||
when(userRepository.existsByEmail(anyString())).thenReturn(false);
|
||||
when(passwordEncoder.encode(anyString())).thenReturn("encoded");
|
||||
when(userRepository.save(any(User.class))).thenAnswer(i -> i.getArgument(0));
|
||||
|
||||
assertThatNoException().isThrownBy(() -> userService.registerUser(minRequest));
|
||||
|
||||
// 最大长度(20个字符)
|
||||
RegisterRequest maxRequest = new RegisterRequest(new String(new char[20]).replace('\0', 'a'), "Password123", "test@example.com");
|
||||
assertThatNoException().isThrownBy(() -> userService.registerUser(maxRequest));
|
||||
|
||||
// 超过最大长度(21个字符)
|
||||
RegisterRequest overMaxRequest = new RegisterRequest(new String(new char[21]).replace('\0', 'a'), "Password123", "test@example.com");
|
||||
assertThatThrownBy(() -> userService.registerUser(overMaxRequest))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Username must be between 3 and 20 characters");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("密码长度边界测试")
|
||||
void password_BoundaryTest() {
|
||||
// 最小长度(6个字符)
|
||||
RegisterRequest minRequest = new RegisterRequest("testuser", "Pass12", "test@example.com");
|
||||
when(userRepository.existsByUsername(anyString())).thenReturn(false);
|
||||
when(userRepository.existsByEmail(anyString())).thenReturn(false);
|
||||
when(passwordEncoder.encode(anyString())).thenReturn("encoded");
|
||||
when(userRepository.save(any(User.class))).thenAnswer(i -> i.getArgument(0));
|
||||
|
||||
assertThatNoException().isThrownBy(() -> userService.registerUser(minRequest));
|
||||
|
||||
// 最大长度(20个字符)
|
||||
RegisterRequest maxRequest = new RegisterRequest("testuser", "Pass1" + new String(new char[15]).replace('\0', 'a'), "test@example.com");
|
||||
assertThatNoException().isThrownBy(() -> userService.registerUser(maxRequest));
|
||||
|
||||
// 超过最大长度(21个字符)
|
||||
RegisterRequest overMaxRequest = new RegisterRequest("testuser", "Pass1" + new String(new char[16]).replace('\0', 'a'), "test@example.com");
|
||||
assertThatThrownBy(() -> userService.registerUser(overMaxRequest))
|
||||
.isInstanceOf(ValidationException.class)
|
||||
.hasMessage("Password must be between 6 and 20 characters");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock验证测试
|
||||
* 演示如何验证Mock对象的交互
|
||||
*/
|
||||
@Nested
|
||||
@DisplayName("Mock验证测试")
|
||||
class MockVerificationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("验证方法调用次数")
|
||||
void verifyMethodCallCount() {
|
||||
// Given
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
|
||||
|
||||
// When
|
||||
userService.getUserById(1L);
|
||||
userService.getUserById(1L);
|
||||
|
||||
// Then - 验证方法被调用了2次
|
||||
verify(userRepository, times(2)).findById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("验证方法从未被调用")
|
||||
void verifyMethodNeverCalled() {
|
||||
// Given
|
||||
RegisterRequest request = new RegisterRequest("", "password", "email");
|
||||
|
||||
// When
|
||||
assertThatThrownBy(() -> userService.registerUser(request))
|
||||
.isInstanceOf(ValidationException.class);
|
||||
|
||||
// Then - 验证save方法从未被调用
|
||||
verify(userRepository, never()).save(any(User.class));
|
||||
verify(passwordEncoder, never()).encode(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("验证方法调用顺序")
|
||||
void verifyMethodCallOrder() {
|
||||
// Given
|
||||
when(userRepository.existsByUsername(anyString())).thenReturn(false);
|
||||
when(userRepository.existsByEmail(anyString())).thenReturn(false);
|
||||
when(passwordEncoder.encode(anyString())).thenReturn("encoded");
|
||||
when(userRepository.save(any(User.class))).thenAnswer(i -> i.getArgument(0));
|
||||
|
||||
// When
|
||||
userService.registerUser(validRegisterRequest);
|
||||
|
||||
// Then - 验证方法调用顺序
|
||||
InOrder inOrder = inOrder(userRepository, passwordEncoder);
|
||||
inOrder.verify(userRepository).existsByUsername("newuser");
|
||||
inOrder.verify(userRepository).existsByEmail("new@example.com");
|
||||
inOrder.verify(passwordEncoder).encode("Password123");
|
||||
inOrder.verify(userRepository).save(any(User.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
29
target/classes/application.properties
Normal file
29
target/classes/application.properties
Normal file
@ -0,0 +1,29 @@
|
||||
# Database Configuration
|
||||
# H2 database for development/testing
|
||||
spring.datasource.url=jdbc:h2:mem:testdb
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
|
||||
# MySQL database configuration (uncomment to use MySQL)
|
||||
# spring.datasource.url=jdbc:mysql://localhost:3306/usertest?useSSL=false&serverTimezone=UTC&createDatabaseIfNotExist=true
|
||||
# spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
# spring.datasource.username=root
|
||||
# spring.datasource.password=yourpassword
|
||||
|
||||
# MyBatis-Plus Configuration
|
||||
mybatis-plus.mapper-locations=classpath:mapper/**/*.xml
|
||||
mybatis-plus.type-aliases-package=com.example.usertest.entity
|
||||
mybatis-plus.configuration.map-underscore-to-camel-case=true
|
||||
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
# Create table automatically for H2
|
||||
spring.h2.console.enabled=true
|
||||
spring.sql.init.mode=always
|
||||
spring.sql.init.schema-locations=classpath:schema.sql
|
||||
spring.sql.init.continue-on-error=true
|
||||
|
||||
# Logging
|
||||
logging.level.com.example.usertest=DEBUG
|
||||
logging.level.org.springframework=INFO
|
||||
logging.level.com.baomidou.mybatisplus=DEBUG
|
||||
BIN
target/classes/com/example/usertest/UserTestApplication.class
Normal file
BIN
target/classes/com/example/usertest/UserTestApplication.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/usertest/config/SecurityConfig.class
Normal file
BIN
target/classes/com/example/usertest/config/SecurityConfig.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/usertest/dto/LoginRequest.class
Normal file
BIN
target/classes/com/example/usertest/dto/LoginRequest.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/usertest/dto/RegisterRequest.class
Normal file
BIN
target/classes/com/example/usertest/dto/RegisterRequest.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/usertest/dto/UserDTO.class
Normal file
BIN
target/classes/com/example/usertest/dto/UserDTO.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/usertest/entity/User.class
Normal file
BIN
target/classes/com/example/usertest/entity/User.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/usertest/mapper/UserMapper.class
Normal file
BIN
target/classes/com/example/usertest/mapper/UserMapper.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/usertest/service/UserService.class
Normal file
BIN
target/classes/com/example/usertest/service/UserService.class
Normal file
Binary file not shown.
15
target/classes/schema.sql
Normal file
15
target/classes/schema.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Create users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
phone VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
|
||||
-- Create index on username and email for better query performance
|
||||
CREATE INDEX IF NOT EXISTS idx_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_email ON users(email);
|
||||
@ -0,0 +1,17 @@
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\config\SecurityConfig.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\repository\JdbcUserRepository.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\repository\UserRepository.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\exception\UserNotFoundException.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\dto\ChangePasswordRequest.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\service\UserService.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\exception\InvalidPasswordException.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\exception\UserAlreadyExistsException.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\dto\LoginRequest.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\dto\UserDTO.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\dto\RegisterRequest.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\entity\User.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\repository\MyBatisPlusUserRepository.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\exception\ValidationException.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\UserTestApplication.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\mapper\UserMapper.java
|
||||
E:\aug-ai\springboot-test\src\main\java\com\example\usertest\config\MyBatisPlusConfig.java
|
||||
@ -0,0 +1 @@
|
||||
E:\aug-ai\springboot-test\src\test\java\com\example\usertest\service\UserServiceTest.java
|
||||
@ -0,0 +1,5 @@
|
||||
# Created at 2025-06-27T18:09:51.601
|
||||
Boot Manifest-JAR contains absolute paths in classpath 'D:\program\maven-res\org\apache\maven\surefire\surefire-booter\3.0.0-M9\surefire-booter-3.0.0-M9.jar'
|
||||
Hint: <argLine>-Djdk.net.URLClassPath.disableClassPathURLCheck=true</argLine>
|
||||
'other' has different root
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.example.usertest.service.UserServiceTest$BoundaryTests
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.53 s - in com.example.usertest.service.UserServiceTest$BoundaryTests
|
||||
@ -0,0 +1,190 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.example.usertest.service.UserServiceTest$ChangePasswordTests
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 6, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.071 s <<< FAILURE! - in com.example.usertest.service.UserServiceTest$ChangePasswordTests
|
||||
com.example.usertest.service.UserServiceTest$ChangePasswordTests.changePassword_SamePassword Time elapsed: 0.014 s <<< FAILURE!
|
||||
org.opentest4j.AssertionFailedError:
|
||||
|
||||
Expecting message to be:
|
||||
"New password must be different from old password"
|
||||
but was:
|
||||
"Password must contain at least one digit"
|
||||
|
||||
Throwable that failed the check:
|
||||
|
||||
com.example.usertest.exception.ValidationException: Password must contain at least one digit
|
||||
at com.example.usertest.service.UserService.validatePassword(UserService.java:254)
|
||||
at com.example.usertest.service.UserService.validateChangePasswordRequest(UserService.java:237)
|
||||
at com.example.usertest.service.UserService.changePassword(UserService.java:126)
|
||||
at com.example.usertest.service.UserServiceTest$ChangePasswordTests.lambda$2(UserServiceTest.java:386)
|
||||
at org.assertj.core.api.ThrowableAssert.catchThrowable(ThrowableAssert.java:63)
|
||||
at org.assertj.core.api.AssertionsForClassTypes.catchThrowable(AssertionsForClassTypes.java:878)
|
||||
at org.assertj.core.api.Assertions.catchThrowable(Assertions.java:1337)
|
||||
at org.assertj.core.api.Assertions.assertThatThrownBy(Assertions.java:1181)
|
||||
at com.example.usertest.service.UserServiceTest$ChangePasswordTests.changePassword_SamePassword(UserServiceTest.java:386)
|
||||
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
|
||||
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.lang.reflect.Method.invoke(Method.java:498)
|
||||
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
|
||||
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
|
||||
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
|
||||
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
|
||||
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
|
||||
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
|
||||
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
|
||||
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
|
||||
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
|
||||
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
|
||||
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
|
||||
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
|
||||
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
|
||||
at java.util.ArrayList.forEach(ArrayList.java:1257)
|
||||
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
|
||||
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
|
||||
at java.util.ArrayList.forEach(ArrayList.java:1257)
|
||||
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
|
||||
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
|
||||
at java.util.ArrayList.forEach(ArrayList.java:1257)
|
||||
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
|
||||
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
|
||||
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
|
||||
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
|
||||
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
|
||||
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
|
||||
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
|
||||
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
|
||||
at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:55)
|
||||
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:223)
|
||||
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:175)
|
||||
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:139)
|
||||
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:456)
|
||||
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:169)
|
||||
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:595)
|
||||
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:581)
|
||||
|
||||
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
|
||||
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
|
||||
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
|
||||
at com.example.usertest.service.UserServiceTest$ChangePasswordTests.changePassword_SamePassword(UserServiceTest.java:388)
|
||||
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
|
||||
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.lang.reflect.Method.invoke(Method.java:498)
|
||||
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
|
||||
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
|
||||
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
|
||||
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
|
||||
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
|
||||
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
|
||||
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
|
||||
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
|
||||
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
|
||||
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
|
||||
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
|
||||
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
|
||||
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
|
||||
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
|
||||
at java.util.ArrayList.forEach(ArrayList.java:1257)
|
||||
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
|
||||
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
|
||||
at java.util.ArrayList.forEach(ArrayList.java:1257)
|
||||
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
|
||||
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
|
||||
at java.util.ArrayList.forEach(ArrayList.java:1257)
|
||||
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
|
||||
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
|
||||
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
|
||||
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
|
||||
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
|
||||
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
|
||||
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
|
||||
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
|
||||
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
|
||||
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
|
||||
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
|
||||
at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:55)
|
||||
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:223)
|
||||
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:175)
|
||||
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:139)
|
||||
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:456)
|
||||
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:169)
|
||||
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:595)
|
||||
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:581)
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.example.usertest.service.UserServiceTest$DeleteUserTests
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.017 s - in com.example.usertest.service.UserServiceTest$DeleteUserTests
|
||||
@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.example.usertest.service.UserServiceTest$GetUserTests
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 s - in com.example.usertest.service.UserServiceTest$GetUserTests
|
||||
@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.example.usertest.service.UserServiceTest$LoginUserTests
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.035 s - in com.example.usertest.service.UserServiceTest$LoginUserTests
|
||||
@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.example.usertest.service.UserServiceTest$MockVerificationTests
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.02 s - in com.example.usertest.service.UserServiceTest$MockVerificationTests
|
||||
@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.example.usertest.service.UserServiceTest$RegisterUserTests
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.034 s - in com.example.usertest.service.UserServiceTest$RegisterUserTests
|
||||
@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.example.usertest.service.UserServiceTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.749 s - in com.example.usertest.service.UserServiceTest
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user