feat: 重构测试框架并优化代码结构
refactor(tests): 将e2e_tests迁移到tests_suite和api_integration_tests style: 为Java类添加文档注释 docs: 更新.gitignore和配置文件 test: 添加性能测试和Playwright测试脚本 chore: 清理旧测试文件和配置
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8084';
|
||||
const TEST_DURATION = __ENV.DURATION || '30s';
|
||||
const VUS = __ENV.VUS || '10';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
baseline: {
|
||||
executor: 'constant-vus',
|
||||
vus: 10,
|
||||
duration: '30s',
|
||||
startTime: '0s',
|
||||
},
|
||||
stress_test: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 10,
|
||||
stages: [
|
||||
{ duration: '1m', target: 50 },
|
||||
{ duration: '2m', target: 100 },
|
||||
{ duration: '1m', target: 50 },
|
||||
{ duration: '1m', target: 10 }
|
||||
],
|
||||
startTime: '0s',
|
||||
},
|
||||
spike_test: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 10,
|
||||
stages: [
|
||||
{ duration: '30s', target: 10 },
|
||||
{ duration: '10s', target: 200 },
|
||||
{ duration: '30s', target: 10 }
|
||||
],
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
let response = http.get(`${BASE_URL}/actuator/health`);
|
||||
|
||||
check(response, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
'has UP status': (r) => r.json('status') === 'UP',
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown() {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"scenarios": {
|
||||
"baseline": {
|
||||
"executor": "constant-vus",
|
||||
"vus": 10,
|
||||
"duration": "30s"
|
||||
},
|
||||
"stress_test": {
|
||||
"executor": "ramping-vus",
|
||||
"startVUs": 10,
|
||||
"stages": [
|
||||
{ "duration": "1m", "target": 50 },
|
||||
{ "duration": "2m", "target": 100 },
|
||||
{ "duration": "1m", "target": 50 },
|
||||
{ "duration": "1m", "target": 10 }
|
||||
]
|
||||
},
|
||||
"spike_test": {
|
||||
"executor": "ramping-vus",
|
||||
"startVUs": 10,
|
||||
"stages": [
|
||||
{ "duration": "30s", "target": 10 },
|
||||
{ "duration": "10s", "target": 200 },
|
||||
{ "duration": "30s", "target": 10 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"thresholds": {
|
||||
"http_req_duration": [
|
||||
{ "target": "p(95)<500", "abortOnFail": true }
|
||||
],
|
||||
"http_req_failed": [
|
||||
{ "target": "rate<0.05", "abortOnFail": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||
const TEST_DURATION = __ENV.DURATION || '30s';
|
||||
const VUS = __ENV.VUS || '10';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: 'constant-vus',
|
||||
vus: parseInt(VUS),
|
||||
duration: TEST_DURATION,
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export function setup() {
|
||||
let loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
check(loginRes, {
|
||||
'login successful': (r) => r.status === 200,
|
||||
'has token': (r) => r.json('token') !== undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
token: loginRes.json('token'),
|
||||
};
|
||||
}
|
||||
|
||||
export default function (data) {
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${data.token}`,
|
||||
};
|
||||
|
||||
let responses = http.batch([
|
||||
['GET', `${BASE_URL}/api/users`, null, { headers }],
|
||||
['GET', `${BASE_URL}/api/roles`, null, { headers }],
|
||||
['GET', `${BASE_URL}/api/config`, null, { headers }],
|
||||
['GET', `${BASE_URL}/api/notices`, null, { headers }],
|
||||
['GET', `${BASE_URL}/api/files`, null, { headers }],
|
||||
]);
|
||||
|
||||
responses.forEach((res) => {
|
||||
check(res, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
});
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown(data) {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||
const TEST_DURATION = __ENV.DURATION || '30s';
|
||||
const VUS = __ENV.VUS || '10';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: 'constant-vus',
|
||||
vus: parseInt(VUS),
|
||||
duration: TEST_DURATION,
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
let responses = http.batch([
|
||||
['GET', `${BASE_URL}/actuator/health`, null, null],
|
||||
['GET', `${BASE_URL}/actuator/info`, null, null],
|
||||
]);
|
||||
|
||||
responses.forEach((res) => {
|
||||
check(res, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
});
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown() {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
baseline: {
|
||||
executor: 'constant-vus',
|
||||
vus: 10,
|
||||
duration: '30s',
|
||||
startTime: '0s',
|
||||
},
|
||||
stress_test: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 10,
|
||||
stages: [
|
||||
{ duration: '1m', target: 50 },
|
||||
{ duration: '2m', target: 100 },
|
||||
{ duration: '1m', target: 50 },
|
||||
{ duration: '1m', target: 10 }
|
||||
],
|
||||
startTime: '0s',
|
||||
},
|
||||
spike_test: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 10,
|
||||
stages: [
|
||||
{ duration: '30s', target: 10 },
|
||||
{ duration: '10s', target: 200 },
|
||||
{ duration: '30s', target: 10 }
|
||||
],
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
let response = http.get(`${BASE_URL}/actuator/health`);
|
||||
|
||||
check(response, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
'has UP status': (r) => r.json('status') === 'UP',
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown() {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||
const TEST_DURATION = __ENV.DURATION || '30s';
|
||||
const VUS = __ENV.VUS || '10';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: 'constant-vus',
|
||||
vus: parseInt(VUS),
|
||||
duration: TEST_DURATION,
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
let response = http.get(`${BASE_URL}/actuator/health`);
|
||||
|
||||
check(response, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
'has UP status': (r) => r.json('status') === 'UP',
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown() {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
性能测试基础框架
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
import asyncio
|
||||
import statistics
|
||||
from typing import List, Dict, Any
|
||||
from httpx import AsyncClient
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.slow
|
||||
class PerformanceTest:
|
||||
"""性能测试基类"""
|
||||
|
||||
@pytest.fixture
|
||||
async def perf_client(self, authenticated_client: AsyncClient) -> AsyncClient:
|
||||
"""性能测试客户端"""
|
||||
return authenticated_client
|
||||
|
||||
@pytest.fixture
|
||||
def performance_thresholds(self):
|
||||
"""性能阈值配置"""
|
||||
return {
|
||||
"response_time_p95": 2000, # 95%的请求响应时间应小于2秒
|
||||
"response_time_p99": 5000, # 99%的请求响应时间应小于5秒
|
||||
"error_rate": 0.05, # 错误率应小于5%
|
||||
"throughput_min": 10, # 最小吞吐量(请求/秒)
|
||||
}
|
||||
|
||||
async def measure_request_time(self, client: AsyncClient, method: str,
|
||||
url: str, **kwargs) -> float:
|
||||
"""测量单个请求时间"""
|
||||
start_time = time.time()
|
||||
|
||||
if method.upper() == "GET":
|
||||
response = await client.get(url, **kwargs)
|
||||
elif method.upper() == "POST":
|
||||
response = await client.post(url, **kwargs)
|
||||
elif method.upper() == "PUT":
|
||||
response = await client.put(url, **kwargs)
|
||||
elif method.upper() == "DELETE":
|
||||
response = await client.delete(url, **kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
|
||||
end_time = time.time()
|
||||
response_time = (end_time - start_time) * 1000 # 转换为毫秒
|
||||
|
||||
return response_time
|
||||
|
||||
async def measure_concurrent_requests(self, client: AsyncClient, method: str,
|
||||
url: str, concurrency: int = 10,
|
||||
**kwargs) -> Dict[str, Any]:
|
||||
"""测量并发请求性能"""
|
||||
async def make_request():
|
||||
return await self.measure_request_time(client, method, url, **kwargs)
|
||||
|
||||
start_time = time.time()
|
||||
results = await asyncio.gather(*[make_request() for _ in range(concurrency)])
|
||||
end_time = time.time()
|
||||
|
||||
total_time = (end_time - start_time) * 1000 # 毫秒
|
||||
response_times = results
|
||||
|
||||
return {
|
||||
"concurrency": concurrency,
|
||||
"total_time_ms": total_time,
|
||||
"response_times_ms": response_times,
|
||||
"min_time_ms": min(response_times),
|
||||
"max_time_ms": max(response_times),
|
||||
"avg_time_ms": statistics.mean(response_times),
|
||||
"median_time_ms": statistics.median(response_times),
|
||||
"p95_time_ms": self._percentile(response_times, 95),
|
||||
"p99_time_ms": self._percentile(response_times, 99),
|
||||
"throughput_rps": concurrency / (total_time / 1000),
|
||||
"success_count": len(response_times),
|
||||
}
|
||||
|
||||
def _percentile(self, data: List[float], percentile: float) -> float:
|
||||
"""计算百分位数"""
|
||||
sorted_data = sorted(data)
|
||||
index = int(len(sorted_data) * percentile / 100)
|
||||
return sorted_data[min(index, len(sorted_data) - 1)]
|
||||
|
||||
def assert_performance(self, results: Dict[str, Any], thresholds: Dict[str, Any]):
|
||||
"""断言性能指标"""
|
||||
p95_time = results["p95_time_ms"]
|
||||
p99_time = results["p99_time_ms"]
|
||||
throughput = results["throughput_rps"]
|
||||
|
||||
if p95_time > thresholds["response_time_p95"]:
|
||||
pytest.fail(f"P95响应时间 {p95_time:.2f}ms 超过阈值 {thresholds['response_time_p95']}ms")
|
||||
|
||||
if p99_time > thresholds["response_time_p99"]:
|
||||
pytest.fail(f"P99响应时间 {p99_time:.2f}ms 超过阈值 {thresholds['response_time_p99']}ms")
|
||||
|
||||
if throughput < thresholds["throughput_min"]:
|
||||
pytest.fail(f"吞吐量 {throughput:.2f} rps 低于最小值 {thresholds['throughput_min']} rps")
|
||||
|
||||
logger.info(f"性能测试通过: P95={p95_time:.2f}ms, P99={p99_time:.2f}ms, 吞吐量={throughput:.2f} rps")
|
||||
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.slow
|
||||
class TestAPIPerformance(PerformanceTest):
|
||||
"""API性能测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_list_performance(self, perf_client: AsyncClient, performance_thresholds):
|
||||
"""测试用户列表API性能"""
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/users", concurrency=20
|
||||
)
|
||||
|
||||
self.assert_performance(results, performance_thresholds)
|
||||
logger.info(f"用户列表API性能: {results}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_list_performance(self, perf_client: AsyncClient, performance_thresholds):
|
||||
"""测试角色列表API性能"""
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/roles", concurrency=20
|
||||
)
|
||||
|
||||
self.assert_performance(results, performance_thresholds)
|
||||
logger.info(f"角色列表API性能: {results}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notice_list_performance(self, perf_client: AsyncClient, performance_thresholds):
|
||||
"""测试通知列表API性能"""
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/notices", concurrency=20
|
||||
)
|
||||
|
||||
self.assert_performance(results, performance_thresholds)
|
||||
logger.info(f"通知列表API性能: {results}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_performance(self, perf_client: AsyncClient, performance_thresholds):
|
||||
"""测试搜索API性能"""
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/users/page?keyword=test", concurrency=15
|
||||
)
|
||||
|
||||
self.assert_performance(results, performance_thresholds)
|
||||
logger.info(f"搜索API性能: {results}")
|
||||
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.slow
|
||||
class TestLoadTesting(PerformanceTest):
|
||||
"""负载测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sustained_load(self, perf_client: AsyncClient):
|
||||
"""测试持续负载"""
|
||||
duration_seconds = 30
|
||||
requests_per_second = 5
|
||||
total_requests = duration_seconds * requests_per_second
|
||||
|
||||
response_times = []
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(total_requests):
|
||||
response_time = await self.measure_request_time(
|
||||
perf_client, "GET", "/api/users"
|
||||
)
|
||||
response_times.append(response_time)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed < duration_seconds:
|
||||
sleep_time = max(0, (i + 1) / requests_per_second - elapsed)
|
||||
await asyncio.sleep(max(0, sleep_time))
|
||||
|
||||
avg_time = statistics.mean(response_times)
|
||||
p95_time = self._percentile(response_times, 95)
|
||||
|
||||
logger.info(f"持续负载测试 - 平均响应时间: {avg_time:.2f}ms, P95: {p95_time:.2f}ms")
|
||||
|
||||
assert avg_time < 3000, f"平均响应时间 {avg_time:.2f}ms 超过阈值 3000ms"
|
||||
assert p95_time < 5000, f"P95响应时间 {p95_time:.2f}ms 超过阈值 5000ms"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_spike_load(self, perf_client: AsyncClient):
|
||||
"""测试突发负载"""
|
||||
spike_sizes = [10, 50, 100, 50, 10]
|
||||
|
||||
for spike_size in spike_sizes:
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/users", concurrency=spike_size
|
||||
)
|
||||
|
||||
logger.info(f"突发负载测试 (并发={spike_size}): P95={results['p95_time_ms']:.2f}ms")
|
||||
|
||||
assert results["p95_time_ms"] < 10000, \
|
||||
f"突发负载 {spike_size} 并发时 P95响应时间超时"
|
||||
Reference in New Issue
Block a user