29 KiB
29 KiB
🦟 蚊子项目 - 完整测试验证方案
📋 测试策略概览
基于评审报告和修复内容,我们设计了完整的测试验证方案,确保代码质量和功能正确性。
测试维度
| 维度 | 测试类型 | 覆盖率 | 工具 |
|---|---|---|---|
| 单元测试 | 逻辑单元测试 | 90%+ | JUnit 5, Mockito |
| 集成测试 | API接口测试 | 100% | Spring Boot Test, MockMvc |
| 安全测试 | 安全漏洞测试 | 100% | OWASP ZAP, Postman |
| 性能测试 | 负载和压力测试 | 核心接口 | JMeter, Gatling |
| 前端测试 | 组件和E2E测试 | 85%+ | Vitest, Playwright |
| 端到端测试 | 用户流程测试 | 核心流程 | Selenium, Cypress |
🔒 安全测试验证
1. SSRF漏洞修复验证
测试用例
@SpringBootTest
@AutoConfigureMockMvc
class ShortLinkControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldBlockInternalIPs() throws Exception {
// 测试内网IP访问
mockMvc.perform(get("/r/test123")
.header("X-Forwarded-For", "192.168.1.100"))
.andExpect(status().isBadRequest());
// 测试localhost访问
mockMvc.perform(get("/r/test123")
.header("X-Forwarded-For", "127.0.0.1"))
.andExpect(status().isBadRequest());
// 测试私有网络
mockMvc.perform(get("/r/test123")
.header("X-Forwarded-For", "10.0.0.1"))
.andExpect(status().isBadRequest());
}
@Test
void shouldAllowExternalURLs() throws Exception {
mockMvc.perform(get("/r/test123")
.header("X-Forwarded-For", "8.8.8.8"))
.andExpect(status().isFound());
}
@Test
void shouldValidateURLScheme() throws Exception {
// 测试非HTTP/HTTPS协议
mockMvc.perform(post("/api/v1/internal/shorten")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"originalUrl\":\"ftp://example.com\"}"))
.andExpect(status().isBadRequest());
}
}
自动化脚本
#!/bin/bash
# SSRF安全测试脚本
echo "=== SSRF安全测试 ==="
# 测试内网访问
echo "1. 测试内网IP访问..."
curl -s -o /dev/null -w "%{http_code}" \
-H "X-Forwarded-For: 192.168.1.100" \
"http://localhost:8080/r/test123"
# 测试localhost访问
echo -e "\n2. 测试localhost访问..."
curl -s -o /dev/null -w "%{http_code}" \
-H "X-Forwarded-For: 127.0.0.1" \
"http://localhost:8080/r/test123"
# 测试有效URL
echo -e "\n3. 测试有效URL访问..."
curl -s -o /dev/null -w "%{http_code}" \
-H "X-Forwarded-For: 8.8.8.8" \
"http://localhost:8080/r/test123"
echo -e "\n=== SSRF测试完成 ==="
2. API密钥恢复机制验证
测试用例
@SpringBootTest
@AutoConfigureMockMvc
class ApiKeySecurityControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ApiKeyRepository apiKeyRepository;
@Test
void shouldRevealApiKeyWithValidVerification() throws Exception {
// 创建测试API密钥
ApiKeyEntity apiKey = apiKeyRepository.save(createTestApiKey());
// 测试重新显示
mockMvc.perform(post("/api/v1/api-keys/{id}/reveal", apiKey.getId())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"verificationCode\":\"test123\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").exists());
}
@Test
void shouldNotRevealRevokedApiKey() throws Exception {
ApiKeyEntity apiKey = apiKeyRepository.save(createTestApiKey());
apiKey.setRevokedAt(OffsetDateTime.now());
apiKeyRepository.save(apiKey);
mockMvc.perform(post("/api/v1/api-keys/{id}/reveal", apiKey.getId()))
.andExpect(status().isBadRequest());
}
@Test
void shouldRotateApiKey() throws Exception {
ApiKeyEntity apiKey = apiKeyRepository.save(createTestApiKey());
mockMvc.perform(post("/api/v1/api-keys/{id}/rotate", apiKey.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").exists());
}
private ApiKeyEntity createTestApiKey() {
ApiKeyEntity apiKey = new ApiKeyEntity();
apiKey.setActivityId(1L);
apiKey.setIsActive(true);
apiKey.setEncryptedKey("encrypted-test-key");
return apiKey;
}
}
3. 速率限制强制Redis验证
测试用例
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("prod")
class RateLimitInterceptorTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldEnforceRedisRateLimitInProduction() throws Exception {
// 生产环境测试,需要Redis配置
mockMvc.perform(get("/api/v1/activities/1"))
.andExpect(status().isOk());
// 模拟超过限制
for (int i = 0; i < 110; i++) {
mockMvc.perform(get("/api/v1/activities/1"));
}
// 应该被限制
mockMvc.perform(get("/api/v1/activities/1"))
.andExpect(status().isTooManyRequests());
}
@Test
void shouldFailWithoutRedisInProduction() throws Exception {
// 如果Redis未配置,应该抛出异常
mockMvc.perform(get("/api/v1/activities/1"))
.andExpect(result -> result.getResolvedException() instanceof IllegalStateException)
.andExpect(result -> result.getResolvedException()
.getMessage().contains("Production环境必须配置Redis"));
}
}
🧪 单元测试覆盖
测试覆盖率目标
- 核心服务类: 95%+
- 控制器类: 90%+
- 工具类: 85%+
- 整体覆盖率: 90%+
测试文件清单
# 后端测试文件
src/test/java/com/mosquito/project/
├── controller/
│ ├── ActivityControllerTest.java
│ ├── ApiKeySecurityControllerTest.java
│ ├── ShortLinkControllerSecurityTest.java
│ └── UserControllerTest.java
├── service/
│ ├── ActivityServiceCacheTest.java
│ ├── ActivityServiceTest.java
│ ├── ApiKeySecurityServiceTest.java
│ └── UrlValidatorTest.java
├── interceptor/
│ └── RateLimitInterceptorTest.java
└── exception/
└── GlobalExceptionHandlerTest.java
# 前端测试文件
frontend/tests/
├── unit/
│ ├── components/
│ │ ├── MosquitoShareButton.spec.ts
│ │ ├── MosquitoPosterCard.spec.ts
│ │ └── MosquitoLeaderboard.spec.ts
│ └── utils/
│ └── api-client.spec.ts
└── e2e/
├── share-flow.spec.ts
├── poster-generation.spec.ts
└── leaderboard.spec.ts
核心测试示例
ActivityService缓存测试
@Test
@DisplayName("排行榜缓存应该正确失效")
void shouldEvictCacheWhenCreatingReward() {
// 缓存初始状态
when(activityRepository.existsById(any())).thenReturn(true);
when(userInviteRepository.countInvitesByActivityIdGroupByInviter(any()))
.thenReturn(List.of(new Object[]{100L, 5L}));
// 第一次调用,应该缓存
List<LeaderboardEntry> firstCall = activityService.getLeaderboard(1L);
verify(cacheManager, times(1)).getCache(any());
// 创建奖励,应该清除缓存
Reward reward = new Reward(100);
activityService.createReward(reward, false);
// 第二次调用,应该重新查询
List<LeaderboardEntry> secondCall = activityService.getLeaderboard(1L);
verify(cacheManager, times(2)).getCache(any());
}
前端组件测试
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import MosquitoShareButton from '@/components/MosquitoShareButton.vue'
import { useMosquito } from '@mosquito/vue-enhanced'
vi.mock('@mosquito/vue-enhanced')
describe('MosquitoShareButton', () => {
it('应该正确显示加载状态', async () => {
const mockGetShareUrl = vi.fn().mockResolvedValue('test-url')
vi.mocked(useMosquito).mockReturnValue({
getShareUrl: mockGetShareUrl,
config: { baseUrl: 'test' }
})
const wrapper = mount(MosquitoShareButton, {
props: {
activityId: 1,
userId: 100
}
})
// 触发点击
await wrapper.find('button').trigger('click')
// 应该显示加载状态
expect(wrapper.find('.loading-spinner').exists()).toBe(true)
expect(wrapper.find('button').attributes('disabled')).toBe('disabled')
})
it('应该正确处理复制成功事件', async () => {
const mockGetShareUrl = vi.fn().mockResolvedValue('test-url')
const mockEmit = vi.fn()
vi.mocked(useMosquito).mockReturnValue({
getShareUrl: mockGetShareUrl,
config: { baseUrl: 'test' }
})
const wrapper = mount(MosquitoShareButton, {
props: {
activityId: 1,
userId: 100
},
emits: ['copied']
})
await wrapper.find('button').trigger('click')
// 等待异步操作完成
await vi.waitFor(() => {
expect(mockGetShareUrl).toHaveBeenCalled()
})
// 应该触发复制成功事件
expect(wrapper.emitted('copied')).toBeTruthy()
})
})
🚀 集成测试验证
API集成测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class ApiIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void shouldCompleteShareFlow() {
// 1. 创建活动
ActivityRequest request = new ActivityRequest();
request.setName("测试活动");
request.setStartTime(LocalDateTime.now().plusDays(1));
request.setEndTime(LocalDateTime.now().plusDays(7));
ResponseEntity<Activity> response = restTemplate.postForEntity(
"/api/v1/activities", request, Activity.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
Activity activity = response.getBody();
// 2. 获取分享链接
ResponseEntity<String> shareResponse = restTemplate.getForEntity(
String.format("/api/v1/me/share-url?activityId=%d&userId=100", activity.getId()),
String.class);
assertThat(shareResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
String shareUrl = shareResponse.getBody();
// 3. 生成短链接
ShortenRequest shortenRequest = new ShortenRequest();
shortenRequest.setOriginalUrl(shareUrl);
ResponseEntity<ShortLink> shortResponse = restTemplate.postForEntity(
"/api/v1/internal/shorten", shortenRequest, ShortLink.class);
assertThat(shortResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
// 4. 重定向测试
ResponseEntity<Void> redirectResponse = restTemplate.getForEntity(
String.format("/r/%s", shortResponse.getBody().getCode()),
Void.class);
assertThat(redirectResponse.getStatusCode()).isEqualTo(HttpStatus.FOUND);
assertThat(redirectResponse.getHeaders().getLocation().toString()).isEqualTo(shareUrl);
}
@Test
void shouldHandleApiRateLimiting() {
// 连续请求超过限制
for (int i = 0; i < 105; i++) {
ResponseEntity<String> response = restTemplate.getForEntity(
"/api/v1/activities/1", String.class);
if (i == 100) {
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS);
} else {
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
}
}
数据库集成测试
@DataSqlConfig
@SpringBootTest
class DatabaseIntegrationTest {
@Autowired
private ActivityRepository activityRepository;
@Autowired
private ApiKeyRepository apiKeyRepository;
@Test
@Sql(scripts = "/test-data.sql")
void shouldMaintainDataIntegrity() {
// 测试外键约束
assertThrows(DataIntegrityViolationException.class, () -> {
ApiKeyEntity invalidKey = new ApiKeyEntity();
invalidKey.setActivityId(999L); // 不存在的活动ID
invalidKey.setEncryptedKey("test");
apiKeyRepository.save(invalidKey);
});
// 测试级联删除
Activity activity = activityRepository.findById(1L).orElseThrow();
activityRepository.delete(activity);
assertThat(apiKeyRepository.findByActivityId(1L)).isEmpty();
}
}
⚡ 性能测试
JMeter测试计划
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0">
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="蚊子项目性能测试" enabled="true">
<arguments>
<Argument name="BASE_URL">http://localhost:8080</Argument>
<Argument name="THREAD_COUNT">100</Argument>
<Argument name="RAMP_UP">30</Argument>
<Argument name="TEST_DURATION">300</Argument>
</arguments>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="并发用户测试" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<stringProp name="ThreadGroup.num_threads">100</stringProp>
<stringProp name="ThreadGroup.ramp_time">30</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
<Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="默认参数" enabled="true">
<CollectionProp name="Arguments.arguments">
<Argument name="activityId">1</Argument>
<Argument name="userId">100</Argument>
<Argument name="template">default</Argument>
</CollectionProp>
</Arguments>
<HTTPSampler guiclass="HTTPSamplerGui" testclass="HTTPSampler" testname="获取分享链接" enabled="true">
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">8080</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/me/share-url</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<HTTPArguments guiclass="HTTPArgumentsPanel" testclass="HTTPArguments" testname="用户参数" enabled="true">
<collectionProp name="Arguments.arguments">
<HTTPArgument>
<stringProp name="Argument.name">activityId</stringProp>
<stringProp name="Argument.value">${activityId}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</HTTPArgument>
<HTTPArgument>
<stringProp name="Argument.name">userId</stringProp>
<stringProp name="Argument.value">${userId}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</HTTPArgument>
</collectionProp>
</HTTPArguments>
</HTTPSampler>
<ResponseAssertion guiclass="ResponseAssertionGui" testclass="ResponseAssertion" testname="响应状态断言" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="972197263">200</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Asserion.assume_success">false</boolProp>
<intProp name="Assertion.scope">all</intProp>
</ResponseAssertion>
</ThreadGroup>
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="查看结果树" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<timeStamp>true</timeStamp>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
<latency1>true</latency1>
<encoding1>true</encoding1>
<sampleCount1>true</sampleCount1>
<errorCount1>true</errorCount1>
<hostname1>true</hostname1>
<threads1>true</threads1>
<sampleInterval>false</sampleInterval>
</value>
</objProp>
</ResultCollector>
</TestPlan>
</jmeterTestPlan>
性能指标
| 指标 | 目标值 | 监控工具 |
|---|---|---|
| API响应时间 | < 200ms | JMeter, Micrometer |
| 并发用户数 | 1000+ | JMeter |
| 错误率 | < 0.1% | Grafana |
| 内存使用 | < 2GB | VisualVM |
| CPU使用率 | < 70% | Prometheus |
| 数据库连接池 | < 80% 使用率 | HikariCP监控 |
🌐 端到端测试
Cypress测试脚本
// cypress/e2e/share-flow.cy.ts
describe('分享功能端到端测试', () => {
beforeEach(() => {
cy.visit('/login')
cy.get('#username').type('testuser')
cy.get('#password').type('password123')
cy.get('form').submit()
cy.url().should('include', '/dashboard')
})
it('应该完成完整的分享流程', () => {
// 1. 创建活动
cy.contains('创建活动').click()
cy.get('#activity-name').type('端到端测试活动')
cy.get('#start-time').type('2024-01-01T10:00')
cy.get('#end-time').type('2024-01-07T23:59')
cy.contains('提交').click()
// 2. 获取分享链接
cy.contains('分享活动').click()
cy.get('#share-button').click()
// 3. 验证链接复制
cy.contains('分享链接已复制到剪贴板').should('be.visible')
// 4. 测试短链接
cy.get('#short-link').should('exist')
cy.get('#short-link').click()
// 5. 验证重定向
cy.url().should('include', '/landing')
// 6. 测试海报生成
cy.contains('生成海报').click()
cy.get('#poster-preview').should('be.visible')
// 7. 测试排行榜
cy.contains('排行榜').click()
cy.get('.leaderboard-item').should('have.length.gt', 0)
})
it('应该处理错误情况', () => {
// 测试网络错误
cy.intercept('GET', '/api/v1/me/share-url', {
statusCode: 500,
body: { message: '服务器内部错误' }
})
cy.contains('分享活动').click()
cy.get('#share-button').click()
cy.contains('获取分享链接失败').should('be.visible')
})
})
🔍 测试执行指南
自动化测试脚本
#!/bin/bash
# test-runner.sh - 完整测试执行脚本
set -e
echo "🦟 蚊子项目 - 完整测试验证"
echo "================================"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 测试结果统计
TOTAL_TESTS=0
PASSED_TESTS=0
FAILED_TESTS=0
# 函数:执行测试并统计
run_test() {
local test_name="$1"
local command="$2"
echo -e "${YELLOW}执行测试: $test_name${NC}"
echo "命令: $command"
TOTAL_TESTS=$((TOTAL_TESTS + 1))
if eval "$command"; then
echo -e "${GREEN}✅ $test_name 通过${NC}"
PASSED_TESTS=$((PASSED_TESTS + 1))
else
echo -e "${RED}❌ $test_name 失败${NC}"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi
echo "--------------------------------"
}
# 1. 环境准备
echo -e "${YELLOW}🚀 1. 环境准备${NC}"
run_test "检查Java环境" "java -version"
run_test "检查Maven环境" "mvn -version"
run_test "检查Node.js环境" "node --version"
run_test "检查npm环境" "npm --version"
# 2. 代码质量检查
echo -e "${YELLOW}🔍 2. 代码质量检查${NC}"
run_test "编译检查" "mvn clean compile"
run_test "代码风格检查" "mvn checkstyle:check"
run_test "静态代码分析" "mvn spotbugs:check"
# 3. 单元测试
echo -e "${YELLOW}🧪 3. 单元测试${NC}"
run_test "后端单元测试" "mvn test -Dspring-boot.test.include=com.mosquito.project.*Test"
run_test "测试覆盖率检查" "mvn jacoco:report"
run_test "覆盖率验证" "mvn jacoco:check -Djacoco.skip=false"
# 4. 集成测试
echo -e "${YELLOW}🔗 4. 集成测试${NC}"
run_test "API集成测试" "mvn verify -Dspring-boot.test.include=com.mosquito.project.*IT"
run_test "数据库集成测试" "mvn flyway:migrate && mvn test -Dspring-boot.test.include=com.mosquito.project.*DataTest"
# 5. 安全测试
echo -e "${YELLOW}🔒 5. 安全测试${NC}"
run_test "SSRF漏洞验证" "./scripts/test-ssrf.sh"
run_test "API密钥安全验证" "mvn test -Dtest=ApiKeySecurityControllerTest"
run_test "速率限制验证" "mvn test -Dtest=RateLimitInterceptorTest"
# 6. 前端测试
echo -e "${YELLOW}🎨 6. 前端测试${NC}"
cd frontend
run_test "前端依赖安装" "npm install"
run_test "前端单元测试" "npm run test:unit"
run_test "前端端到端测试" "npm run test:e2e"
cd ..
# 7. 性能测试
echo -e "${YELLOW}⚡ 7. 性能测试${NC}"
run_test "JMeter基础性能测试" "jmeter -n -t performance-test.jmx -l results.jtl"
run_test "性能分析" "jmeter -g results.jtl -o performance-report"
# 8. 安全扫描
echo -e "${YELLOW}🛡️ 8. 安全扫描${NC}"
run_test "OWASP ZAP扫描" "zap-baseline.py -t http://localhost:8080 -c zap-baseline.conf"
run_test "依赖漏洞检查" "mvn dependency-check:check"
# 9. 文档验证
echo -e "${YELLOW}📚 9. 文档验证${NC}"
run_test "API文档生成" "mvn springdoc-openapi:generate"
run_test "文档链接检查" "./scripts/check-docs-links.sh"
# 测试结果汇总
echo -e "${GREEN}================================${NC}"
echo -e "${GREEN}🎯 测试结果汇总${NC}"
echo -e "${GREEN}================================${NC}"
echo -e "总测试数: $TOTAL_TESTS"
echo -e "${GREEN}通过数: $PASSED_TESTS${NC}"
echo -e "${RED}失败数: $FAILED_TESTS${NC}"
# 计算成功率
if [ $TOTAL_TESTS -gt 0 ]; then
SUCCESS_RATE=$((PASSED_TESTS * 100 / TOTAL_TESTS))
echo -e "成功率: ${GREEN}$SUCCESS_RATE%${NC}"
fi
# 判断是否通过
if [ $FAILED_TESTS -eq 0 ]; then
echo -e "${GREEN}🎉 所有测试通过!${NC}"
exit 0
else
echo -e "${RED}⚠️ 有 $FAILED_TESTS 个测试失败${NC}"
exit 1
fi
CI/CD集成
# .github/workflows/test.yml
name: Complete Test Suite
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: mosquito_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Run Security Tests
run: |
./scripts/test-runner.sh
env:
SPRING_PROFILES_ACTIVE: test
- name: Generate Test Report
run: |
mvn surefire-report:report
mvn jacoco:report
- name: Upload Test Results
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
target/surefire-reports/
target/site/jacoco/
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./target/site/jacoco/jacoco.xml
flags: unittests
name: codecov-umbrella
- name: Run Frontend Tests
run: |
cd frontend
npm install
npm run test:unit -- --coverage
npm run test:e2e
env:
CI: true
- name: Upload Frontend Test Results
uses: actions/upload-artifact@v4
with:
name: frontend-test-results
path: |
frontend/coverage/
frontend/test-results/
📊 测试报告模板
测试执行报告
# 🦟 蚊子项目 - 测试执行报告
**执行时间**: 2026-01-22 14:30:00
**执行环境**: Ubuntu 20.04, JDK 17, Node 18
**测试版本**: v2.0.0
## 📈 测试结果汇总
### 整体指标
- **测试通过率**: 98.5% (317/322)
- **代码覆盖率**: 92.3%
- **安全漏洞数**: 0
- **性能指标**: 达标
### 分项测试结果
#### 🔒 安全测试 - ✅ 全部通过
| 测试项目 | 状态 | 详情 |
|----------|------|------|
| SSRF漏洞修复 | ✅ 通过 | 内网IP访问被正确拦截 |
| API密钥安全 | ✅ 通过 | 恢复机制正常工作 |
| 速率限制 | ✅ 通过 | Redis强制限制生效 |
| 输入验证 | ✅ 通过 | 所有输入验证正常 |
#### 🧪 单元测试 - ✅ 高覆盖率
| 模块 | 覆盖率 | 状态 |
|------|--------|------|
| ActivityService | 95.2% | ✅ |
| ApiKeyService | 94.8% | ✅ |
| ShortLinkController | 93.5% | ✅ |
| GlobalExceptionHandler | 92.1% | ✅ |
#### 🔗 集成测试 - ✅ 核心流程通过
| 流程 | 状态 | 详情 |
|------|------|------|
| 用户注册登录 | ✅ 正常 |
| 活动创建管理 | ✅ 正常 |
| 分享功能 | ✅ 正常 |
| 海报生成 | ✅ 正常 |
| 排行榜 | ✅ 正常 |
#### ⚡ 性能测试 - ✅ 指标达标
| 指标 | 目标值 | 实际值 | 状态 |
|------|--------|--------|------|
| API响应时间 | < 200ms | 145ms | ✅ |
| 并发处理 | 1000用户 | 1200用户 | ✅ |
| 内存使用 | < 2GB | 1.2GB | ✅ |
| 错误率 | < 0.1% | 0.05% | ✅ |
#### 🎨 前端测试 - ✅ 组件正常
| 测试类型 | 覆盖率 | 状态 |
|----------|--------|------|
| 组件单元测试 | 88.5% | ✅ |
| E2E流程测试 | 86.2% | ✅ |
| 可访问性测试 | ✅ 通过 |
## 🐛 发现的问题
### 已修复的问题
1. **SSRF漏洞** - 已修复,添加URL白名单验证
2. **API密钥暴露** - 已修复,实现加密存储和恢复机制
3. **缓存失效** - 已修复,添加@CacheEvict注解
### 待改进问题
1. **前端加载状态** - 部分组件加载状态显示不够流畅
2. **错误提示** - 某些错误提示可以更加用户友好
## 📋 建议
### 短期改进(1-2周)
1. 优化前端组件加载状态动画
2. 改进错误提示信息
3. 增加用户操作引导
### 中期改进(1个月)
1. 实现自动化性能监控
2. 增加API版本控制测试
3. 完善集成测试场景
### 长期改进(3个月)
1. 实现持续性能测试
2. 增加安全自动化扫描
3. 建立质量门禁机制
## 🎯 下一步计划
1. **发布前验证** - 在预生产环境进行完整测试
2. **用户验收测试** - 邀请真实用户进行测试
3. **生产监控** - 建立生产环境质量监控
---
*报告生成时间: 2026-01-22 14:45:00*
*测试负责人: QA Team*
*审核人: Tech Lead*
✅ 验证检查清单
安全验证清单
- SSRF漏洞修复测试
- API密钥恢复机制测试
- 速率限制强制Redis测试
- 输入验证测试
- 异常处理测试
功能验证清单
- 用户注册登录流程
- 活动创建管理功能
- 分享链接生成和重定向
- 海报生成功能
- 排行榜统计和展示
性能验证清单
- 100并发用户测试
- 内存使用监控
- 响应时间测试
- 错误率监控
前端验证清单
- Vue组件功能测试
- 响应式设计测试
- 错误处理测试
- 可访问性测试
通过完整的测试验证方案,确保蚊子项目的修复质量和功能稳定性。