## 🏗️ Architecture & Design - Add ADR collection (15 Architecture Decision Records) - Comprehensive system design documentation - Implementation roadmap and project planning ## 📋 Requirements & Documentation - Detailed functional requirements - API specifications (OpenAPI & AsyncAPI) - Postman collections and optimization reports ## 🗄️ Database & Configuration - Database migration scripts (CDC, core schema) - Resilience4j configuration for resilience patterns - Alert manager and Prometheus monitoring rules ## 🎯 Project Templates - Admin frontend template (Arco Pro + Vue 3) - Big screen data visualization template (ECharts) - Enterprise backend template (Jeecg Boot + Spring Boot) ## 🧪 Testing & Quality - K6 performance testing scenarios - Load testing benchmark scripts - Quality assurance framework setup ## 📦 Infrastructure & Deployment - Complete infrastructure configurations - Deployment strategies and CI/CD pipelines - Monitoring and observability setup This commit establishes the complete foundation for the MVP Sprint 01 development cycle with all necessary architectural decisions, templates, and documentation ready for team collaboration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
测试策略与实施方案
概述
基于项目需求和系统架构,制定全面的测试策略,包括单元测试、集成测试、端到端测试、性能测试和安全测试,确保系统质量和稳定性。
1. 测试策略总览
1.1 测试金字塔
graph TB
subgraph "测试金字塔"
E2E[端到端测试<br/>E2E Tests<br/>10%]
Integration[集成测试<br/>Integration Tests<br/>20%]
Unit[单元测试<br/>Unit Tests<br/>70%]
end
subgraph "测试类型"
Manual[手工测试]
Auto[自动化测试]
Performance[性能测试]
Security[安全测试]
Accessibility[可访问性测试]
end
E2E --> Manual
E2E --> Auto
Integration --> Auto
Integration --> Performance
Unit --> Auto
Security --> Auto
Security --> Manual
1.2 测试覆盖目标
| 测试类型 | 覆盖目标 | 工具选择 | 执行频率 |
|---|---|---|---|
| 单元测试 | 代码覆盖率 > 80% | JUnit 5, Vitest | 每次提交 |
| 集成测试 | API覆盖率 > 90% | TestContainers, Supertest | 每次提交 |
| 端到端测试 | 关键流程 100% | Playwright, Cypress | 每日构建 |
| 性能测试 | 响应时间 < 200ms | K6, JMeter | 每周执行 |
| 安全测试 | 0个高危漏洞 | OWASP ZAP, SonarQube | 每次发布 |
1.3 测试环境规划
# 测试环境配置
environments:
unit:
description: "单元测试环境"
isolation: "内存数据库"
database: "H2/TestContainers"
external_services: "Mock"
integration:
description: "集成测试环境"
isolation: "容器化"
database: "PostgreSQL容器"
external_services: "WireMock"
e2e:
description: "端到端测试环境"
isolation: "独立集群"
database: "PostgreSQL"
external_services: "测试桩服务"
performance:
description: "性能测试环境"
isolation: "生产级配置"
database: "PostgreSQL集群"
external_services: "实际服务"
staging:
description: "预发布环境"
isolation: "生产镜像"
database: "生产数据副本"
external_services: "生产服务"
2. 单元测试实施
2.1 Java 单元测试
2.1.1 测试框架配置
<!-- pom.xml - 测试依赖 -->
<dependencies>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- AssertJ -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Testcontainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.1.2 Service 层测试
// UserServiceTest.java
@ExtendWith(MockitoExtension.class)
@DisplayName("用户服务测试")
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private UserEventPublisher eventPublisher;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@InjectMocks
private UserServiceImpl userService;
private TestDataBuilder testDataBuilder;
@BeforeEach
void setUp() {
testDataBuilder = new TestDataBuilder();
}
@Nested
@DisplayName("创建用户")
class CreateUserTests {
@Test
@DisplayName("成功创建用户")
void shouldCreateUserSuccessfully() {
// Given
CreateUserRequest request = testDataBuilder.buildCreateUserRequest();
User expectedUser = testDataBuilder.buildUser();
when(userRepository.existsByUsername(request.getUsername())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(expectedUser);
// When
UserResponse response = userService.createUser(request);
// Then
assertThat(response).isNotNull();
assertThat(response.getId()).isEqualTo(expectedUser.getId());
assertThat(response.getUsername()).isEqualTo(expectedUser.getUsername());
assertThat(response.getStatus()).isEqualTo(UserStatus.PENDING);
// 验证方法调用
verify(userRepository).existsByUsername(request.getUsername());
verify(userRepository).save(argThat(user ->
user.getUsername().equals(request.getUsername()) &&
user.getEmail().equals(request.getEmail()) &&
user.getStatus() == UserStatus.PENDING
));
verify(eventPublisher).publish(argThat(event ->
event instanceof UserRegisteredEvent &&
((UserRegisteredEvent) event).getUserId().equals(expectedUser.getId())
));
}
@Test
@DisplayName("用户名已存在时抛出异常")
void shouldThrowExceptionWhenUsernameExists() {
// Given
CreateUserRequest request = testDataBuilder.buildCreateUserRequest();
when(userRepository.existsByUsername(request.getUsername())).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(UserExistsException.class)
.hasMessageContaining("用户名已存在");
verify(userRepository).existsByUsername(request.getUsername());
verify(userRepository, never()).save(any());
verify(eventPublisher, never()).publish(any());
}
@ParameterizedTest
@DisplayName("无效输入参数测试")
@MethodSource("invalidCreateUserRequests")
void shouldThrowExceptionForInvalidInput(CreateUserRequest request, String expectedMessage) {
// When & Then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(ValidationException.class)
.hasMessageContaining(expectedMessage);
}
static Stream<Arguments> invalidCreateUserRequests() {
return Stream.of(
Arguments.of(null, "创建用户请求不能为空"),
Arguments.of(CreateUserRequest.builder().build(), "用户名不能为空"),
Arguments.of(CreateUserRequest.builder().username("test").build(), "邮箱不能为空")
);
}
}
@Nested
@DisplayName("查询用户")
class FindUserTests {
@Test
@DisplayName("根据ID查询用户")
void shouldFindUserById() {
// Given
String userId = "USR_01HMXVKJ8RG9CQXY85ZZQK2N3T";
User expectedUser = testDataBuilder.buildUser(userId);
when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser));
// When
Optional<UserResponse> result = userService.findById(userId);
// Then
assertThat(result).isPresent();
assertThat(result.get().getId()).isEqualTo(userId);
verify(userRepository).findById(userId);
}
@Test
@DisplayName("用户不存在时返回空")
void shouldReturnEmptyWhenUserNotExists() {
// Given
String userId = "USR_NOT_EXISTS";
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// When
Optional<UserResponse> result = userService.findById(userId);
// Then
assertThat(result).isEmpty();
verify(userRepository).findById(userId);
}
}
}
// 测试数据构建器
public class TestDataBuilder {
public CreateUserRequest buildCreateUserRequest() {
return CreateUserRequest.builder()
.username("testuser")
.email("[email protected]")
.phone("13800138000")
.userType(UserType.INDIVIDUAL)
.build();
}
public User buildUser() {
return buildUser("USR_01HMXVKJ8RG9CQXY85ZZQK2N3T");
}
public User buildUser(String id) {
return User.builder()
.id(id)
.username("testuser")
.email("[email protected]")
.phone("13800138000")
.userType(UserType.INDIVIDUAL)
.status(UserStatus.PENDING)
.tenantId("TENANT_01HMXVKJ8RG9CQXY85ZZQK2N3P")
.createdTime(LocalDateTime.now())
.build();
}
}
2.1.3 Repository 层测试
// UserRepositoryTest.java
@DataJpaTest
@Testcontainers
@DisplayName("用户仓储测试")
class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("gmkx_test")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
private TestDataBuilder testDataBuilder;
@BeforeEach
void setUp() {
testDataBuilder = new TestDataBuilder();
}
@Test
@DisplayName("根据用户名查询用户")
void shouldFindUserByUsername() {
// Given
User user = testDataBuilder.buildUser();
entityManager.persistAndFlush(user);
// When
Optional<User> found = userRepository.findByUsername(user.getUsername());
// Then
assertThat(found).isPresent();
assertThat(found.get().getUsername()).isEqualTo(user.getUsername());
assertThat(found.get().getEmail()).isEqualTo(user.getEmail());
}
@Test
@DisplayName("检查用户名是否存在")
void shouldCheckUsernameExists() {
// Given
User user = testDataBuilder.buildUser();
entityManager.persistAndFlush(user);
// When & Then
assertThat(userRepository.existsByUsername(user.getUsername())).isTrue();
assertThat(userRepository.existsByUsername("nonexistent")).isFalse();
}
@Test
@DisplayName("按租户和状态查询用户")
void shouldFindUsersByTenantAndStatus() {
// Given
String tenantId = "TENANT_01HMXVKJ8RG9CQXY85ZZQK2N3P";
List<User> users = List.of(
testDataBuilder.buildUser().toBuilder().tenantId(tenantId).status(UserStatus.ACTIVE).build(),
testDataBuilder.buildUser().toBuilder().tenantId(tenantId).status(UserStatus.INACTIVE).build(),
testDataBuilder.buildUser().toBuilder().tenantId("OTHER_TENANT").status(UserStatus.ACTIVE).build()
);
users.forEach(user -> entityManager.persistAndFlush(user));
// When
List<User> activeUsers = userRepository.findByTenantIdAndStatus(tenantId, UserStatus.ACTIVE);
// Then
assertThat(activeUsers).hasSize(1);
assertThat(activeUsers.get(0).getTenantId()).isEqualTo(tenantId);
assertThat(activeUsers.get(0).getStatus()).isEqualTo(UserStatus.ACTIVE);
}
@Test
@DisplayName("分页查询用户")
void shouldFindUsersWithPagination() {
// Given
List<User> users = IntStream.range(0, 15)
.mapToObj(i -> testDataBuilder.buildUser())
.toList();
users.forEach(user -> entityManager.persistAndFlush(user));
// When
Pageable pageable = PageRequest.of(0, 10, Sort.by("createdTime").descending());
Page<User> page = userRepository.findAll(pageable);
// Then
assertThat(page.getContent()).hasSize(10);
assertThat(page.getTotalElements()).isEqualTo(15);
assertThat(page.getTotalPages()).isEqualTo(2);
assertThat(page.isFirst()).isTrue();
assertThat(page.hasNext()).isTrue();
}
}
2.2 前端单元测试
2.2.1 Vue 组件测试
// AchievementCard.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import AchievementCard from '@/components/business/AchievementCard.vue'
import type { Achievement } from '@/api/types'
const mockAchievement: Achievement = {
id: 'ACH_01HMXVKJ8RG9CQXY85ZZQK2N3T',
title: '人工智能图像识别技术',
summary: '基于深度学习的图像识别技术,准确率达到95%以上',
description: '详细描述...',
techField: '人工智能',
achievementType: 'PATENT',
maturityLevel: 'PRODUCT',
ownerId: 'USR_01HMXVKJ8RG9CQXY85ZZQK2N3P',
status: 'PUBLISHED',
viewCount: 128,
favoriteCount: 15,
createdTime: '2024-01-15T10:30:00Z'
}
describe('AchievementCard', () => {
it('should render achievement information correctly', () => {
const wrapper = mount(AchievementCard, {
props: {
achievement: mockAchievement
}
})
expect(wrapper.find('.achievement-card__title').text()).toBe(mockAchievement.title)
expect(wrapper.find('.achievement-card__summary').text()).toBe(mockAchievement.summary)
expect(wrapper.find('[data-testid="view-count"]').text()).toBe('128')
expect(wrapper.find('[data-testid="favorite-count"]').text()).toBe('15')
})
it('should emit viewDetail event when view button clicked', async () => {
const wrapper = mount(AchievementCard, {
props: {
achievement: mockAchievement
}
})
await wrapper.find('[data-testid="view-detail-btn"]').trigger('click')
expect(wrapper.emitted('viewDetail')).toBeTruthy()
expect(wrapper.emitted('viewDetail')?.[0]).toEqual([mockAchievement.id])
})
it('should emit addToFavorite event when favorite button clicked', async () => {
const wrapper = mount(AchievementCard, {
props: {
achievement: mockAchievement
}
})
await wrapper.find('[data-testid="favorite-btn"]').trigger('click')
expect(wrapper.emitted('addToFavorite')).toBeTruthy()
expect(wrapper.emitted('addToFavorite')?.[0]).toEqual([mockAchievement.id])
})
it('should truncate long summary', () => {
const longSummary = 'a'.repeat(150)
const achievementWithLongSummary = {
...mockAchievement,
summary: longSummary
}
const wrapper = mount(AchievementCard, {
props: {
achievement: achievementWithLongSummary
}
})
const summaryText = wrapper.find('.achievement-card__summary').text()
expect(summaryText).toHaveLength(103) // 100 + '...'
expect(summaryText).toEndWith('...')
})
it('should hide actions when showActions is false', () => {
const wrapper = mount(AchievementCard, {
props: {
achievement: mockAchievement,
showActions: false
}
})
expect(wrapper.find('.achievement-card__actions').exists()).toBe(false)
})
})
2.2.2 Composables 测试
// useTable.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { nextTick } from 'vue'
import { useTable } from '@/composables/useTable'
import type { Achievement } from '@/api/types'
const mockFetchData = vi.fn()
const mockAchievements: Achievement[] = [
{
id: 'ACH_1',
title: '成果1',
summary: '摘要1',
// ... 其他字段
},
{
id: 'ACH_2',
title: '成果2',
summary: '摘要2',
// ... 其他字段
}
]
describe('useTable', () => {
beforeEach(() => {
mockFetchData.mockClear()
})
it('should initialize with default values', () => {
const { loading, data, total, pagination } = useTable({
columns: [],
fetchData: mockFetchData
})
expect(loading.value).toBe(false)
expect(data.value).toEqual([])
expect(total.value).toBe(0)
expect(pagination.current).toBe(1)
expect(pagination.pageSize).toBe(20)
})
it('should fetch data successfully', async () => {
mockFetchData.mockResolvedValue({
data: mockAchievements,
total: 2
})
const { fetchData, loading, data, total } = useTable({
columns: [],
fetchData: mockFetchData
})
expect(loading.value).toBe(false)
await fetchData()
expect(mockFetchData).toHaveBeenCalledWith({
page: 1,
size: 20,
sort: undefined
})
expect(data.value).toEqual(mockAchievements)
expect(total.value).toBe(2)
expect(loading.value).toBe(false)
})
it('should handle fetch error', async () => {
mockFetchData.mockRejectedValue(new Error('网络错误'))
const { fetchData, loading, error } = useTable({
columns: [],
fetchData: mockFetchData
})
await fetchData()
expect(loading.value).toBe(false)
expect(error.value).toBe('网络错误')
})
it('should handle pagination', async () => {
mockFetchData.mockResolvedValue({
data: mockAchievements,
total: 50
})
const { handlePageChange, pagination, fetchData } = useTable({
columns: [],
fetchData: mockFetchData
})
await handlePageChange(3)
expect(pagination.current).toBe(3)
expect(mockFetchData).toHaveBeenCalledWith({
page: 3,
size: 20,
sort: undefined
})
})
it('should handle filters', async () => {
mockFetchData.mockResolvedValue({
data: [],
total: 0
})
const { handleFilter } = useTable({
columns: [],
fetchData: mockFetchData
})
await handleFilter({ techField: '人工智能', status: 'PUBLISHED' })
expect(mockFetchData).toHaveBeenCalledWith({
page: 1,
size: 20,
techField: '人工智能',
status: 'PUBLISHED',
sort: undefined
})
})
})
2.2.3 API 接口测试
// api/achievement.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { AchievementApi } from '@/api/modules/achievement'
import type { Achievement } from '@/api/types'
const mockAchievement: Achievement = {
id: 'ACH_01HMXVKJ8RG9CQXY85ZZQK2N3T',
title: '测试成果',
summary: '测试摘要',
// ... 其他字段
}
// Mock服务器
const server = setupServer(
rest.get('/api/v1/achievements', (req, res, ctx) => {
const page = req.url.searchParams.get('page') || '1'
const size = req.url.searchParams.get('size') || '20'
return res(
ctx.json({
code: 200,
message: 'success',
data: {
data: [mockAchievement],
total: 1,
page: parseInt(page),
size: parseInt(size)
}
})
)
}),
rest.get('/api/v1/achievements/:id', (req, res, ctx) => {
const { id } = req.params
if (id === 'not-found') {
return res(
ctx.status(404),
ctx.json({
code: 404,
message: '成果不存在'
})
)
}
return res(
ctx.json({
code: 200,
message: 'success',
data: mockAchievement
})
)
}),
rest.post('/api/v1/achievements', (req, res, ctx) => {
return res(
ctx.status(201),
ctx.json({
code: 200,
message: '创建成功',
data: mockAchievement
})
)
})
)
beforeEach(() => server.listen())
afterEach(() => server.resetHandlers())
afterEach(() => server.close())
describe('AchievementApi', () => {
it('should get achievement list', async () => {
const result = await AchievementApi.getList({
page: 1,
size: 20
})
expect(result.data).toEqual([mockAchievement])
expect(result.total).toBe(1)
})
it('should get achievement by id', async () => {
const result = await AchievementApi.getById(mockAchievement.id)
expect(result).toEqual(mockAchievement)
})
it('should throw error when achievement not found', async () => {
await expect(AchievementApi.getById('not-found')).rejects.toThrow()
})
it('should create achievement', async () => {
const createRequest = {
title: '新成果',
summary: '新摘要',
techField: '人工智能',
achievementType: 'PATENT' as const
}
const result = await AchievementApi.create(createRequest)
expect(result).toEqual(mockAchievement)
})
})
3. 集成测试实施
3.1 API 集成测试
3.1.1 Spring Boot 集成测试
// AchievementControllerIntegrationTest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@DirtiesContext
@DisplayName("成果控制器集成测试")
class AchievementControllerIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("gmkx_test")
.withUsername("test")
.withPassword("test");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", redis::getFirstMappedPort);
}
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private AchievementRepository achievementRepository;
@Autowired
private UserRepository userRepository;
private String authToken;
private User testUser;
@BeforeEach
void setUp() {
// 创建测试用户
testUser = createTestUser();
// 获取认证令牌
authToken = getAuthToken(testUser);
}
@AfterEach
void tearDown() {
achievementRepository.deleteAll();
userRepository.deleteAll();
}
@Test
@DisplayName("创建成果 - 成功")
void shouldCreateAchievementSuccessfully() {
// Given
CreateAchievementRequest request = CreateAchievementRequest.builder()
.title("AI图像识别技术")
.summary("基于深度学习的图像识别")
.description("详细描述...")
.techField("人工智能")
.achievementType(AchievementType.PATENT)
.maturityLevel(MaturityLevel.PRODUCT)
.build();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(authToken);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<CreateAchievementRequest> entity = new HttpEntity<>(request, headers);
// When
ResponseEntity<ApiResponse<AchievementResponse>> response = restTemplate.exchange(
"/api/v1/achievements",
HttpMethod.POST,
entity,
new ParameterizedTypeReference<ApiResponse<AchievementResponse>>() {}
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().isSuccess()).isTrue();
AchievementResponse achievement = response.getBody().getData();
assertThat(achievement.getId()).isNotNull();
assertThat(achievement.getTitle()).isEqualTo(request.getTitle());
assertThat(achievement.getTechField()).isEqualTo(request.getTechField());
assertThat(achievement.getStatus()).isEqualTo(AchievementStatus.DRAFT);
// 验证数据库
Optional<Achievement> saved = achievementRepository.findById(achievement.getId());
assertThat(saved).isPresent();
assertThat(saved.get().getTitle()).isEqualTo(request.getTitle());
}
@Test
@DisplayName("查询成果列表 - 分页")
void shouldGetAchievementListWithPagination() {
// Given
List<Achievement> achievements = createTestAchievements(25);
achievementRepository.saveAll(achievements);
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(authToken);
HttpEntity<Void> entity = new HttpEntity<>(headers);
// When
ResponseEntity<ApiResponse<PageResult<AchievementResponse>>> response = restTemplate.exchange(
"/api/v1/achievements?page=1&size=10&sort=createdTime,desc",
HttpMethod.GET,
entity,
new ParameterizedTypeReference<ApiResponse<PageResult<AchievementResponse>>>() {}
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
PageResult<AchievementResponse> page = response.getBody().getData();
assertThat(page.getData()).hasSize(10);
assertThat(page.getTotal()).isEqualTo(25);
assertThat(page.getPage()).isEqualTo(1);
assertThat(page.getSize()).isEqualTo(10);
assertThat(page.getTotalPages()).isEqualTo(3);
}
@Test
@DisplayName("搜索成果 - 关键词匹配")
void shouldSearchAchievementsByKeyword() {
// Given
Achievement aiAchievement = Achievement.builder()
.title("人工智能图像识别")
.summary("基于深度学习的AI技术")
.techField("人工智能")
.status(AchievementStatus.PUBLISHED)
.ownerId(testUser.getId())
.tenantId(testUser.getTenantId())
.build();
Achievement bioAchievement = Achievement.builder()
.title("生物医药新技术")
.summary("创新的生物制药方法")
.techField("生物医药")
.status(AchievementStatus.PUBLISHED)
.ownerId(testUser.getId())
.tenantId(testUser.getTenantId())
.build();
achievementRepository.saveAll(List.of(aiAchievement, bioAchievement));
SearchAchievementRequest searchRequest = SearchAchievementRequest.builder()
.keyword("人工智能")
.page(1)
.size(20)
.build();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(authToken);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<SearchAchievementRequest> entity = new HttpEntity<>(searchRequest, headers);
// When
ResponseEntity<ApiResponse<PageResult<AchievementResponse>>> response = restTemplate.exchange(
"/api/v1/achievements/search",
HttpMethod.POST,
entity,
new ParameterizedTypeReference<ApiResponse<PageResult<AchievementResponse>>>() {}
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
PageResult<AchievementResponse> result = response.getBody().getData();
assertThat(result.getData()).hasSize(1);
assertThat(result.getData().get(0).getTitle()).contains("人工智能");
}
@Test
@DisplayName("未授权访问 - 返回401")
void shouldReturn401ForUnauthorizedAccess() {
// Given
CreateAchievementRequest request = CreateAchievementRequest.builder()
.title("未授权测试")
.build();
HttpEntity<CreateAchievementRequest> entity = new HttpEntity<>(request);
// When
ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/achievements",
HttpMethod.POST,
entity,
String.class
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
private User createTestUser() {
User user = User.builder()
.username("testuser")
.email("[email protected]")
.userType(UserType.INDIVIDUAL)
.status(UserStatus.ACTIVE)
.tenantId("TENANT_TEST")
.build();
return userRepository.save(user);
}
private String getAuthToken(User user) {
// 这里实现获取认证令牌的逻辑
// 可以调用认证接口或直接生成测试令牌
return "Bearer test-token-" + user.getId();
}
private List<Achievement> createTestAchievements(int count) {
return IntStream.range(0, count)
.mapToObj(i -> Achievement.builder()
.title("成果" + i)
.summary("摘要" + i)
.techField("技术领域" + (i % 5))
.status(AchievementStatus.PUBLISHED)
.ownerId(testUser.getId())
.tenantId(testUser.getTenantId())
.build())
.collect(Collectors.toList());
}
}
3.1.2 消息集成测试
// AchievementEventIntegrationTest.java
@SpringBootTest
@Testcontainers
@DirtiesContext
@DisplayName("成果事件集成测试")
class AchievementEventIntegrationTest {
@Container
static RabbitMQContainer rabbitmq = new RabbitMQContainer("rabbitmq:3.12-management-alpine")
.withUser("test", "test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.rabbitmq.host", rabbitmq::getHost);
registry.add("spring.rabbitmq.port", rabbitmq::getAmqpPort);
registry.add("spring.rabbitmq.username", () -> "test");
registry.add("spring.rabbitmq.password", () -> "test");
}
@Autowired
private AchievementService achievementService;
@Autowired
private RabbitTemplate rabbitTemplate;
@MockBean
private SearchIndexService searchIndexService;
private TestEventListener testEventListener;
@BeforeEach
void setUp() {
testEventListener = new TestEventListener();
testEventListener.reset();
}
@Test
@DisplayName("发布成果时发送事件")
void shouldSendEventWhenAchievementPublished() throws InterruptedException {
// Given
CreateAchievementRequest request = CreateAchievementRequest.builder()
.title("测试成果")
.summary("测试摘要")
.techField("人工智能")
.achievementType(AchievementType.PATENT)
.build();
AchievementResponse achievement = achievementService.createAchievement(request);
// When
achievementService.publishAchievement(achievement.getId());
// Then
// 等待异步事件处理
Thread.sleep(1000);
// 验证事件被发送
verify(searchIndexService, timeout(2000)).indexAchievement(any(Achievement.class));
// 验证消息队列接收到事件
Object message = rabbitTemplate.receiveAndConvert("achievement.events.published");
assertThat(message).isNotNull();
if (message instanceof AchievementPublishedEvent event) {
assertThat(event.getAchievementId()).isEqualTo(achievement.getId());
assertThat(event.getEventType()).isEqualTo("AchievementPublished");
}
}
@EventListener
public static class TestEventListener {
private final List<Object> receivedEvents = new ArrayList<>();
@EventListener
public void handleAchievementEvent(AchievementPublishedEvent event) {
receivedEvents.add(event);
}
public List<Object> getReceivedEvents() {
return new ArrayList<>(receivedEvents);
}
public void reset() {
receivedEvents.clear();
}
}
}
3.2 数据库集成测试
3.2.1 事务一致性测试
// TransactionIntegrationTest.java
@SpringBootTest
@Testcontainers
@Transactional
@DisplayName("事务一致性集成测试")
class TransactionIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@Autowired
private AchievementService achievementService;
@Autowired
private OutboxEventRepository outboxEventRepository;
@Autowired
private TestEntityManager entityManager;
@Test
@DisplayName("创建成果时同时写入事件表")
void shouldWriteEventWhenCreateAchievement() {
// Given
CreateAchievementRequest request = CreateAchievementRequest.builder()
.title("事务测试成果")
.summary("测试事务一致性")
.techField("测试")
.achievementType(AchievementType.PATENT)
.build();
// When
AchievementResponse achievement = achievementService.createAchievement(request);
entityManager.flush();
// Then
List<OutboxEvent> events = outboxEventRepository.findByAggregateId(achievement.getId());
assertThat(events).hasSize(1);
OutboxEvent event = events.get(0);
assertThat(event.getEventType()).isEqualTo("AchievementCreated");
assertThat(event.getStatus()).isEqualTo(OutboxEventStatus.PENDING);
}
@Test
@DisplayName("业务异常时事务回滚")
void shouldRollbackWhenBusinessException() {
// Given
CreateAchievementRequest request = CreateAchievementRequest.builder()
.title(null) // 触发验证异常
.build();
// When & Then
assertThatThrownBy(() -> achievementService.createAchievement(request))
.isInstanceOf(ValidationException.class);
// 验证数据库没有脏数据
List<Achievement> achievements = achievementRepository.findAll();
assertThat(achievements).isEmpty();
List<OutboxEvent> events = outboxEventRepository.findAll();
assertThat(events).isEmpty();
}
}
4. 端到端测试实施
4.1 Playwright 端到端测试
4.1.1 测试配置
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/results.xml' }]
],
use: {
baseURL: process.env.E2E_BASE_URL || 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI
}
})
4.1.2 页面对象模型
// tests/e2e/pages/achievement-page.ts
import { Page, Locator } from '@playwright/test'
export class AchievementPage {
readonly page: Page
readonly titleInput: Locator
readonly summaryInput: Locator
readonly descriptionInput: Locator
readonly techFieldSelect: Locator
readonly submitButton: Locator
readonly achievementList: Locator
readonly searchInput: Locator
readonly searchButton: Locator
constructor(page: Page) {
this.page = page
this.titleInput = page.locator('[data-testid="achievement-title"]')
this.summaryInput = page.locator('[data-testid="achievement-summary"]')
this.descriptionInput = page.locator('[data-testid="achievement-description"]')
this.techFieldSelect = page.locator('[data-testid="tech-field-select"]')
this.submitButton = page.locator('[data-testid="submit-button"]')
this.achievementList = page.locator('[data-testid="achievement-list"]')
this.searchInput = page.locator('[data-testid="search-input"]')
this.searchButton = page.locator('[data-testid="search-button"]')
}
async goto() {
await this.page.goto('/achievement')
}
async createAchievement(data: {
title: string
summary: string
description: string
techField: string
}) {
await this.page.click('[data-testid="create-achievement-btn"]')
await this.titleInput.fill(data.title)
await this.summaryInput.fill(data.summary)
await this.descriptionInput.fill(data.description)
await this.techFieldSelect.selectOption(data.techField)
await this.submitButton.click()
}
async searchAchievements(keyword: string) {
await this.searchInput.fill(keyword)
await this.searchButton.click()
}
async getAchievementCards() {
return this.achievementList.locator('.achievement-card')
}
async waitForAchievementCreated() {
await this.page.waitForSelector('[data-testid="success-message"]')
}
}
4.1.3 端到端测试用例
// tests/e2e/achievement.spec.ts
import { test, expect } from '@playwright/test'
import { AchievementPage } from './pages/achievement-page'
import { LoginPage } from './pages/login-page'
test.describe('成果管理', () => {
let achievementPage: AchievementPage
let loginPage: LoginPage
test.beforeEach(async ({ page }) => {
achievementPage = new AchievementPage(page)
loginPage = new LoginPage(page)
// 登录
await loginPage.goto()
await loginPage.login('[email protected]', 'password123')
await page.waitForURL('/dashboard')
})
test('创建成果流程', async ({ page }) => {
// 导航到成果页面
await achievementPage.goto()
// 创建成果
await achievementPage.createAchievement({
title: '端到端测试成果',
summary: '这是一个测试成果',
description: '详细描述内容...',
techField: '人工智能'
})
// 验证创建成功
await achievementPage.waitForAchievementCreated()
// 验证成果出现在列表中
const achievementCards = await achievementPage.getAchievementCards()
await expect(achievementCards).toHaveCount(1)
const firstCard = achievementCards.first()
await expect(firstCard.locator('.achievement-card__title')).toHaveText('端到端测试成果')
})
test('搜索成果功能', async ({ page }) => {
// 先创建几个测试成果
await achievementPage.goto()
await achievementPage.createAchievement({
title: '人工智能图像识别',
summary: 'AI技术',
description: '基于深度学习...',
techField: '人工智能'
})
await achievementPage.createAchievement({
title: '生物医药新技术',
summary: '生物技术',
description: '创新的生物制药...',
techField: '生物医药'
})
// 搜索测试
await achievementPage.searchAchievements('人工智能')
// 验证搜索结果
const searchResults = await achievementPage.getAchievementCards()
await expect(searchResults).toHaveCount(1)
const resultCard = searchResults.first()
await expect(resultCard.locator('.achievement-card__title')).toContainText('人工智能')
})
test('成果详情查看', async ({ page }) => {
// 创建成果
await achievementPage.goto()
await achievementPage.createAchievement({
title: '测试成果详情',
summary: '测试摘要',
description: '详细描述内容',
techField: '人工智能'
})
// 点击查看详情
const achievementCards = await achievementPage.getAchievementCards()
const firstCard = achievementCards.first()
await firstCard.locator('[data-testid="view-detail-btn"]').click()
// 验证详情页面
await page.waitForURL(/\/achievement\/[^\/]+$/)
await expect(page.locator('[data-testid="achievement-title"]')).toHaveText('测试成果详情')
await expect(page.locator('[data-testid="achievement-summary"]')).toHaveText('测试摘要')
await expect(page.locator('[data-testid="achievement-description"]')).toContainText('详细描述内容')
})
test('响应式设计测试', async ({ page, isMobile }) => {
await achievementPage.goto()
if (isMobile) {
// 移动端测试
await expect(page.locator('[data-testid="mobile-menu-btn"]')).toBeVisible()
// 点击移动菜单
await page.click('[data-testid="mobile-menu-btn"]')
await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible()
} else {
// 桌面端测试
await expect(page.locator('[data-testid="desktop-sidebar"]')).toBeVisible()
}
})
test('无障碍性测试', async ({ page }) => {
await achievementPage.goto()
// 检查页面标题
await expect(page).toHaveTitle(/成果管理/)
// 检查主要地标
await expect(page.locator('main')).toBeVisible()
await expect(page.locator('[role="navigation"]')).toBeVisible()
// 检查表单标签
await page.click('[data-testid="create-achievement-btn"]')
const titleInput = page.locator('[data-testid="achievement-title"]')
await expect(titleInput).toHaveAttribute('aria-label')
const summaryInput = page.locator('[data-testid="achievement-summary"]')
await expect(summaryInput).toHaveAttribute('aria-label')
})
test('错误处理测试', async ({ page }) => {
await achievementPage.goto()
// 测试表单验证
await page.click('[data-testid="create-achievement-btn"]')
await achievementPage.submitButton.click()
// 验证错误消息
await expect(page.locator('[data-testid="title-error"]')).toBeVisible()
await expect(page.locator('[data-testid="summary-error"]')).toBeVisible()
// 测试网络错误
await page.route('**/api/v1/achievements', route => route.abort())
await achievementPage.createAchievement({
title: '网络错误测试',
summary: '测试摘要',
description: '测试描述',
techField: '人工智能'
})
// 验证错误提示
await expect(page.locator('[data-testid="error-message"]')).toBeVisible()
})
})
4.2 API 端到端测试
4.2.1 REST API 测试
// tests/e2e/api/achievement-api.spec.ts
import { test, expect } from '@playwright/test'
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080'
const TEST_USER_TOKEN = process.env.TEST_USER_TOKEN
test.describe('成果API端到端测试', () => {
let authHeaders: Record<string, string>
test.beforeAll(async () => {
authHeaders = {
'Authorization': `Bearer ${TEST_USER_TOKEN}`,
'Content-Type': 'application/json'
}
})
test('完整的成果生命周期', async ({ request }) => {
// 1. 创建成果
const createResponse = await request.post(`${API_BASE_URL}/api/v1/achievements`, {
headers: authHeaders,
data: {
title: 'API测试成果',
summary: '这是一个API测试成果',
description: '详细描述内容...',
techField: '人工智能',
achievementType: 'PATENT',
maturityLevel: 'PRODUCT'
}
})
expect(createResponse.status()).toBe(200)
const createData = await createResponse.json()
expect(createData.success).toBe(true)
const achievementId = createData.data.id
expect(achievementId).toBeTruthy()
// 2. 获取成果详情
const getResponse = await request.get(
`${API_BASE_URL}/api/v1/achievements/${achievementId}`,
{ headers: authHeaders }
)
expect(getResponse.status()).toBe(200)
const getData = await getResponse.json()
expect(getData.data.title).toBe('API测试成果')
expect(getData.data.status).toBe('DRAFT')
// 3. 更新成果
const updateResponse = await request.put(
`${API_BASE_URL}/api/v1/achievements/${achievementId}`,
{
headers: authHeaders,
data: {
title: 'API测试成果(已更新)',
summary: '更新后的摘要'
}
}
)
expect(updateResponse.status()).toBe(200)
const updateData = await updateResponse.json()
expect(updateData.data.title).toBe('API测试成果(已更新)')
// 4. 发布成果
const publishResponse = await request.post(
`${API_BASE_URL}/api/v1/achievements/${achievementId}/publish`,
{ headers: authHeaders }
)
expect(publishResponse.status()).toBe(200)
const publishData = await publishResponse.json()
expect(publishData.data.status).toBe('PUBLISHED')
// 5. 搜索成果
const searchResponse = await request.post(
`${API_BASE_URL}/api/v1/achievements/search`,
{
headers: authHeaders,
data: {
keyword: 'API测试',
page: 1,
size: 10
}
}
)
expect(searchResponse.status()).toBe(200)
const searchData = await searchResponse.json()
expect(searchData.data.data.length).toBeGreaterThan(0)
const foundAchievement = searchData.data.data.find(
(item: any) => item.id === achievementId
)
expect(foundAchievement).toBeTruthy()
// 6. 删除成果
const deleteResponse = await request.delete(
`${API_BASE_URL}/api/v1/achievements/${achievementId}`,
{ headers: authHeaders }
)
expect(deleteResponse.status()).toBe(200)
// 7. 验证删除
const verifyResponse = await request.get(
`${API_BASE_URL}/api/v1/achievements/${achievementId}`,
{ headers: authHeaders }
)
expect(verifyResponse.status()).toBe(404)
})
test('批量操作测试', async ({ request }) => {
// 批量创建成果
const achievements = []
for (let i = 0; i < 5; i++) {
const response = await request.post(`${API_BASE_URL}/api/v1/achievements`, {
headers: authHeaders,
data: {
title: `批量测试成果${i}`,
summary: `批量测试摘要${i}`,
techField: '人工智能',
achievementType: 'PATENT'
}
})
expect(response.status()).toBe(200)
const data = await response.json()
achievements.push(data.data.id)
}
// 批量发布
const batchPublishResponse = await request.post(
`${API_BASE_URL}/api/v1/achievements/batch/publish`,
{
headers: authHeaders,
data: {
achievementIds: achievements
}
}
)
expect(batchPublishResponse.status()).toBe(200)
// 验证批量发布结果
for (const id of achievements) {
const response = await request.get(
`${API_BASE_URL}/api/v1/achievements/${id}`,
{ headers: authHeaders }
)
const data = await response.json()
expect(data.data.status).toBe('PUBLISHED')
}
})
test('并发访问测试', async ({ request }) => {
// 并发创建成果
const promises = Array.from({ length: 10 }, (_, i) =>
request.post(`${API_BASE_URL}/api/v1/achievements`, {
headers: authHeaders,
data: {
title: `并发测试成果${i}`,
summary: `并发测试摘要${i}`,
techField: '人工智能',
achievementType: 'PATENT'
}
})
)
const responses = await Promise.all(promises)
// 验证所有请求都成功
responses.forEach(response => {
expect(response.status()).toBe(200)
})
// 验证创建的成果数量
const listResponse = await request.get(
`${API_BASE_URL}/api/v1/achievements?page=1&size=50`,
{ headers: authHeaders }
)
const listData = await listResponse.json()
expect(listData.data.total).toBeGreaterThanOrEqual(10)
})
})
5. 性能测试实施
5.1 K6 性能测试
5.1.1 基础性能测试
// tests/performance/basic-load.js
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate, Trend } from 'k6/metrics'
// 自定义指标
const errorRate = new Rate('error_rate')
const responseTime = new Trend('response_time_custom')
export const options = {
// 阶段式负载测试
stages: [
{ duration: '2m', target: 10 }, // 启动阶段
{ duration: '5m', target: 50 }, // 正常负载
{ duration: '2m', target: 100 }, // 高负载
{ duration: '5m', target: 100 }, // 持续高负载
{ duration: '2m', target: 0 }, // 降低负载
],
// 阈值定义
thresholds: {
http_req_duration: ['p(95)<200'], // 95%请求响应时间<200ms
http_req_failed: ['rate<0.01'], // 错误率<1%
error_rate: ['rate<0.05'], // 自定义错误率<5%
response_time_custom: ['p(90)<150'], // 90%自定义响应时间<150ms
},
}
const BASE_URL = 'http://localhost:8080'
const AUTH_TOKEN = 'Bearer test-token'
export function setup() {
// 测试前准备
console.log('开始性能测试...')
// 创建测试数据
const response = http.post(`${BASE_URL}/api/v1/test/setup`, {
count: 1000
}, {
headers: { 'Authorization': AUTH_TOKEN }
})
check(response, {
'测试数据创建成功': (r) => r.status === 200
})
return { testDataReady: true }
}
export default function() {
const headers = {
'Authorization': AUTH_TOKEN,
'Content-Type': 'application/json'
}
// 测试场景1: 查询成果列表
const listResponse = http.get(`${BASE_URL}/api/v1/achievements?page=1&size=20`, {
headers: headers
})
const listSuccess = check(listResponse, {
'成果列表状态码200': (r) => r.status === 200,
'成果列表响应时间<200ms': (r) => r.timings.duration < 200,
'成果列表返回数据': (r) => r.json('data.data').length > 0
})
errorRate.add(!listSuccess)
responseTime.add(listResponse.timings.duration)
sleep(1)
// 测试场景2: 搜索成果
const searchPayload = JSON.stringify({
keyword: '人工智能',
page: 1,
size: 10
})
const searchResponse = http.post(`${BASE_URL}/api/v1/achievements/search`, searchPayload, {
headers: headers
})
const searchSuccess = check(searchResponse, {
'搜索状态码200': (r) => r.status === 200,
'搜索响应时间<300ms': (r) => r.timings.duration < 300,
'搜索返回结果': (r) => r.json('data.total') >= 0
})
errorRate.add(!searchSuccess)
sleep(2)
// 测试场景3: 获取成果详情
if (listResponse.status === 200) {
const achievements = listResponse.json('data.data')
if (achievements.length > 0) {
const randomAchievement = achievements[Math.floor(Math.random() * achievements.length)]
const detailResponse = http.get(`${BASE_URL}/api/v1/achievements/${randomAchievement.id}`, {
headers: headers
})
const detailSuccess = check(detailResponse, {
'详情状态码200': (r) => r.status === 200,
'详情响应时间<150ms': (r) => r.timings.duration < 150
})
errorRate.add(!detailSuccess)
}
}
sleep(1)
}
export function teardown(data) {
// 测试后清理
console.log('性能测试完成,清理测试数据...')
const response = http.post(`${BASE_URL}/api/v1/test/cleanup`, {}, {
headers: { 'Authorization': AUTH_TOKEN }
})
check(response, {
'测试数据清理成功': (r) => r.status === 200
})
}
5.1.2 压力测试
// tests/performance/stress-test.js
import http from 'k6/http'
import { check, sleep } from 'k6'
export const options = {
// 压力测试配置
stages: [
{ duration: '5m', target: 100 }, // 正常负载
{ duration: '10m', target: 300 }, // 压力负载
{ duration: '5m', target: 500 }, // 极限负载
{ duration: '10m', target: 500 }, // 持续极限负载
{ duration: '5m', target: 0 }, // 恢复
],
thresholds: {
http_req_duration: ['p(95)<500'], // 压力测试下95%请求<500ms
http_req_failed: ['rate<0.05'], // 错误率<5%
},
}
export default function() {
// 压力测试场景
const responses = http.batch([
['GET', `${BASE_URL}/api/v1/achievements?page=${Math.floor(Math.random() * 10) + 1}`],
['GET', `${BASE_URL}/api/v1/demands?page=${Math.floor(Math.random() * 10) + 1}`],
['GET', `${BASE_URL}/api/v1/activities?page=${Math.floor(Math.random() * 5) + 1}`]
])
responses.forEach((response, index) => {
check(response, {
[`批量请求${index}状态码200`]: (r) => r.status === 200
})
})
sleep(Math.random() * 3 + 1) // 1-4秒随机等待
}
5.2 数据库性能测试
5.2.1 查询性能测试
-- tests/performance/database-performance.sql
-- 测试1: 成果列表查询性能
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, title, summary, tech_field, status, view_count, created_time
FROM t_achievement
WHERE tenant_id = 'TENANT_TEST'
AND status = 'PUBLISHED'
ORDER BY created_time DESC
LIMIT 20 OFFSET 0;
-- 测试2: 成果搜索查询性能
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, title, summary, tech_field, ts_rank(search_vector, query) as rank
FROM t_achievement, plainto_tsquery('chinese', '人工智能') query
WHERE tenant_id = 'TENANT_TEST'
AND status = 'PUBLISHED'
AND search_vector @@ query
ORDER BY rank DESC, created_time DESC
LIMIT 20;
-- 测试3: 复合查询性能
EXPLAIN (ANALYZE, BUFFERS)
SELECT a.id, a.title, a.summary, u.username as owner_name
FROM t_achievement a
JOIN t_user u ON a.owner_id = u.id
WHERE a.tenant_id = 'TENANT_TEST'
AND a.tech_field = '人工智能'
AND a.created_time >= CURRENT_DATE - INTERVAL '30 days'
AND a.status = 'PUBLISHED'
ORDER BY a.view_count DESC, a.created_time DESC
LIMIT 20;
-- 性能基准测试
-- 创建测试数据
INSERT INTO t_achievement (id, title, summary, tech_field, status, owner_id, tenant_id, created_time)
SELECT
'ACH_' || LPAD(generate_series::text, 26, '0'),
'测试成果' || generate_series,
'这是第' || generate_series || '个测试成果的摘要',
CASE (generate_series % 5)
WHEN 0 THEN '人工智能'
WHEN 1 THEN '生物医药'
WHEN 2 THEN '新材料'
WHEN 3 THEN '新能源'
ELSE '电子信息'
END,
'PUBLISHED',
'USR_01HMXVKJ8RG9CQXY85ZZQK2N3P',
'TENANT_TEST',
CURRENT_TIMESTAMP - (random() * INTERVAL '365 days')
FROM generate_series(1, 100000);
-- 分析查询计划
ANALYZE t_achievement;
-- 测试索引效果
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE tablename = 't_achievement';
6. 安全测试实施
6.1 OWASP ZAP 自动化安全测试
6.1.1 安全测试脚本
# tests/security/zap-security-test.py
import time
from zapv2 import ZAPv2
class SecurityTestRunner:
def __init__(self):
self.zap = ZAPv2(proxies={'http': 'http://127.0.0.1:8080', 'https': 'http://127.0.0.1:8080'})
self.target_url = 'http://localhost:3000'
def run_security_scan(self):
print("开始安全扫描...")
# 1. 启动ZAP代理
print("启动ZAP代理...")
# 2. 爬虫扫描
self.spider_scan()
# 3. 主动扫描
self.active_scan()
# 4. 生成报告
self.generate_report()
def spider_scan(self):
print("开始爬虫扫描...")
# 配置认证
self.configure_authentication()
# 启动爬虫
scan_id = self.zap.spider.scan(self.target_url)
# 等待爬虫完成
while int(self.zap.spider.status(scan_id)) < 100:
print(f"爬虫进度: {self.zap.spider.status(scan_id)}%")
time.sleep(5)
print("爬虫扫描完成")
def active_scan(self):
print("开始主动安全扫描...")
# 启动主动扫描
scan_id = self.zap.ascan.scan(self.target_url)
# 等待扫描完成
while int(self.zap.ascan.status(scan_id)) < 100:
print(f"主动扫描进度: {self.zap.ascan.status(scan_id)}%")
time.sleep(10)
print("主动扫描完成")
def configure_authentication(self):
"""配置认证信息"""
# 设置认证方法
self.zap.authentication.set_authentication_method(
contextid=0,
authMethodName='formBasedAuthentication',
authMethodConfigParams='loginUrl=http://localhost:3000/login&loginRequestData=username%3D{%25username%25}%26password%3D{%25password%25}'
)
# 设置登录指示器
self.zap.authentication.set_logged_in_indicator(
contextid=0,
loggedInIndicatorRegex='\\Q<a href="/logout">\\E'
)
# 设置登出指示器
self.zap.authentication.set_logged_out_indicator(
contextid=0,
loggedOutIndicatorRegex='\\Q<input type="password"\\E'
)
# 添加用户
user_id = self.zap.users.new_user(contextid=0, name='testuser')
self.zap.users.set_authentication_credentials(
contextid=0,
userid=user_id,
authCredentialsConfigParams='username=testuser%40example.com&password=password123'
)
self.zap.users.set_user_enabled(contextid=0, userid=user_id, enabled=True)
def generate_report(self):
"""生成安全测试报告"""
# 获取扫描结果
alerts = self.zap.core.alerts()
high_risk = [alert for alert in alerts if alert['risk'] == 'High']
medium_risk = [alert for alert in alerts if alert['risk'] == 'Medium']
low_risk = [alert for alert in alerts if alert['risk'] == 'Low']
print(f"\n安全扫描结果:")
print(f"高风险漏洞: {len(high_risk)}")
print(f"中风险漏洞: {len(medium_risk)}")
print(f"低风险漏洞: {len(low_risk)}")
# 生成HTML报告
html_report = self.zap.core.htmlreport()
with open('security-report.html', 'w', encoding='utf-8') as f:
f.write(html_report)
# 生成JSON报告
json_report = self.zap.core.jsonreport()
with open('security-report.json', 'w', encoding='utf-8') as f:
f.write(json_report)
print("安全测试报告已生成")
# 检查是否存在高风险漏洞
if len(high_risk) > 0:
print("⚠️ 发现高风险安全漏洞!")
for alert in high_risk:
print(f"- {alert['name']}: {alert['description']}")
exit(1)
else:
print("✅ 未发现高风险安全漏洞")
if __name__ == "__main__":
runner = SecurityTestRunner()
runner.run_security_scan()
6.2 SQL注入测试
6.2.1 自动化SQL注入测试
# tests/security/sql-injection-test.py
import requests
import json
from typing import List, Dict
class SQLInjectionTester:
def __init__(self, base_url: str, auth_token: str):
self.base_url = base_url
self.headers = {
'Authorization': f'Bearer {auth_token}',
'Content-Type': 'application/json'
}
# SQL注入攻击载荷
self.payloads = [
"' OR '1'='1",
"' OR '1'='1' --",
"' OR '1'='1' /*",
"'; DROP TABLE users; --",
"' UNION SELECT * FROM users --",
"admin'--",
"admin' #",
"admin'/*",
"' or 1=1#",
"' or 1=1--",
"' or 1=1/*",
"') or '1'='1--",
"') or ('1'='1--"
]
def test_search_endpoints(self) -> List[Dict]:
"""测试搜索接口的SQL注入漏洞"""
vulnerabilities = []
# 测试成果搜索
for payload in self.payloads:
try:
response = requests.post(
f"{self.base_url}/api/v1/achievements/search",
headers=self.headers,
json={
"keyword": payload,
"page": 1,
"size": 10
},
timeout=10
)
# 检查响应
if self.is_vulnerable_response(response, payload):
vulnerabilities.append({
"endpoint": "/api/v1/achievements/search",
"parameter": "keyword",
"payload": payload,
"response_status": response.status_code,
"response_text": response.text[:200]
})
except Exception as e:
print(f"测试载荷 {payload} 时发生错误: {e}")
return vulnerabilities
def test_filter_parameters(self) -> List[Dict]:
"""测试过滤参数的SQL注入漏洞"""
vulnerabilities = []
# 测试成果列表过滤
for payload in self.payloads:
try:
params = {
"techField": payload,
"status": "PUBLISHED",
"page": 1,
"size": 10
}
response = requests.get(
f"{self.base_url}/api/v1/achievements",
headers=self.headers,
params=params,
timeout=10
)
if self.is_vulnerable_response(response, payload):
vulnerabilities.append({
"endpoint": "/api/v1/achievements",
"parameter": "techField",
"payload": payload,
"response_status": response.status_code,
"response_text": response.text[:200]
})
except Exception as e:
print(f"测试过滤参数时发生错误: {e}")
return vulnerabilities
def is_vulnerable_response(self, response: requests.Response, payload: str) -> bool:
"""判断响应是否表明存在SQL注入漏洞"""
# 检查SQL错误消息
sql_errors = [
"sql syntax",
"mysql_fetch",
"ora-01756",
"microsoft ole db provider",
"unclosed quotation mark",
"sqlite_master",
"postgresql",
"pg_user"
]
response_text = response.text.lower()
# 检查是否包含SQL错误信息
for error in sql_errors:
if error in response_text:
return True
# 检查响应状态码
if response.status_code == 500:
return True
# 检查响应时间异常
if response.elapsed.total_seconds() > 10:
return True
return False
def run_all_tests(self) -> Dict:
"""运行所有SQL注入测试"""
print("开始SQL注入安全测试...")
all_vulnerabilities = []
# 测试搜索接口
search_vulns = self.test_search_endpoints()
all_vulnerabilities.extend(search_vulns)
# 测试过滤参数
filter_vulns = self.test_filter_parameters()
all_vulnerabilities.extend(filter_vulns)
# 生成报告
report = {
"total_tests": len(self.payloads) * 2, # 搜索 + 过滤
"vulnerabilities_found": len(all_vulnerabilities),
"vulnerabilities": all_vulnerabilities,
"risk_level": "HIGH" if all_vulnerabilities else "NONE"
}
# 保存报告
with open('sql-injection-report.json', 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(f"SQL注入测试完成,发现 {len(all_vulnerabilities)} 个潜在漏洞")
return report
if __name__ == "__main__":
tester = SQLInjectionTester(
base_url="http://localhost:8080",
auth_token="test-token"
)
report = tester.run_all_tests()
if report["vulnerabilities_found"] > 0:
print("⚠️ 发现SQL注入漏洞!")
exit(1)
else:
print("✅ 未发现SQL注入漏洞")
7. 测试自动化与CI/CD集成
7.1 GitHub Actions 集成
7.1.1 测试工作流
# .github/workflows/test.yml
name: 测试工作流
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
env:
NODE_VERSION: '18'
JAVA_VERSION: '17'
PYTHON_VERSION: '3.11'
jobs:
# 单元测试
unit-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: gmkx_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
# Java后端测试
- name: 设置JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
- name: 缓存Maven依赖
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: 运行后端单元测试
run: |
./mvnw test -Dspring.profiles.active=test
./mvnw jacoco:report
- name: 上传测试覆盖率
uses: codecov/codecov-action@v3
with:
file: target/site/jacoco/jacoco.xml
flags: backend
# Node.js前端测试
- name: 设置Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: gmkx-web/package-lock.json
- name: 安装前端依赖
run: |
cd gmkx-web
npm ci
- name: 运行前端单元测试
run: |
cd gmkx-web
npm run test:coverage
- name: 上传前端覆盖率
uses: codecov/codecov-action@v3
with:
file: gmkx-web/coverage/lcov.info
flags: frontend
# 集成测试
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4
- name: 设置Java环境
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
- name: 运行集成测试
run: |
./mvnw verify -Dspring.profiles.active=integration-test
- name: 上传测试报告
uses: actions/upload-artifact@v3
if: always()
with:
name: integration-test-results
path: target/failsafe-reports/
# 端到端测试
e2e-tests:
runs-on: ubuntu-latest
needs: [unit-tests, integration-tests]
steps:
- uses: actions/checkout@v4
- name: 设置Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: gmkx-web/package-lock.json
- name: 安装依赖
run: |
cd gmkx-web
npm ci
npx playwright install --with-deps
- name: 启动测试环境
run: |
docker-compose -f docker-compose.test.yml up -d
sleep 30 # 等待服务启动
- name: 运行E2E测试
run: |
cd gmkx-web
npm run test:e2e
- name: 上传E2E测试报告
uses: actions/upload-artifact@v3
if: always()
with:
name: e2e-test-results
path: |
gmkx-web/test-results/
gmkx-web/playwright-report/
- name: 清理测试环境
if: always()
run: |
docker-compose -f docker-compose.test.yml down -v
# 性能测试
performance-tests:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs: e2e-tests
steps:
- uses: actions/checkout@v4
- name: 安装K6
run: |
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: 启动性能测试环境
run: |
docker-compose -f docker-compose.perf.yml up -d
sleep 60 # 等待服务完全启动
- name: 运行性能测试
run: |
k6 run tests/performance/basic-load.js
- name: 上传性能测试报告
uses: actions/upload-artifact@v3
if: always()
with:
name: performance-test-results
path: performance-report.json
# 安全测试
security-tests:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: 运行OWASP依赖检查
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'gmkx-platform'
path: '.'
format: 'ALL'
- name: 上传依赖检查报告
uses: actions/upload-artifact@v3
with:
name: dependency-check-report
path: reports/
- name: 运行Trivy漏洞扫描
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: 上传Trivy扫描结果
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
# 代码质量检查
code-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # SonarQube需要完整历史
- name: 设置Java环境
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
- name: 缓存SonarQube依赖
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: 运行SonarQube分析
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
./mvnw sonar:sonar \
-Dsonar.projectKey=gmkx-platform \
-Dsonar.organization=your-org \
-Dsonar.host.url=https://sonarcloud.io
7.2 测试报告生成
7.2.1 综合测试报告
# scripts/generate-test-report.py
import json
import xml.etree.ElementTree as ET
from datetime import datetime
from jinja2 import Template
import os
class TestReportGenerator:
def __init__(self):
self.report_data = {
'timestamp': datetime.now().isoformat(),
'unit_tests': {},
'integration_tests': {},
'e2e_tests': {},
'performance_tests': {},
'security_tests': {},
'code_coverage': {}
}
def parse_junit_report(self, file_path: str) -> dict:
"""解析JUnit测试报告"""
tree = ET.parse(file_path)
root = tree.getroot()
return {
'total': int(root.attrib.get('tests', 0)),
'passed': int(root.attrib.get('tests', 0)) - int(root.attrib.get('failures', 0)) - int(root.attrib.get('errors', 0)),
'failed': int(root.attrib.get('failures', 0)),
'errors': int(root.attrib.get('errors', 0)),
'skipped': int(root.attrib.get('skipped', 0)),
'time': float(root.attrib.get('time', 0))
}
def parse_coverage_report(self, file_path: str) -> dict:
"""解析代码覆盖率报告"""
with open(file_path, 'r') as f:
data = json.load(f)
return {
'line_coverage': data.get('line_coverage', 0),
'branch_coverage': data.get('branch_coverage', 0),
'function_coverage': data.get('function_coverage', 0)
}
def parse_performance_report(self, file_path: str) -> dict:
"""解析性能测试报告"""
with open(file_path, 'r') as f:
data = json.load(f)
metrics = data.get('metrics', {})
return {
'avg_response_time': metrics.get('http_req_duration', {}).get('avg', 0),
'p95_response_time': metrics.get('http_req_duration', {}).get('p(95)', 0),
'throughput': metrics.get('http_reqs', {}).get('rate', 0),
'error_rate': metrics.get('http_req_failed', {}).get('rate', 0)
}
def generate_html_report(self) -> str:
"""生成HTML测试报告"""
template_str = '''
<!DOCTYPE html>
<html>
<head>
<title>测试报告 - {{ timestamp }}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; }
.section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.passed { color: #28a745; }
.failed { color: #dc3545; }
.warning { color: #ffc107; }
.metric { display: inline-block; margin: 10px; padding: 10px; background: #f8f9fa; border-radius: 3px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<div class="header">
<h1>光明科学城科技成果转化平台 - 测试报告</h1>
<p>生成时间: {{ timestamp }}</p>
</div>
<div class="section">
<h2>测试概览</h2>
<div class="metric">
<strong>单元测试:</strong>
<span class="{% if unit_tests.failed > 0 %}failed{% else %}passed{% endif %}">
{{ unit_tests.passed }}/{{ unit_tests.total }} 通过
</span>
</div>
<div class="metric">
<strong>集成测试:</strong>
<span class="{% if integration_tests.failed > 0 %}failed{% else %}passed{% endif %}">
{{ integration_tests.passed }}/{{ integration_tests.total }} 通过
</span>
</div>
<div class="metric">
<strong>E2E测试:</strong>
<span class="{% if e2e_tests.failed > 0 %}failed{% else %}passed{% endif %}">
{{ e2e_tests.passed }}/{{ e2e_tests.total }} 通过
</span>
</div>
</div>
<div class="section">
<h2>代码覆盖率</h2>
<table>
<tr>
<th>类型</th>
<th>覆盖率</th>
<th>状态</th>
</tr>
<tr>
<td>行覆盖率</td>
<td>{{ "%.1f"|format(code_coverage.line_coverage * 100) }}%</td>
<td class="{% if code_coverage.line_coverage >= 0.8 %}passed{% else %}warning{% endif %}">
{% if code_coverage.line_coverage >= 0.8 %}✓ 良好{% else %}⚠ 需改进{% endif %}
</td>
</tr>
<tr>
<td>分支覆盖率</td>
<td>{{ "%.1f"|format(code_coverage.branch_coverage * 100) }}%</td>
<td class="{% if code_coverage.branch_coverage >= 0.7 %}passed{% else %}warning{% endif %}">
{% if code_coverage.branch_coverage >= 0.7 %}✓ 良好{% else %}⚠ 需改进{% endif %}
</td>
</tr>
</table>
</div>
<div class="section">
<h2>性能测试结果</h2>
<table>
<tr>
<th>指标</th>
<th>值</th>
<th>状态</th>
</tr>
<tr>
<td>平均响应时间</td>
<td>{{ "%.0f"|format(performance_tests.avg_response_time) }} ms</td>
<td class="{% if performance_tests.avg_response_time <= 200 %}passed{% else %}warning{% endif %}">
{% if performance_tests.avg_response_time <= 200 %}✓ 优秀{% else %}⚠ 超标{% endif %}
</td>
</tr>
<tr>
<td>P95响应时间</td>
<td>{{ "%.0f"|format(performance_tests.p95_response_time) }} ms</td>
<td class="{% if performance_tests.p95_response_time <= 500 %}passed{% else %}warning{% endif %}">
{% if performance_tests.p95_response_time <= 500 %}✓ 良好{% else %}⚠ 超标{% endif %}
</td>
</tr>
<tr>
<td>吞吐量</td>
<td>{{ "%.1f"|format(performance_tests.throughput) }} req/s</td>
<td class="passed">✓</td>
</tr>
<tr>
<td>错误率</td>
<td>{{ "%.2f"|format(performance_tests.error_rate * 100) }}%</td>
<td class="{% if performance_tests.error_rate <= 0.01 %}passed{% else %}failed{% endif %}">
{% if performance_tests.error_rate <= 0.01 %}✓ 良好{% else %}✗ 超标{% endif %}
</td>
</tr>
</table>
</div>
<div class="section">
<h2>安全测试结果</h2>
<p>
安全漏洞扫描:
<span class="{% if security_tests.vulnerabilities == 0 %}passed{% else %}failed{% endif %}">
{% if security_tests.vulnerabilities == 0 %}
✓ 未发现高危漏洞
{% else %}
⚠ 发现 {{ security_tests.vulnerabilities }} 个安全问题
{% endif %}
</span>
</p>
</div>
</body>
</html>
'''
template = Template(template_str)
return template.render(**self.report_data)
def collect_all_reports(self):
"""收集所有测试报告"""
# 收集JUnit报告
if os.path.exists('target/surefire-reports/TEST-*.xml'):
for file in glob.glob('target/surefire-reports/TEST-*.xml'):
self.report_data['unit_tests'] = self.parse_junit_report(file)
break
# 收集覆盖率报告
if os.path.exists('target/site/jacoco/jacoco.xml'):
self.report_data['code_coverage'] = self.parse_coverage_report('coverage-summary.json')
# 收集性能测试报告
if os.path.exists('performance-report.json'):
self.report_data['performance_tests'] = self.parse_performance_report('performance-report.json')
# 收集安全测试报告
if os.path.exists('security-report.json'):
with open('security-report.json') as f:
security_data = json.load(f)
self.report_data['security_tests'] = {
'vulnerabilities': security_data.get('vulnerabilities_found', 0)
}
def generate_report(self):
"""生成完整测试报告"""
self.collect_all_reports()
# 生成HTML报告
html_report = self.generate_html_report()
with open('test-report.html', 'w', encoding='utf-8') as f:
f.write(html_report)
# 生成JSON报告
with open('test-report.json', 'w', encoding='utf-8') as f:
json.dump(self.report_data, f, ensure_ascii=False, indent=2)
print("测试报告已生成: test-report.html")
if __name__ == "__main__":
generator = TestReportGenerator()
generator.generate_report()
这个综合的测试策略和实施方案涵盖了从单元测试到生产环境的完整测试体系,确保系统质量和稳定性。