基于AI的代码仓库搜索引擎--测试报告
一.测试目标及测试任务概括
1.项目背景
本项目是一个基于 AI 的代码仓库搜索引擎,通过使用官方Gitee Open API 实现海量开源仓库数据的自动化采集,核心技术包括 Jieba分词构建倒排索引用于关键词检索、text2vec-base-chinese 大模型 + FAISS 实现语义向量搜索,前端基于 React + MUI构建,后端采用 Spring Boot + Python 双服务架构。搜索流程为"倒排索引精确召回 → 语义向量泛化匹配 → 双路取交集 → 数据库回表",最终向用户提供兼顾速度与相关性的代码仓库搜索体验。
2.系统设计
系统划分为四大层:
①数据采集层:通过 Gitee Open API 自动分页抓取用户/组织维度的仓库信息,包括仓库全名、HTML 访问地址及 README文档内容,支持单次批量 INSERT 持久化至 MySQL。
②索引构建层:包含倒排索引与向量索引。倒排索引基于 Jieba 分词引擎对 README文本进行中文分词,配合停用词过滤后构建磁盘级索引文件。向量索引由 Python 侧独立构建,使用 text2vec-base-chinese 将逐条 README 编码为语义向量,存入 FAISS 索引并建立 ID 映射表,供在线语义搜索调用。
③搜索服务层:核心搜索逻辑位 Spring Boot 的 IndexService,采用"关键词 +语义"双路合并策略——倒排索引按词频评分排序快速出 ID 候选集(上限 2000),Python 服务返回 FAISS 向量相似度排序的 ID列表,两路求交集后通过数据库回表查询完整仓库信息,最终返回前端展示。
④前端交互层:React + Material UI构建单页搜索界面,支持搜索框输入、回车触发、关键词红色高亮、分页浏览及结果链接跳转,通过 http-proxy-middleware代理请求至 Java 后端。
3.技术栈
-
后端:Spring Boot、MyBatis-Plus、MyBatis、Jieba 分词、text2vec-base-chinese
-
数据库:MySQL
二.测试相关信息
1.测试配置
| 类别 | 配置/版本 |
| 硬件 | Legion R7000P |
| 手动测试浏览器 | edge 148.0.3967.70 (正式版本) (64 位) |
| 开发工具 | IntelliJ IDEA 2025.1.7 |
| 自动化测试工具 | Selenium 4.21.0 |
| 操作系统 | Windows 10 家庭版 |
| 自动化测试浏览器 | chrome 148.0.7778.97(正式版本) (64 位) |
2.测试用例

三、自动化测试
1.公共类 Utils
/**
* 自动化测试基类:封装公共方法(驱动创建、等待、截图)
* 全局只创建一个 WebDriver 实例,所有测试类共享
*/
public class BaseTest {
protected static WebDriver driver;
protected static WebDriverWait wait;
private static boolean initialized = false;
protected static final String BASE_URL = "http://localhost:3000";
protected static final String SCREENSHOT_DIR = "target/screenshots";
// 直接使用已有驱动,跳过 WebDriverManager 下载
private static final String CHROME_DRIVER_PATH =
"C:/Users/Pikaqi/.cache/selenium/chromedriver/win64/148.0.7778.97/chromedriver.exe";
@BeforeAll
public static void setUp() {
if (!initialized) {
System.setProperty("webdriver.chrome.driver", CHROME_DRIVER_PATH);
ChromeOptions options = new ChromeOptions();
options.addArguments("--remote-allow-origins=*");
options.addArguments("--disable-gpu");
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
driver = new ChromeDriver(options);
driver.manage().window().maximize();
// 隐式等待:全局最多等10秒
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
// 显式等待对象:最长10秒,每500ms轮询一次
wait = new WebDriverWait(driver, Duration.ofSeconds(10), Duration.ofMillis(500));
// JVM 退出时自动关闭浏览器
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (driver != null) {
driver.quit();
}
}));
initialized = true;
System.out.println("=== Chrome Driver 初始化完成 ===");
}
}
@AfterAll
public static void tearDown() {
if (driver != null) {
driver.quit();
driver = null;
initialized = false;
System.out.println("=== Chrome Driver 已关闭 ===");
}
}
// ==================== 等待方法 ====================
/**
* 强制等待(线程休眠)
*/
protected void forceWait(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
//wait.until是智能等待:它每隔 0.5 秒去检查一次条件是否满足,一旦满足(比如元素出现了),立刻执行下一步
/**
* 显式等待:元素可见
*/
protected WebElement waitForElementVisible(By by) {
return wait.until(ExpectedConditions.visibilityOfElementLocated(by));
}
/**
* 显式等待:元素可点击
*/
protected WebElement waitForElementClickable(By by) {
return wait.until(ExpectedConditions.elementToBeClickable(by));
}
/**
* 显式等待:页面标题包含指定文本
*/
protected boolean waitForTitleContains(String title) {
return wait.until(ExpectedConditions.titleContains(title));
}
// ==================== 屏幕截图 ====================
/**
* 截图保存到 target/screenshots/
*/
protected void takeScreenshot(String filename) {
try {
File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
Path dir = Paths.get(SCREENSHOT_DIR);
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
Path dest = dir.resolve(filename + "_" + ts + ".png");
Files.copy(src.toPath(), dest);
System.out.println("截图已保存: " + dest);
} catch (IOException e) {
System.err.println("截图失败: " + e.getMessage());
}
}
}
2.页面搜索功能
/**
* 主搜索页面测试用例
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class SearchPageTest extends BaseTest {
// 搜索结果计数的 XPath:<h6>找到 X 个结果</h6>
private static final String RESULT_COUNT_XPATH = "//*[@id='root']/div/h6";
@Test
@Order(1)
@DisplayName("T001 - 页面加载:标题与搜索组件存在")
public void testPageLoad() {
driver.get(BASE_URL);
forceWait(1000);
assertTrue(driver.getTitle().contains("pikaqi"), "页面标题应包含 pikaqi");
WebElement btn = waitForElementVisible(By.xpath("//button[contains(text(),'搜索')]"));
assertNotNull(btn, "搜索按钮应存在");
WebElement input = driver.findElement(By.tagName("input"));
assertNotNull(input, "搜索输入框应存在");
}
@Test
@Order(2)
@DisplayName("T002 - 空搜索弹窗:不输入关键词点搜索,提示请输入关键词")
public void testEmptySearchShowsWarning() {
driver.get(BASE_URL);
WebElement btn = driver.findElement(By.xpath("//button[contains(text(),'搜索')]"));
btn.click();
// 等待 MUI Snackbar 弹出
WebElement alert = waitForElementVisible(
By.xpath("//*[contains(text(),'请输入关键词进行搜索')]"));
assertNotNull(alert, "空搜索应弹出提示:请输入关键词进行搜索");
// 等待 Snackbar 入场动画完成再截图
forceWait(300);
takeScreenshot("empty_search_warning");
}
@Test
@Order(3)
@DisplayName("T003 - 正常关键词搜索:结果数大于 0")
public void testNormalSearch() {
driver.get(BASE_URL);
WebElement input = waitForElementVisible(By.tagName("input"));
input.clear();
input.sendKeys("Java");
WebElement btn = driver.findElement(By.xpath("//button[contains(text(),'搜索')]"));
btn.click();
// 显式等待结果计数出现
WebElement countLabel = waitForElementVisible(By.xpath(RESULT_COUNT_XPATH));
String text = countLabel.getText(); // 格式:"找到 X 个结果"
int count = Integer.parseInt(text.replaceAll("[^0-9]", ""));
assertTrue(count > 0, "搜索结果数应大于 0,实际: " + count);
takeScreenshot("search_java");
}
@Test
@Order(4)
@DisplayName("T004 - 回车键触发搜索")
public void testEnterKeySearch() {
driver.get(BASE_URL);
WebElement input = waitForElementVisible(By.tagName("input"));
input.clear();
input.sendKeys("Python");
input.sendKeys(Keys.ENTER);
WebElement countLabel = waitForElementVisible(By.xpath(RESULT_COUNT_XPATH));
assertNotNull(countLabel, "回车搜索后应显示结果计数");
String text = countLabel.getText();
assertTrue(text.contains("找到"), "应显示'找到 X 个结果'");
}
@Test
@Order(5)
@DisplayName("T005 - 搜索结果包含 Gitee 链接")
public void testResultContainsGiteeLink() {
driver.get(BASE_URL);
WebElement input = waitForElementVisible(By.tagName("input"));
input.clear();
input.sendKeys("Spring");
driver.findElement(By.xpath("//button[contains(text(),'搜索')]")).click();
// 等待结果加载
forceWait(2000);
List<WebElement> links = driver.findElements(By.cssSelector("a[href*='gitee.com']"));
assumeFalse(links.isEmpty(), "跳过:当前无搜索结果");
assertTrue(links.get(0).getAttribute("href").contains("gitee.com"),
"结果链接应指向 Gitee");
}
@Test
@Order(6)
@DisplayName("T006 - 搜索无结果关键词,计数为 0")
public void testSearchNoResult() {
driver.get(BASE_URL);
WebElement input = waitForElementVisible(By.tagName("input"));
input.clear();
input.sendKeys("xyznonexistentkeyword12345");
driver.findElement(By.xpath("//button[contains(text(),'搜索')]")).click();
WebElement countLabel = waitForElementVisible(By.xpath(RESULT_COUNT_XPATH));
String text = countLabel.getText();
assertTrue(text.contains("0 个结果") || text.contains("0"),
"无结果时应显示 0,实际: " + text);
takeScreenshot("search_no_result");
}
@Test
@Order(7)
@DisplayName("T007 - XSS 防护:搜索 script 标签不触发 alert 弹窗")
public void testSearchXSSProtection() {
driver.get(BASE_URL);
WebElement input = waitForElementVisible(By.tagName("input"));
input.clear();
input.sendKeys("<script>alert(1)</script>");
driver.findElement(By.xpath("//button[contains(text(),'搜索')]")).click();
forceWait(2000);
// 尝试切换到原生 JS alert 弹窗,如果存在则说明有 XSS 漏洞
try {
driver.switchTo().alert();
fail("存在 XSS 漏洞:页面弹出了 alert 弹窗");
} catch (NoAlertPresentException e) {
// 预期:没有弹窗 = 没有 XSS
}
}
@Test
@Order(8)
@DisplayName("T008 - 分页切换:搜索 C语言 结果 > 10 时切换至第 2 页")
public void testPagination() {
driver.get(BASE_URL);
WebElement input = waitForElementVisible(By.tagName("input"));
input.clear();
input.sendKeys("C语言");
driver.findElement(By.xpath("//button[contains(text(),'搜索')]")).click();
// 等待结果加载
WebElement countLabel = waitForElementVisible(By.xpath(RESULT_COUNT_XPATH));
String countText = countLabel.getText();
int count = Integer.parseInt(countText.replaceAll("[^0-9]", ""));
assumeFalse(count <= 10, "跳过:结果数不足 10,无需分页");
// 卡片内的 h6 是仓库名,与顶部的计数 h6 不同
String firstCardH6 = "(//div[contains(@class,'MuiCard')]//h6)[1]";
// 记录第 1 页第一个仓库名
WebElement firstCardTitle = driver.findElement(By.xpath(firstCardH6));
String page1FirstTitle = firstCardTitle.getText();
assertFalse(page1FirstTitle.isEmpty(), "第 1 页应有仓库名");
// 点击分页组件中的第 2 页按钮
WebElement page2Btn = driver.findElement(By.xpath("//button[@aria-label='Go to page 2']"));
page2Btn.click();
forceWait(1000);
// 验证第 2 页第一个仓库名与第 1 页不同
WebElement page2FirstTitle = driver.findElement(By.xpath(firstCardH6));
String page2FirstTitleText = page2FirstTitle.getText();
assertFalse(page2FirstTitleText.isEmpty(), "第 2 页应有仓库名");
assertNotEquals(page1FirstTitle, page2FirstTitleText,
"切换分页后展示内容应不同");
takeScreenshot("pagination_page2");
}
}

3.跳转页面
/**
* 仓库链接跳转页面测试用例
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class LinkNavigationTest extends BaseTest {
@Test
@Order(1) //控制测试执行顺序
@DisplayName("T001 - 搜索结果链接点击跳转至 Gitee") //
public void testLinkJumpsToGitee() {
driver.get(BASE_URL);
// 搜索关键词
WebElement input = waitForElementVisible(By.tagName("input"));
input.clear();
input.sendKeys("Java");
input.sendKeys(Keys.ENTER);
forceWait(2000);
// 查找 Gitee 链接
List<WebElement> links = driver.findElements(By.cssSelector("a[href*='gitee.com']"));
assumeFalse(links.isEmpty(), "跳过:无搜索结果,无法验证链接跳转");
String originalWindow = driver.getWindowHandle();
WebElement firstLink = links.get(0);
firstLink.click();
// 切换到新标签页
forceWait(1000);
List<String> windows = new ArrayList<>(driver.getWindowHandles());
assertTrue(windows.size() > 1, "点击链接应打开新标签页");
driver.switchTo().window(windows.get(1));
String currentUrl = driver.getCurrentUrl();
assertTrue(currentUrl.contains("gitee.com"), "跳转后 URL 应包含 gitee.com,实际: " + currentUrl);
takeScreenshot("gitee_page");
driver.close();
driver.switchTo().window(originalWindow);
}
@Test
@Order(2)
@DisplayName("T002 - 搜索结果链接在新标签页打开")
public void testLinkOpensInNewTab() {
driver.get(BASE_URL);
WebElement input = waitForElementVisible(By.tagName("input"));
input.clear();
input.sendKeys("Spring");
input.sendKeys(Keys.ENTER);
forceWait(2000);
List<WebElement> links = driver.findElements(By.cssSelector("a[href*='gitee.com']"));
assumeFalse(links.isEmpty(), "跳过:无搜索结果,无法验证链接属性");
String target = links.get(0).getAttribute("target");
assertEquals("_blank", target, "外部链接 target 应为 _blank");
String rel = links.get(0).getAttribute("rel");
assertNotNull(rel, "外部链接应有 rel 属性");
assertTrue(rel.contains("noopener"), "rel 应包含 noopener,实际: " + rel);
}
}
四、性能测试
本次测试采用 JMeter 的 Stepping Thread Group(梯度压测线程组)对系统的 搜索接口 进行压力测试。测试重点在于观察系统在高并发递增场景下的性能拐点、吞吐量极限以及响应稳定性。
1.线程组设置
压测模型配置
-
初始活跃线程:10 个并发线程。
-
递增步长:每 2 秒增加 2 个线程。
-
最大并发负载:10 个线程,持续压测 30 秒后逐步释放。
-
总样本数:累计发送请求 1,078 次。

2.响应时间
响应时间随负载变化的趋势
-
低负载阶段(0~13秒):并发线程处于 1~4 个时,接口响应时间极快,保持在 280ms - 340ms 之间。
-
负载爬升与峰值阶段(13~60秒):随着并发线程逐步爬升并维持在 10 个满载状态,由于服务器 CPU 算力(向量比对与文本分词)以及数据库连接池压力上升,响应时间呈阶梯状缓慢上扬,最终在 480ms - 640ms 之间发生周期性震荡。
-
释放阶段(60秒后):随着线程释放,响应时间瞬间回落至 280ms 状态。

3.吞吐量
吞吐量(QPS)吞吐能力分析
-
爬升期:前 20 秒内,随着并发用户数的增加,QPS 呈现出线性上升趋势,从 3/sec 飙升至 18/sec。
-
饱和期:在 26~60 秒的满载期间,吞吐量死死稳定在 18/sec - 23/sec 之间窄幅震荡。
-
结论:系统的性能瓶颈大约在 21 QPS 左右。此时系统达到了当前硬件/环境配置下的最大算力输出。

4.聚合报告
| 指标名称 | 测试结果值 | 性能评估 |
| 总样本数 (Samples) | 1,078 次 | 样本量充足 |
| 平均响应时间 (Average) | 463 ms | 整体表现优秀,半秒内返回结果 |
| 90% 百分位数 | 585 ms | 90% 的用户请求在 0.58 秒内完成 |
| 95% 百分位数 | 602 ms | 高并发下长尾延迟控制良好,无严重卡顿 |
| 最大响应时间 (Max) | 685 ms | 最慢的一次请求也在 0.7 秒内,未发生超大延迟 |
| 异常率 (Error %) | 0.00% | 系统表现稳定,无熔断、无报错、无 500 |
| 平均吞吐量 (Throughput) | 16.1 / sec | 系统当前每秒可稳定处理 16.1 次复杂语义搜索 |

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐






所有评论(0)