translate properly docs/

This commit is contained in:
AlexisLeDain
2026-04-08 21:49:38 +02:00
parent 08eb812da6
commit b54ce43ef3
12 changed files with 1158 additions and 2759 deletions
+96 -124
View File
@@ -1,29 +1,27 @@
---
name: quarkus-patterns
description: Quarkus 3.x LTS architecture patterns with Camel for messaging, RESTful API design, CDI services, data access with Panache, and async processing. Use for Java Quarkus backend work with event-driven architectures.
description: Quarkus 3.x LTS架构模式,Camel消息传递、RESTful API设计、CDI服务、Panache数据访问和异步处理。用于具有事件驱动架构的Java Quarkus后端工作。
origin: ECC
---
> **Note / 注意**: 本文件尚未翻译为中文,目前为英文原版。欢迎提交翻译 PR。
# Quarkus 开发模式
# Quarkus Development Patterns
使用Apache Camel的云原生事件驱动服务的Quarkus 3.x架构和API模式。
Quarkus 3.x architecture and API patterns for cloud-native, event-driven services with Apache Camel.
## 何时激活
## When to Activate
- 使用JAX-RS或RESTEasy Reactive构建REST API
- 构建资源 → 服务 → 仓库层结构
- 使用Apache Camel和RabbitMQ实现事件驱动模式
- 配置Hibernate Panache、缓存或响应式流
- 添加验证、异常映射或分页
- 为开发/预发布/生产环境设置配置文件(YAML配置)
- 使用LogContext和Logback/Logstash编码器进行自定义日志记录
- 使用CompletableFuture进行异步操作
- 实现条件流处理
- 使用GraalVM原生编译
- Building REST APIs with JAX-RS or RESTEasy Reactive
- Structuring resource → service → repository layers
- Implementing event-driven patterns with Apache Camel and RabbitMQ
- Configuring Hibernate Panache, caching, or reactive streams
- Adding validation, exception mapping, or pagination
- Setting up profiles for dev/staging/production environments (YAML config)
- Custom logging with LogContext and Logback/Logstash encoder
- Working with CompletableFuture for async operations
- Implementing conditional flow processing
- Working with GraalVM native compilation
## Service Layer with Multiple Dependencies (Lombok)
## 多依赖服务层(Lombok
```java
@Slf4j
@@ -43,7 +41,7 @@ public class As2ProcessingService {
String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID);
// Conditional flow logic
// 条件流逻辑
boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW));
log.info("Is CHORUS_FLOW message: {}", isChorusFlow);
@@ -62,7 +60,7 @@ public class As2ProcessingService {
log.info("Invoice validation completed. Message is valid");
// CompletableFuture async operation
// CompletableFuture异步操作
try(InputStream inputStream = Files.newInputStream(filePath)) {
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
fileStorageService.uploadOriginalFile(inputStream,
@@ -85,7 +83,7 @@ public class As2ProcessingService {
documentInfo, originalFileName, structureIdPartner,
flowProfile, invoiceValidationResult.getDocumentHash());
// Async Camel publishing
// 异步Camel发布
businessRulesPublisher.publishAsync(payload);
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
}
@@ -94,16 +92,16 @@ public class As2ProcessingService {
}
```
**Key Patterns:**
- `@RequiredArgsConstructor` for constructor injection via Lombok
- `@Slf4j` for Logback logging
- Scoped LogContext with try-with-resources
- Conditional flow logic based on runtime parameters
- CompletableFuture with `.join()` for async operations
- Event tracking for success/error scenarios
- Async Camel message publishing
**关键模式:**
- 通过Lombok的`@RequiredArgsConstructor`进行构造函数注入
- 通过`@Slf4j`进行Logback日志记录
- 使用try-with-resources的作用域LogContext
- 基于运行时参数的条件流逻辑
- 使用`.join()`CompletableFuture异步操作
- 成功/错误场景的事件跟踪
- 异步Camel消息发布
## Custom Logging Context Pattern (Logback)
## 自定义日志上下文模式(Logback
```java
@ApplicationScoped
@@ -112,14 +110,14 @@ public class ProcessingService {
public void processDocument(Document doc) {
LogContext logContext = CustomLog.getCurrentContext();
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
// Add context to all log statements
// 向所有日志语句添加上下文
logContext.put("documentId", doc.getId().toString());
logContext.put("documentType", doc.getType());
logContext.put("userId", SecurityContext.getUserId());
log.info("Starting document processing");
// All logs within this scope inherit the context
// 此作用域内的所有日志都继承上下文
processInternal(doc);
log.info("Document processing completed");
@@ -131,7 +129,7 @@ public class ProcessingService {
}
```
**Logback Configuration (logback.xml):**
**Logback配置(logback.xml:**
```xml
<configuration>
@@ -149,7 +147,7 @@ public class ProcessingService {
</configuration>
```
## Event Service Pattern
## 事件服务模式
```java
@ApplicationScoped
@@ -181,13 +179,13 @@ public class EventService {
}
private String serializePayload(Object payload) {
// JSON serialization
// JSON序列化
return objectMapper.writeValueAsString(payload);
}
}
```
## Camel Message Publishing (RabbitMQ)
## Camel消息发布(RabbitMQ
```java
@ApplicationScoped
@@ -215,7 +213,7 @@ public class BusinessRulesPublisher {
}
```
**Camel Route Configuration:**
**Camel路由配置:**
```java
@ApplicationScoped
@@ -242,7 +240,7 @@ public class BusinessRulesRoute extends RouteBuilder {
}
```
## Camel Direct Routes (In-Memory)
## Camel Direct路由(内存中)
```java
@ApplicationScoped
@@ -250,13 +248,13 @@ public class DocumentProcessingRoute extends RouteBuilder {
@Override
public void configure() {
// Error handling
// 错误处理
onException(ValidationException.class)
.handled(true)
.to("direct:validation-error-handler")
.log("Validation error: ${exception.message}");
// Main processing route
// 主处理路由
from("direct:process-document")
.routeId("document-processing")
.log("Processing document: ${header.documentId}")
@@ -278,7 +276,7 @@ public class DocumentProcessingRoute extends RouteBuilder {
}
```
## Camel File Processing
## Camel文件处理
```java
@ApplicationScoped
@@ -308,27 +306,7 @@ public class FileMonitoringRoute extends RouteBuilder {
}
```
## Camel Bean Invocation
```java
@ApplicationScoped
public class InvoiceRoute extends RouteBuilder {
@Override
public void configure() {
from("direct:invoice-validation")
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
.log("Validation result: ${body}");
from("direct:persist-and-publish")
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
.bean(BusinessRulesPublisher.class, "publishAsync")
.bean(EventService.class, "createSuccessEvent(${body}, 'PUBLISHED')");
}
}
```
## REST API Structure
## REST API结构
```java
@Path("/api/documents")
@@ -367,7 +345,7 @@ public class DocumentResource {
}
```
## Repository Pattern (Panache Repository)
## 仓库模式(Panache Repository
```java
@ApplicationScoped
@@ -389,7 +367,7 @@ public class DocumentRepository implements PanacheRepository<Document> {
}
```
## Service Layer with Transactions
## 带事务的服务层
```java
@ApplicationScoped
@@ -416,16 +394,10 @@ public class DocumentService {
public Optional<Document> findById(Long id) {
return repo.findByIdOptional(id);
}
public PaginatedList<Document> list(int page, int size) {
return repo.findAll()
.page(page, size)
.list();
}
}
```
## DTOs and Validation
## DTO和验证
```java
public record CreateDocumentRequest(
@@ -442,7 +414,7 @@ public record DocumentResponse(Long id, String referenceNumber, DocumentStatus s
}
```
## Exception Mapping
## 异常映射
```java
@Provider
@@ -473,7 +445,7 @@ public class GenericExceptionMapper implements ExceptionMapper<Exception> {
}
```
## CompletableFuture Async Operations
## CompletableFuture异步操作
```java
@ApplicationScoped
@@ -512,7 +484,7 @@ public class FileStorageService {
}
```
## Caching
## 缓存
```java
@ApplicationScoped
@@ -533,7 +505,7 @@ public class DocumentCacheService {
}
```
## Configuration as YAML
## YAML配置
```yaml
# application.yml
@@ -580,7 +552,7 @@ public class DocumentCacheService {
username: ${RABBITMQ_USER}
password: ${RABBITMQ_PASSWORD}
# Camel configuration
# Camel配置
camel:
rabbitmq:
queue:
@@ -588,7 +560,7 @@ camel:
invoice-processing: invoice-processing-queue
```
## Health Checks
## 健康检查
```java
@Readiness
@@ -626,7 +598,7 @@ public class CamelHealthCheck implements HealthCheck {
}
```
## Dependencies (Maven)
## 依赖(Maven
```xml
<properties>
@@ -657,7 +629,7 @@ public class CamelHealthCheck implements HealthCheck {
</dependencyManagement>
<dependencies>
<!-- Quarkus Core -->
<!-- Quarkus核心 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
@@ -667,7 +639,7 @@ public class CamelHealthCheck implements HealthCheck {
<artifactId>quarkus-config-yaml</artifactId>
</dependency>
<!-- Camel Extensions -->
<!-- Camel扩展 -->
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
@@ -689,7 +661,7 @@ public class CamelHealthCheck implements HealthCheck {
<scope>provided</scope>
</dependency>
<!-- Logging -->
<!-- 日志 -->
<dependency>
<groupId>io.quarkiverse.logging.logback</groupId>
<artifactId>quarkus-logging-logback</artifactId>
@@ -701,56 +673,56 @@ public class CamelHealthCheck implements HealthCheck {
</dependencies>
```
## Best Practices
## 最佳实践
### Architecture
- Use `@RequiredArgsConstructor` with Lombok for constructor injection
- Keep service layer thin; delegate complex logic to specialized classes
- Use Camel routes for message routing and integration patterns
- Prefer Panache Repository pattern for data access
### 架构
- 使用Lombok的`@RequiredArgsConstructor`进行构造函数注入
- 保持服务层精简,将复杂逻辑委托给专门的类
- 使用Camel路由进行消息路由和集成模式
- 数据访问优先使用Panache Repository模式
### Event-Driven
- Always track operations with EventService (success/error events)
- Use Camel `direct:` endpoints for in-memory routing
- Use `spring-rabbitmq` component for RabbitMQ integration
- Implement async publishing with `ProducerTemplate.asyncSendBody()`
### 事件驱动
- 始终使用EventService跟踪操作(成功/错误事件)
- 使用Camel`direct:`端点进行内存路由
- 使用`spring-rabbitmq`组件进行RabbitMQ集成
- 使用`ProducerTemplate.asyncSendBody()`实现异步发布
### Logging
- Use Logback with Logstash encoder for structured logging
- Propagate LogContext through service calls with `SafeAutoCloseable`
- Add contextual information to LogContext for request tracing
- Use `@Slf4j` instead of manual logger instantiation
### 日志
- 使用Logstash编码器的Logback进行结构化日志
- 使用`SafeAutoCloseable`在服务调用间传播LogContext
- 向LogContext添加上下文信息以进行请求追踪
- 使用`@Slf4j`代替手动日志实例化
### Async Operations
- Use CompletableFuture for non-blocking I/O operations
- Call `.join()` when you need to wait for completion
- Handle exceptions from CompletableFuture properly
- Pass LogContext to async operations for tracing
### 异步操作
- 使用CompletableFuture进行非阻塞I/O操作
- 需要等待完成时调用`.join()`
- 正确处理CompletableFuture的异常
- 为追踪目的向异步操作传递LogContext
### Configuration
- Use YAML configuration (`quarkus-config-yaml`)
- Profile-aware configuration for dev/test/prod environments
- Externalize sensitive configuration to environment variables
- Use `@ConfigProperty` for type-safe config injection
### 配置
- 使用YAML配置(`quarkus-config-yaml`
- dev/test/prod环境的配置文件感知配置
- 将敏感配置外部化到环境变量
- 使用`@ConfigProperty`进行类型安全的配置注入
### Validation
- Validate at resource layer with `@Valid`
- Use Bean Validation annotations on DTOs
- Map exceptions to proper HTTP responses with `@Provider`
### 验证
- 在资源层使用`@Valid`进行验证
- 在DTO上使用Bean Validation注解
- 使用`@Provider`将异常映射到适当的HTTP响应
### Transactions
- Use `@Transactional` on service methods that modify data
- Keep transactions short and focused
- Avoid calling async operations within transactions
### 事务
- 在修改数据的服务方法上使用`@Transactional`
- 保持事务短小且聚焦
- 避免在事务内调用异步操作
### Testing
- Use `camel-quarkus-junit5` for route testing
- Use AssertJ for assertions
- Mock all external dependencies
- Test conditional flow logic thoroughly
### 测试
- 使用`camel-quarkus-junit5`进行路由测试
- 使用AssertJ进行断言
- 模拟所有外部依赖
- 彻底测试条件流逻辑
### Quarkus-Specific
- Stay on latest LTS version (3.x)
- Use Quarkus dev mode for hot reload
- Add health checks for production readiness
- Test native compilation compatibility periodically
### Quarkus特定
- 保持最新的LTS版本(3.x
- 使用Quarkus开发模式进行热重载
- 添加健康检查以确保生产就绪
- 定期测试原生编译兼容性
+61 -154
View File
@@ -1,32 +1,29 @@
---
name: quarkus-security
description: Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.
description: Quarkus安全最佳实践:认证、授权、JWT/OIDCRBAC、输入验证、CSRF、密钥管理和依赖安全。
origin: ECC
---
> **Note / 注意**: 本文件尚未翻译为中文,目前为英文原版。欢迎提交翻译 PR。
# Quarkus 安全审查
# Quarkus Security Review
使用认证、授权和输入验证保护Quarkus应用程序的最佳实践。
Best practices for securing Quarkus applications with authentication, authorization, and input validation.
## 何时激活
## When to Activate
- 添加认证(JWT、OIDC、Basic Auth
- 使用@RolesAllowed或SecurityIdentity实现授权
- 验证用户输入(Bean Validation、自定义验证器)
- 配置CORS或安全头
- 管理密钥(Vault、环境变量、配置源)
- 添加速率限制或暴力破解保护
- 扫描依赖CVE
- 使用MicroProfile JWT或SmallRye JWT
- Adding authentication (JWT, OIDC, Basic Auth)
- Implementing authorization with @RolesAllowed or SecurityIdentity
- Validating user input (Bean Validation, custom validators)
- Configuring CORS or security headers
- Managing secrets (Vault, environment variables, config sources)
- Adding rate limiting or brute-force protection
- Scanning dependencies for CVEs
- Working with MicroProfile JWT or SmallRye JWT
## 认证
## Authentication
### JWT Authentication
### JWT认证
```java
// Resource protected with JWT
@Path("/api/protected")
@Authenticated
public class ProtectedResource {
@@ -50,7 +47,7 @@ public class ProtectedResource {
}
```
Configuration (application.properties):
配置(application.properties:
```properties
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=https://auth.example.com
@@ -61,7 +58,7 @@ quarkus.oidc.client-id=backend-service
quarkus.oidc.credentials.secret=${OIDC_SECRET}
```
### Custom Authentication Filter
### 自定义认证过滤器
```java
@Provider
@@ -77,7 +74,6 @@ public class CustomAuthFilter implements ContainerRequestFilter {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
// Validate token and set SecurityIdentity
if (!validateToken(token)) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
}
@@ -85,15 +81,15 @@ public class CustomAuthFilter implements ContainerRequestFilter {
}
private boolean validateToken(String token) {
// Token validation logic
// 令牌验证逻辑
return true;
}
}
```
## Authorization
## 授权
### Role-Based Access Control
### 基于角色的访问控制
```java
@Path("/api/admin")
@@ -125,21 +121,17 @@ public class UserResource {
@Path("/{id}")
@RolesAllowed("USER")
public Response getUser(@PathParam("id") Long id) {
// Check ownership
// 所有权检查
if (!securityIdentity.hasRole("ADMIN") &&
!isOwner(id, securityIdentity.getPrincipal().getName())) {
return Response.status(Response.Status.FORBIDDEN).build();
}
return Response.ok(userService.findById(id)).build();
}
private boolean isOwner(Long userId, String username) {
return userService.isOwner(userId, username);
}
}
```
### Programmatic Security
### 编程式安全
```java
@ApplicationScoped
@@ -163,18 +155,18 @@ public class SecurityService {
}
```
## Input Validation
## 输入验证
### Bean Validation
```java
// BAD: No validation
// BAD: 无验证
@POST
public Response createUser(UserDto dto) {
return Response.ok(userService.create(dto)).build();
}
// GOOD: Validated DTO
// GOOD: 验证DTO
public record CreateUserDto(
@NotBlank @Size(max = 100) String name,
@NotBlank @Email String email,
@@ -190,55 +182,28 @@ public Response createUser(@Valid CreateUserDto dto) {
}
```
### Custom Validators
## SQL注入防护
### Panache Active Record(默认安全)
```java
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UsernameValidator.class)
public @interface ValidUsername {
String message() default "Invalid username format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return false;
return value.matches("^[a-zA-Z0-9_-]{3,20}$");
}
}
// Usage
public record CreateUserDto(
@ValidUsername String username,
@NotBlank @Email String email
) {}
```
## SQL Injection Prevention
### Panache Active Record (Safe by Default)
```java
// GOOD: Parameterized queries with Panache
// GOOD: Panache参数化查询
List<User> users = User.list("email = ?1 and active = ?2", email, true);
Optional<User> user = User.find("username", username).firstResultOptional();
// GOOD: Named parameters
// GOOD: 命名参数
List<User> users = User.list("email = :email and age > :minAge",
Parameters.with("email", email).and("minAge", 18));
```
### Native Queries (Use Parameters)
### 原生查询(使用参数)
```java
// BAD: String concatenation
// BAD: 字符串拼接
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
// GOOD: Parameterized native query
// GOOD: 参数化原生查询
@Entity
public class User extends PanacheEntity {
public static List<User> findByEmailNative(String email) {
@@ -250,7 +215,7 @@ public class User extends PanacheEntity {
}
```
## Password Hashing
## 密码哈希
```java
@ApplicationScoped
@@ -264,33 +229,9 @@ public class PasswordService {
return BcryptUtil.matches(plainPassword, hashedPassword);
}
}
// In service
@ApplicationScoped
public class UserService {
@Inject
PasswordService passwordService;
@Transactional
public User register(CreateUserDto dto) {
String hashedPassword = passwordService.hash(dto.password());
User user = new User();
user.email = dto.email();
user.password = hashedPassword;
user.persist();
return user;
}
public boolean authenticate(String email, String password) {
return User.find("email", email)
.firstResultOptional()
.map(u -> passwordService.verify(password, u.password))
.orElse(false);
}
}
```
## CORS Configuration
## CORS配置
```properties
# application.properties
@@ -303,37 +244,22 @@ quarkus.http.cors.access-control-max-age=24H
quarkus.http.cors.access-control-allow-credentials=true
```
## Secrets Management
## 密钥管理
```properties
# application.properties - NO SECRETS HERE
# application.properties — 此处不放密钥
# Use environment variables
# 使用环境变量
quarkus.datasource.username=${DB_USER}
quarkus.datasource.password=${DB_PASSWORD}
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
# Or use Vault
# 或使用Vault
quarkus.vault.url=https://vault.example.com
quarkus.vault.authentication.kubernetes.role=my-role
```
### HashiCorp Vault Integration
```java
@ApplicationScoped
public class SecretService {
@ConfigProperty(name = "api-key")
String apiKey; // Fetched from Vault
public String getSecret(String key) {
return ConfigProvider.getConfig().getValue(key, String.class);
}
}
```
## Rate Limiting
## 速率限制
```java
@ApplicationScoped
@@ -344,7 +270,7 @@ public class RateLimitFilter implements ContainerRequestFilter {
public void filter(ContainerRequestContext requestContext) {
String clientId = getClientIdentifier(requestContext);
RateLimiter limiter = limiters.computeIfAbsent(clientId,
k -> RateLimiter.create(100.0)); // 100 requests per second
k -> RateLimiter.create(100.0)); // 每秒100个请求
if (!limiter.tryAcquire()) {
requestContext.abortWith(
@@ -354,15 +280,10 @@ public class RateLimitFilter implements ContainerRequestFilter {
);
}
}
private String getClientIdentifier(ContainerRequestContext ctx) {
// Use IP, API key, or user ID
return ctx.getHeaderString("X-Forwarded-For");
}
}
```
## Security Headers
## 安全头
```java
@Provider
@@ -372,10 +293,10 @@ public class SecurityHeadersFilter implements ContainerResponseFilter {
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
MultivaluedMap<String, Object> headers = response.getHeaders();
// Prevent clickjacking
// 防止点击劫持
headers.putSingle("X-Frame-Options", "DENY");
// XSS protection
// XSS保护
headers.putSingle("X-Content-Type-Options", "nosniff");
headers.putSingle("X-XSS-Protection", "1; mode=block");
@@ -389,7 +310,7 @@ public class SecurityHeadersFilter implements ContainerResponseFilter {
}
```
## Audit Logging
## 审计日志
```java
@ApplicationScoped
@@ -408,23 +329,9 @@ public class AuditService {
user, action, resource, Instant.now());
}
}
// Usage in resource
@Path("/api/sensitive")
public class SensitiveResource {
@Inject
AuditService auditService;
@GET
@RolesAllowed("ADMIN")
public Response getData() {
auditService.logAccess("sensitive-data", "READ");
return Response.ok(data).build();
}
}
```
## Dependency Security Scanning
## 依赖安全扫描
```bash
# Maven
@@ -433,23 +340,23 @@ mvn org.owasp:dependency-check-maven:check
# Gradle
./gradlew dependencyCheckAnalyze
# Check Quarkus extensions
# 检查Quarkus扩展
quarkus extension list --installable
```
## Best Practices
## 最佳实践
- Always use HTTPS in production
- Enable JWT or OIDC for stateless authentication
- Use `@RolesAllowed` for declarative authorization
- Validate all input with Bean Validation
- Hash passwords with BCrypt (never plaintext)
- Store secrets in Vault or environment variables
- Use parameterized queries to prevent SQL injection
- Add security headers to all responses
- Implement rate limiting for public endpoints
- Audit sensitive operations
- Keep dependencies updated and scan for CVEs
- Use SecurityIdentity for programmatic checks
- Set appropriate CORS policies
- Test authentication and authorization paths
- 生产环境始终使用HTTPS
- 启用JWT或OIDC进行无状态认证
- 使用`@RolesAllowed`进行声明式授权
- 使用Bean Validation验证所有输入
- 使用BCrypt哈希密码(禁止明文)
- 将密钥存储在Vault或环境变量中
- 使用参数化查询防止SQL注入
- 为所有响应添加安全头
- 为公共端点实现速率限制
- 审计敏感操作
- 保持依赖更新并扫描CVE
- 使用SecurityIdentity进行编程式检查
- 设置适当的CORS策略
- 测试认证和授权路径
+81 -616
View File
@@ -1,36 +1,34 @@
---
name: quarkus-tdd
description: Test-driven development for Quarkus 3.x LTS using JUnit 5, Mockito, REST Assured, Camel testing, and JaCoCo. Use when adding features, fixing bugs, or refactoring event-driven services.
description: 使用JUnit 5MockitoREST AssuredCamel测试和JaCoCo的Quarkus 3.x LTS测试驱动开发。用于添加功能、修复错误或重构事件驱动服务。
origin: ECC
---
> **Note / 注意**: 本文件尚未翻译为中文,目前为英文原版。欢迎提交翻译 PR。
# Quarkus TDD工作流
# Quarkus TDD Workflow
面向80%以上覆盖率(单元+集成)的Quarkus 3.x服务TDD指南。针对Apache Camel的事件驱动架构优化。
TDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.
## 何时使用
## When to Use
- 新功能或REST端点
- Bug修复或重构
- 添加数据访问逻辑、安全规则或响应式流
- 测试Apache Camel路由和事件处理器
- 测试RabbitMQ事件驱动服务
- 测试条件流逻辑
- 验证CompletableFuture异步操作
- 测试LogContext传播
- New features or REST endpoints
- Bug fixes or refactors
- Adding data access logic, security rules, or reactive streams
- Testing Apache Camel routes and event handlers
- Testing event-driven services with RabbitMQ
- Testing conditional flow logic
- Validating CompletableFuture async operations
- Testing LogContext propagation
## 工作流
## Workflow
1. 先写测试(应该失败)
2. 实现通过测试的最少代码
3. 测试通过后重构
4. 使用JaCoCo强制覆盖率(80%以上目标)
1. Write tests first (they should fail)
2. Implement minimal code to pass
3. Refactor with tests green
4. Enforce coverage with JaCoCo (80%+ target)
## 使用@Nested组织的单元测试
## Unit Tests with @Nested Organization
Follow this structured approach for comprehensive, readable tests:
全面、可读测试的结构化方法:
```java
@ExtendWith(MockitoExtension.class)
@@ -62,7 +60,7 @@ class As2ProcessingServiceTest {
@BeforeEach
void setUp() {
// ARRANGE - Common test data
// ARRANGE - 公共测试数据
testFilePath = Path.of("/tmp/test-invoice.xml");
testLogContext = new LogContext();
@@ -119,48 +117,9 @@ class As2ProcessingServiceTest {
verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class),
eq("PERSISTENCE_BLOB_EVENT_TYPE"));
verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class),
eq("BUSINESS_RULES_MESSAGE_SENT"));
verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class));
}
@Test
@DisplayName("Should bypass schematron validation for CHORUS_FLOW")
void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception {
// ARRANGE
testLogContext.put(As2Constants.CHORUS_FLOW, "true");
CustomLog.setCurrentContext(testLogContext);
when(invoiceFlowValidator.validateFlowWithConfig(
eq(testFilePath),
eq(ValidationFlowConfig.xsdOnly()),
eq(EInvoiceSyntaxFormat.UBL),
any(LogContext.class)))
.thenReturn(validationResult);
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(documentInfo));
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(),
eq(FlowProfile.EXTENDED_CTC_FR), any()))
.thenReturn(new BusinessRulesPayload());
// ACT
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
// ASSERT
verify(invoiceFlowValidator).validateFlowWithConfig(
eq(testFilePath),
eq(ValidationFlowConfig.xsdOnly()),
eq(EInvoiceSyntaxFormat.UBL),
any(LogContext.class));
verify(documentJobService).createDocumentAndJobEntities(
any(), any(), any(),
eq(FlowProfile.EXTENDED_CTC_FR),
any());
}
@Test
@DisplayName("Should create error event when file upload fails")
void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception {
@@ -174,7 +133,7 @@ class As2ProcessingServiceTest {
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
.thenReturn(FlowProfile.BASIC);
documentInfo.setPath(""); // Blank path triggers error
documentInfo.setPath(""); // 空路径触发错误
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(documentInfo));
@@ -187,71 +146,26 @@ class As2ProcessingServiceTest {
assertThat(exception.getMessage())
.contains("File path is empty after upload");
verify(eventService).createErrorEvent(
eq(documentInfo),
eq("FILE_UPLOAD_FAILED"),
contains("File path is empty"));
verify(businessRulesPublisher, never()).publishAsync(any());
}
@Test
@DisplayName("Should handle CompletableFuture.join() failure")
void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception {
// ARRANGE
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
CustomLog.setCurrentContext(testLogContext);
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
.thenReturn(validationResult);
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
.thenReturn(FlowProfile.BASIC);
CompletableFuture<StoredDocumentInfo> failedFuture =
CompletableFuture.failedFuture(new StorageException("S3 connection failed"));
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
.thenReturn(failedFuture);
// ACT & ASSERT
assertThrows(
CompletionException.class,
() -> as2ProcessingService.processFile(testFilePath)
);
}
@Test
@DisplayName("Should throw exception when file path is null")
void givenNullFilePath_whenProcessFile_thenThrowsException() {
// ARRANGE
Path nullPath = null;
// ACT & ASSERT
NullPointerException exception = assertThrows(
NullPointerException.class,
() -> as2ProcessingService.processFile(nullPath)
);
verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any());
}
}
}
```
### Key Testing Patterns
### 关键测试模式
1. **@Nested Classes**: Group tests by method being tested
2. **@DisplayName**: Provide readable test descriptions for test reports
3. **Naming Convention**: `givenX_whenY_thenZ` for clarity
4. **AAA Pattern**: Explicit `// ARRANGE`, `// ACT`, `// ASSERT` comments
5. **@BeforeEach**: Setup common test data to reduce duplication
6. **assertDoesNotThrow**: Test success scenarios without catching exceptions
7. **assertThrows**: Test exception scenarios with message validation using AssertJ
8. **Comprehensive Coverage**: Test happy paths, null inputs, edge cases, exceptions
9. **Verify Interactions**: Use Mockito `verify()` to ensure methods are called correctly
10. **Never Verify**: Use `never()` to ensure methods are NOT called in error scenarios
1. **@Nested**: 按被测方法分组测试
2. **@DisplayName**: 为测试报告提供可读描述
3. **命名约定**: 使用`givenX_whenY_thenZ`确保清晰
4. **AAA模式**: 明确的`// ARRANGE``// ACT``// ASSERT`注释
5. **@BeforeEach**: 通用测试数据设置以减少重复
6. **assertDoesNotThrow**: 不捕获异常的成功场景测试
7. **assertThrows**: 带消息验证的异常场景测试
8. **全面覆盖**: 测试正常路径、null输入、边界情况、异常
9. **验证交互**: 使用Mockito`verify()`确保方法被正确调用
10. **Never验证**: 使用`never()`确保错误场景中方法未被调用
## Testing Camel Routes
## 测试Camel路由
```java
@QuarkusTest
@@ -267,338 +181,30 @@ class BusinessRulesRouteTest {
@InjectMock
EventService eventService;
private BusinessRulesPayload testPayload;
@BeforeEach
void setUp() {
// ARRANGE - Test data
testPayload = new BusinessRulesPayload();
testPayload.setDocumentId(1L);
testPayload.setFlowProfile(FlowProfile.BASIC);
}
@Nested
@DisplayName("Tests for business-rules-publisher route")
class BusinessRulesPublisher {
@Test
@DisplayName("Should successfully publish message to RabbitMQ")
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
// ARRANGE
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
mockRabbitMQ.expectedMessageCount(1);
mockRabbitMQ.expectedBodiesReceived(testPayload);
// Replace real endpoint with mock for testing
camelContext.getRouteController().stopRoute("business-rules-publisher");
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
advice.replaceFromWith("direct:business-rules-publisher");
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
});
camelContext.getRouteController().startRoute("business-rules-publisher");
// ACT
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
// ASSERT
mockRabbitMQ.assertIsSatisfied(5000);
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
assertThat(mockRabbitMQ.getExchanges().get(0).getIn().getBody(BusinessRulesPayload.class))
.isEqualTo(testPayload);
}
@Test
@DisplayName("Should handle marshalling to JSON")
void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {
// ARRANGE
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
camelContext.addEndpoint("mock:marshal", mockMarshal);
mockMarshal.expectedMessageCount(1);
camelContext.getRouteController().stopRoute("business-rules-publisher");
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
advice.weaveAddLast().to("mock:marshal");
});
camelContext.getRouteController().startRoute("business-rules-publisher");
// ACT
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
// ASSERT
mockMarshal.assertIsSatisfied(5000);
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
assertThat(body).contains("\"documentId\":1");
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
}
}
@Nested
@DisplayName("Tests for document-processing route")
class DocumentProcessing {
@Test
@DisplayName("Should route invoice to correct processor")
void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {
// ARRANGE
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
mockInvoice.expectedMessageCount(1);
camelContext.getRouteController().stopRoute("document-processing");
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
});
camelContext.getRouteController().startRoute("document-processing");
// ACT
producerTemplate.sendBodyAndHeader("direct:process-document",
testPayload, "documentType", "INVOICE");
// ASSERT
mockInvoice.assertIsSatisfied(5000);
}
@Test
@DisplayName("Should handle validation errors gracefully")
void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {
// ARRANGE
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
mockError.expectedMessageCount(1);
camelContext.getRouteController().stopRoute("document-processing");
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
advice.weaveByToString(".*direct:validation-error-handler.*")
.replace().to("mock:error");
});
camelContext.getRouteController().startRoute("document-processing");
// Mock validator to throw exception
when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document"));
// ACT
producerTemplate.sendBody("direct:process-document", testPayload);
// ASSERT
mockError.assertIsSatisfied(5000);
Exception exception = mockError.getExchanges().get(0).getException();
assertThat(exception).isInstanceOf(ValidationException.class);
assertThat(exception.getMessage()).contains("Invalid document");
}
}
}
```
## Testing Event Services
```java
@ExtendWith(MockitoExtension.class)
@DisplayName("EventService Unit Tests")
class EventServiceTest {
@Mock
private EventRepository eventRepository;
@Mock
private ObjectMapper objectMapper;
@InjectMocks
private EventService eventService;
private BusinessRulesPayload testPayload;
@BeforeEach
void setUp() {
@Test
@DisplayName("Should successfully publish message to RabbitMQ")
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
// ARRANGE
testPayload = new BusinessRulesPayload();
testPayload.setDocumentId(1L);
}
@Nested
@DisplayName("Tests for createSuccessEvent")
class CreateSuccessEvent {
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
mockRabbitMQ.expectedMessageCount(1);
@Test
@DisplayName("Should create success event with correct attributes")
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
// ARRANGE
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
// ACT
assertDoesNotThrow(() ->
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
// ASSERT
verify(eventRepository).persist(argThat(event ->
event.getType().equals("DOCUMENT_PROCESSED") &&
event.getStatus() == EventStatus.SUCCESS &&
event.getPayload().equals("{\"documentId\":1}") &&
event.getTimestamp() != null
));
}
@Test
@DisplayName("Should throw exception when payload is null")
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
// ARRANGE
Object nullPayload = null;
// ACT & ASSERT
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
);
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
verify(eventRepository, never()).persist(any());
}
}
@Nested
@DisplayName("Tests for createErrorEvent")
class CreateErrorEvent {
camelContext.getRouteController().stopRoute("business-rules-publisher");
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
advice.replaceFromWith("direct:business-rules-publisher");
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
});
camelContext.getRouteController().startRoute("business-rules-publisher");
@Test
@DisplayName("Should create error event with error message")
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
// ARRANGE
String errorMessage = "Processing failed";
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
// ACT
assertDoesNotThrow(() ->
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
// ASSERT
verify(eventRepository).persist(argThat(event ->
event.getType().equals("PROCESSING_ERROR") &&
event.getStatus() == EventStatus.ERROR &&
event.getErrorMessage().equals(errorMessage) &&
event.getPayload().equals("{\"documentId\":1}")
));
}
@ParameterizedTest
@DisplayName("Should reject invalid error messages")
@ValueSource(strings = {"", " "})
void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
// ACT & ASSERT
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
);
assertThat(exception.getMessage()).contains("Error message cannot be blank");
}
// ACT
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
// ASSERT
mockRabbitMQ.assertIsSatisfied(5000);
}
}
```
## Testing CompletableFuture
```java
@ExtendWith(MockitoExtension.class)
@DisplayName("FileStorageService Unit Tests")
class FileStorageServiceTest {
@Mock
private S3Client s3Client;
@Mock
private ExecutorService executorService;
@InjectMocks
private FileStorageService fileStorageService;
private InputStream testInputStream;
private LogContext testLogContext;
@BeforeEach
void setUp() {
// ARRANGE
testInputStream = new ByteArrayInputStream("test content".getBytes());
testLogContext = new LogContext();
testLogContext.put("traceId", "trace-123");
}
@Nested
@DisplayName("Tests for uploadOriginalFile")
class UploadOriginalFile {
@Test
@DisplayName("Should successfully upload file and return document info")
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
// ARRANGE
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
Callable<?> callable = invocation.getArgument(0);
return CompletableFuture.completedFuture(callable.call());
});
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
.thenReturn(PutObjectResponse.builder().build());
// ACT
CompletableFuture<StoredDocumentInfo> future =
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
testLogContext, InvoiceFormat.UBL);
StoredDocumentInfo result = future.join();
// ASSERT
assertThat(result).isNotNull();
assertThat(result.getPath()).isNotBlank();
assertThat(result.getSize()).isEqualTo(1024L);
assertThat(result.getUploadedAt()).isNotNull();
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
}
@Test
@DisplayName("Should handle S3 upload failure")
void givenS3Failure_whenUpload_thenCompletableFutureFails() {
// ARRANGE
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
return CompletableFuture.failedFuture(new StorageException("S3 unavailable"));
});
// ACT
CompletableFuture<StoredDocumentInfo> future =
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
testLogContext, InvoiceFormat.UBL);
// ASSERT
assertThatThrownBy(() -> future.join())
.isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(StorageException.class)
.hasMessageContaining("S3 unavailable");
}
@Test
@DisplayName("Should propagate LogContext to async operation")
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
// ARRANGE
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
Callable<?> callable = invocation.getArgument(0);
capturedContext.set(CustomLog.getCurrentContext());
return CompletableFuture.completedFuture(callable.call());
});
// ACT
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
testLogContext, InvoiceFormat.UBL).join();
// ASSERT
assertThat(capturedContext.get()).isNotNull();
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
}
}
}
```
## Resource Layer Tests (REST Assured)
## 资源层测试(REST Assured
```java
@QuarkusTest
@@ -608,27 +214,6 @@ class DocumentResourceTest {
@InjectMock
DocumentService documentService;
@Nested
@DisplayName("Tests for GET /api/documents")
class ListDocuments {
@Test
@DisplayName("Should return list of documents")
void givenDocumentsExist_whenList_thenReturnsOk() {
// ARRANGE
List<Document> documents = List.of(createDocument(1L, "DOC-001"));
when(documentService.list(0, 20)).thenReturn(documents);
// ACT & ASSERT
given()
.when().get("/api/documents")
.then()
.statusCode(200)
.body("$.size()", is(1))
.body("[0].referenceNumber", equalTo("DOC-001"));
}
}
@Nested
@DisplayName("Tests for POST /api/documents")
class CreateDocument {
@@ -654,14 +239,12 @@ class DocumentResourceTest {
.when().post("/api/documents")
.then()
.statusCode(201)
.header("Location", containsString("/api/documents/1"))
.body("referenceNumber", equalTo("DOC-001"));
}
@Test
@DisplayName("Should return 400 for invalid input")
void givenInvalidRequest_whenCreate_thenReturns400() {
// ACT & ASSERT
given()
.contentType(ContentType.JSON)
.body("""
@@ -675,58 +258,12 @@ class DocumentResourceTest {
.statusCode(400);
}
}
private Document createDocument(Long id, String referenceNumber) {
Document document = new Document();
document.setId(id);
document.setReferenceNumber(referenceNumber);
document.setStatus(DocumentStatus.PENDING);
return document;
}
}
```
## Integration Tests with Real Database
## JaCoCo覆盖率
```java
@QuarkusTest
@TestProfile(IntegrationTestProfile.class)
@DisplayName("Document Integration Tests")
class DocumentIntegrationTest {
@Test
@Transactional
@DisplayName("Should create and retrieve document via API")
void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {
// ACT - Create via API
Long id = given()
.contentType(ContentType.JSON)
.body("""
{
"referenceNumber": "INT-001",
"description": "Integration test",
"validUntil": "2030-01-01T00:00:00Z",
"categories": ["test"]
}
""")
.when().post("/api/documents")
.then()
.statusCode(201)
.extract().path("id");
// ASSERT - Retrieve via API
given()
.when().get("/api/documents/" + id)
.then()
.statusCode(200)
.body("referenceNumber", equalTo("INT-001"));
}
}
```
## Coverage with JaCoCo
### Maven Configuration (Complete)
### Maven配置
```xml
<plugin>
@@ -734,29 +271,18 @@ class DocumentIntegrationTest {
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
<executions>
<!-- Prepare agent for test execution -->
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<goals><goal>prepare-agent</goal></goals>
</execution>
<!-- Generate coverage report -->
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
<goals><goal>report</goal></goals>
</execution>
<!-- Enforce coverage thresholds -->
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
@@ -767,11 +293,6 @@ class DocumentIntegrationTest {
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
@@ -781,20 +302,19 @@ class DocumentIntegrationTest {
</plugin>
```
Run tests with coverage:
运行带覆盖率的测试:
```bash
mvn clean test
mvn jacoco:report
mvn jacoco:check
# Report at: target/site/jacoco/index.html
# 报告位于: target/site/jacoco/index.html
```
## Test Dependencies
## 测试依赖
```xml
<dependencies>
<!-- Quarkus Testing -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
@@ -805,30 +325,17 @@ mvn jacoco:check
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- AssertJ (preferred over JUnit assertions) -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
<!-- REST Assured -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<!-- Camel Testing -->
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-junit5</artifactId>
@@ -837,74 +344,32 @@ mvn jacoco:check
</dependencies>
```
## Best Practices
## 最佳实践
### Test Organization
- Use `@Nested` classes to group tests by method being tested
- Use `@DisplayName` for readable test descriptions visible in reports
- Follow `givenX_whenY_thenZ` naming convention for test methods
- Use `@BeforeEach` for common test data setup to reduce duplication
### 测试组织
- 使用`@Nested`类按被测方法分组
- 使用`@DisplayName`提供可读的测试描述
- 遵循`givenX_whenY_thenZ`命名约定
### Test Structure
- Follow AAA pattern with explicit comments (`// ARRANGE`, `// ACT`, `// ASSERT`)
- Use `assertDoesNotThrow` for success scenarios
- Use `assertThrows` for exception scenarios with message validation
- Verify exception messages match expected values using AssertJ `contains()` or `isEqualTo()`
### 测试结构
- 遵循带明确注释的AAA模式(`// ARRANGE``// ACT``// ASSERT`
- 成功场景使用`assertDoesNotThrow`
- 异常场景使用`assertThrows`并验证消息
### Test Coverage
- Test happy paths for all public methods
- Test null input handling
- Test edge cases (empty collections, boundary values, negative IDs, blank strings)
- Test exception scenarios comprehensively
- Mock all external dependencies (repositories, services, Camel endpoints)
- Aim for 80%+ line coverage, 70%+ branch coverage
### 断言
- **始终使用AssertJ**`assertThat`)代替JUnit断言
- 使用流式AssertJ API提高可读性
- 异常断言: `assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessageContaining(...)`
### Assertions
- **Always use AssertJ** (`assertThat`) instead of JUnit assertions
- Use fluent AssertJ API for readability: `assertThat(list).hasSize(3).contains(item)`
- For exceptions: `assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessageContaining(...)`
- For collections: `extracting()`, `filteredOn()`, `containsExactly()`
### 事件驱动测试
- 使用`AdviceWith``MockEndpoint`测试Camel路由
- 验证消息内容、头部和路由逻辑
- 单独测试错误处理路由
- 单元测试中模拟外部系统(RabbitMQ、S3、数据库)
### Testing Integration
- Use `@QuarkusTest` for integration tests
- Use `@InjectMock` to mock dependencies in Quarkus tests
- Prefer REST Assured for API testing
- Use `@TestProfile` for test-specific configuration
### Quarkus特定
- 保持最新的LTS版本(Quarkus 3.x
- 使用Quarkus测试配置文件处理不同场景
- 使用`@InjectMock`代替`@MockBean`Quarkus特定)
### Event-Driven Testing
- Test Camel routes with `AdviceWith` and `MockEndpoint`
- Use `@CamelQuarkusTest` annotation (if using standalone Camel tests)
- Verify message content, headers, and routing logic
- Test error handling routes separately
- Mock external systems (RabbitMQ, S3, databases) in unit tests
### Camel Route Testing
- Use `MockEndpoint` for asserting message flow
- Use `AdviceWith` to modify routes for testing (replace endpoints with mocks)
- Test message transformation and marshalling
- Test exception handling and dead letter queues
### Testing Async Operations
- Test CompletableFuture success and failure scenarios
- Use `.join()` in tests to wait for async completion
- Test exception propagation from CompletableFuture
- Verify LogContext propagation to async operations
### Performance
- Keep tests fast and isolated
- Run tests in continuous mode: `mvn quarkus:test`
- Use parameterized tests (`@ParameterizedTest`) for input variations
- Build reusable test data builders or factory methods
### Quarkus-Specific
- Stay on latest LTS version (Quarkus 3.x)
- Test native compilation compatibility periodically
- Use Quarkus test profiles for different scenarios
- Leverage Quarkus dev services for local testing
- Use `@InjectMock` instead of `@MockBean` (Quarkus-specific)
### Verification Best Practices
- Always verify interactions on mocked dependencies
- Use `verify(mock, never())` to ensure methods are NOT called in error scenarios
- Use `argThat()` for complex argument matching
- Verify the order of calls when it matters: `InOrder` from Mockito
**请记住**: 保持测试快速、隔离和确定性。测试行为而非实现细节。
+117 -286
View File
@@ -1,25 +1,23 @@
---
name: quarkus-verification
description: "Verification loop for Quarkus projects: build, static analysis, tests with coverage, security scans, native compilation, and diff review before release or PR."
description: "Quarkus项目验证循环:构建、静态分析、带覆盖率的测试、安全扫描、原生编译以及发布或PR前的diff审查。"
origin: ECC
---
> **Note / 注意**: 本文件尚未翻译为中文,目前为英文原版。欢迎提交翻译 PR。
# Quarkus 验证循环
# Quarkus Verification Loop
在PR前、重大变更后和部署前运行。
Run before PRs, after major changes, and pre-deploy.
## 何时激活
## When to Activate
- 为Quarkus服务打开PR前
- 大规模重构或依赖升级后
- 预发布或生产的部署前验证
- 运行完整的构建 → lint → 测试 → 安全扫描 → 原生编译流水线
- 验证测试覆盖率达到阈值(80%+
- 测试原生镜像兼容性
- Before opening a pull request for a Quarkus service
- After major refactoring or dependency upgrades
- Pre-deployment verification for staging or production
- Running full build → lint → test → security scan → native compilation pipeline
- Validating test coverage meets thresholds (80%+)
- Testing native image compatibility
## Phase 1: Build
## 阶段1: 构建
```bash
# Maven
@@ -29,17 +27,17 @@ mvn clean verify -DskipTests
./gradlew clean assemble -x test
```
If build fails, stop and fix compilation errors.
构建失败时,停止并修复编译错误。
## Phase 2: Static Analysis
## 阶段2: 静态分析
### Checkstyle, PMD, SpotBugs (Maven)
### CheckstylePMDSpotBugsMaven
```bash
mvn checkstyle:check pmd:check spotbugs:check
```
### SonarQube (if configured)
### SonarQube(如已配置)
```bash
mvn sonar:sonar \
@@ -48,33 +46,33 @@ mvn sonar:sonar \
-Dsonar.login=${SONAR_TOKEN}
```
### Common Issues to Address
### 需要解决的常见问题
- Unused imports or variables
- Complex methods (high cyclomatic complexity)
- Potential null pointer dereferences
- Security issues flagged by SpotBugs
- 未使用的导入或变量
- 复杂方法(高圈复杂度)
- 潜在的空指针解引用
- SpotBugs标记的安全问题
## Phase 3: Tests + Coverage
## 阶段3: 测试 + 覆盖率
```bash
# Run all tests
# 运行所有测试
mvn clean test
# Generate coverage report
# 生成覆盖率报告
mvn jacoco:report
# Enforce coverage threshold (80%)
# 强制覆盖率阈值(80%
mvn jacoco:check
# Or with Gradle
# 或使用Gradle
./gradlew test jacocoTestReport jacocoTestCoverageVerification
```
### Test Categories
### 测试类别
#### Unit Tests
Test service logic with mocked dependencies:
#### 单元测试
使用模拟依赖测试服务逻辑:
```java
@ExtendWith(MockitoExtension.class)
@@ -99,8 +97,8 @@ class UserServiceTest {
}
```
#### Integration Tests
Test with real database (Testcontainers):
#### 集成测试
使用真实数据库(Testcontainers)测试:
```java
@QuarkusTest
@@ -126,8 +124,8 @@ class UserRepositoryIntegrationTest {
}
```
#### API Tests
Test REST endpoints with REST Assured:
#### API测试
使用REST Assured测试REST端点:
```java
@QuarkusTest
@@ -160,324 +158,157 @@ class UserResourceTest {
}
```
### Coverage Report
### 覆盖率报告
Check `target/site/jacoco/index.html` for detailed coverage:
- Overall line coverage (target: 80%+)
- Branch coverage (target: 70%+)
- Identify uncovered critical paths
检查`target/site/jacoco/index.html`获取详细覆盖率:
- 总体行覆盖率(目标: 80%+
- 分支覆盖率(目标: 70%+
- 识别未覆盖的关键路径
## Phase 4: Security Scanning
## 阶段4: 安全扫描
### Dependency Vulnerabilities (Maven)
### 依赖漏洞(Maven
```bash
mvn org.owasp:dependency-check-maven:check
```
Review `target/dependency-check-report.html` for CVEs.
查看`target/dependency-check-report.html`中的CVE
### Quarkus Security Audit
### Quarkus安全审计
```bash
# Check vulnerable extensions
# 检查有漏洞的扩展
mvn quarkus:audit
# List all extensions
# 列出所有扩展
mvn quarkus:list-extensions
```
### OWASP ZAP (API Security Testing)
### 常见安全检查
- [ ] 所有密钥在环境变量中(不在代码中)
- [ ] 所有端点有输入验证
- [ ] 认证/授权已配置
- [ ] CORS正确配置
- [ ] 安全头已设置
- [ ] 密码使用BCrypt哈希
- [ ] SQL注入保护(参数化查询)
- [ ] 公共端点有速率限制
## 阶段5: 原生编译
测试GraalVM原生镜像兼容性:
```bash
docker run -t owasp/zap2docker-stable zap-api-scan.py \
-t http://localhost:8080/q/openapi \
-f openapi
```
### Common Security Checks
- [ ] All secrets in environment variables (not in code)
- [ ] Input validation on all endpoints
- [ ] Authentication/authorization configured
- [ ] CORS properly configured
- [ ] Security headers set
- [ ] Passwords hashed with BCrypt
- [ ] SQL injection protection (parameterized queries)
- [ ] Rate limiting on public endpoints
## Phase 5: Native Compilation
Test GraalVM native image compatibility:
```bash
# Build native executable
# 构建原生可执行文件
mvn package -Dnative
# Or with container
# 或使用容器
mvn package -Dnative -Dquarkus.native.container-build=true
# Test native executable
# 测试原生可执行文件
./target/*-runner
# Run basic smoke tests
# 运行基本冒烟测试
curl http://localhost:8080/q/health/live
curl http://localhost:8080/q/health/ready
```
### Native Image Troubleshooting
### 原生镜像故障排除
Common issues:
- **Reflection**: Add reflection config for dynamic classes
- **Resources**: Include resources with `quarkus.native.resources.includes`
- **JNI**: Register JNI classes if using native libraries
常见问题:
- **Reflection**: 为动态类添加反射配置
- **Resources**: 使用`quarkus.native.resources.includes`包含资源
- **JNI**: 使用原生库时注册JNI类
Example reflection config:
反射配置示例:
```java
@RegisterForReflection(targets = {MyDynamicClass.class})
public class ReflectionConfiguration {}
```
## Phase 6: Performance Testing
### Load Testing with K6
```javascript
// load-test.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '1m', target: 100 },
{ duration: '30s', target: 0 },
],
};
export default function () {
const res = http.get('http://localhost:8080/api/markets');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
}
```
Run:
```bash
k6 run load-test.js
```
### Metrics to Monitor
- Response time (p50, p95, p99)
- Throughput (requests/sec)
- Error rate
- Memory usage
- CPU usage
## Phase 7: Health Checks
## 阶段6: 健康检查
```bash
# Liveness
# 存活检查
curl http://localhost:8080/q/health/live
# Readiness
# 就绪检查
curl http://localhost:8080/q/health/ready
# All health checks
# 所有健康检查
curl http://localhost:8080/q/health
# Metrics (if enabled)
# 指标(如启用)
curl http://localhost:8080/q/metrics
```
Expected responses:
```json
{
"status": "UP",
"checks": [
{
"name": "Database connection",
"status": "UP"
}
]
}
```
## 验证清单
## Phase 8: Container Image Build
### 代码质量
- [ ] 构建无警告通过
- [ ] 静态分析干净(无高/中问题)
- [ ] 代码遵循团队规范
- [ ] PR中无注释代码或TODO
```bash
# Build container image
mvn package -Dquarkus.container-image.build=true
### 测试
- [ ] 所有测试通过
- [ ] 代码覆盖率 ≥ 80%
- [ ] 使用真实数据库的集成测试
- [ ] 安全测试通过
- [ ] 性能在可接受范围内
# Or with specific registry
mvn package \
-Dquarkus.container-image.build=true \
-Dquarkus.container-image.registry=docker.io \
-Dquarkus.container-image.group=myorg \
-Dquarkus.container-image.tag=1.0.0
### 安全
- [ ] 无依赖漏洞
- [ ] 认证/授权已测试
- [ ] 输入验证完成
- [ ] 源代码中无密钥
- [ ] 安全头已配置
# Test container
docker run -p 8080:8080 myorg/my-quarkus-app:1.0.0
```
### 部署
- [ ] 原生编译成功
- [ ] 容器镜像可构建
- [ ] 健康检查正确响应
- [ ] 目标环境配置有效
### Container Security Scan
```bash
# Trivy
trivy image myorg/my-quarkus-app:1.0.0
# Grype
grype myorg/my-quarkus-app:1.0.0
```
## Phase 9: Configuration Validation
```bash
# Check all configuration properties
mvn quarkus:info
# List all config sources
curl http://localhost:8080/q/dev/io.quarkus.quarkus-vertx-http/config
```
### Environment-Specific Checks
- [ ] Database URLs configured per environment
- [ ] Secrets externalized (Vault, env vars)
- [ ] Logging levels appropriate
- [ ] CORS origins set correctly
- [ ] Rate limiting configured
- [ ] Monitoring/tracing enabled
## Phase 10: Documentation Review
- [ ] OpenAPI/Swagger docs up to date (`/q/swagger-ui`)
- [ ] README has setup instructions
- [ ] API changes documented
- [ ] Migration guide for breaking changes
- [ ] Configuration properties documented
Generate OpenAPI spec:
```bash
curl http://localhost:8080/q/openapi -o openapi.json
```
## Verification Checklist
### Code Quality
- [ ] Build passes without warnings
- [ ] Static analysis clean (no high/medium issues)
- [ ] Code follows team conventions
- [ ] No commented-out code or TODOs in PR
### Testing
- [ ] All tests pass
- [ ] Code coverage ≥ 80%
- [ ] Integration tests with real database
- [ ] Security tests pass
- [ ] Performance within acceptable limits
### Security
- [ ] No dependency vulnerabilities
- [ ] Authentication/authorization tested
- [ ] Input validation complete
- [ ] Secrets not in source code
- [ ] Security headers configured
### Deployment
- [ ] Native compilation successful
- [ ] Container image builds
- [ ] Health checks respond correctly
- [ ] Configuration valid for target environment
### Native Image
- [ ] Native executable builds
- [ ] Native tests pass
- [ ] Startup time < 100ms
- [ ] Memory footprint acceptable
## Automated Verification Script
## 自动化验证脚本
```bash
#!/bin/bash
set -e
echo "=== Phase 1: Build ==="
echo "=== 阶段1: 构建 ==="
mvn clean verify -DskipTests
echo "=== Phase 2: Static Analysis ==="
echo "=== 阶段2: 静态分析 ==="
mvn checkstyle:check pmd:check spotbugs:check
echo "=== Phase 3: Tests + Coverage ==="
echo "=== 阶段3: 测试 + 覆盖率 ==="
mvn test jacoco:report jacoco:check
echo "=== Phase 4: Security Scan ==="
echo "=== 阶段4: 安全扫描 ==="
mvn org.owasp:dependency-check-maven:check
echo "=== Phase 5: Native Compilation ==="
echo "=== 阶段5: 原生编译 ==="
mvn package -Dnative -Dquarkus.native.container-build=true
echo "=== All Phases Complete ==="
echo "Review reports:"
echo " - Coverage: target/site/jacoco/index.html"
echo " - Security: target/dependency-check-report.html"
echo " - Native: target/*-runner"
echo "=== 所有阶段完成 ==="
echo "查看报告:"
echo " - 覆盖率: target/site/jacoco/index.html"
echo " - 安全: target/dependency-check-report.html"
echo " - 原生: target/*-runner"
```
## CI/CD Integration
## 最佳实践
### GitHub Actions Example
```yaml
name: Verification
on: [push, pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Cache Maven packages
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- name: Build
run: mvn clean verify -DskipTests
- name: Test with Coverage
run: mvn test jacoco:report jacoco:check
- name: Security Scan
run: mvn org.owasp:dependency-check-maven:check
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
files: target/site/jacoco/jacoco.xml
```
## Best Practices
- Run verification loop before every PR
- Automate in CI/CD pipeline
- Fix issues immediately; don't accumulate debt
- Keep coverage above 80%
- Update dependencies regularly
- Test native compilation periodically
- Monitor performance trends
- Document breaking changes
- Review security scan results
- Validate configuration for each environment
- 每次PR前运行验证循环
- 在CI/CD流水线中自动化
- 立即修复问题,不积累技术债务
- 保持覆盖率在80%以上
- 定期更新依赖
- 定期测试原生编译
- 监控性能趋势
- 记录破坏性变更
- 审查安全扫描结果
- 验证每个环境的配置