【基于微服务的即时通讯系统】测试报告

一、项目背景

BiteIM 是一款基于微服务架构的即时通讯系统,后端代码由我一人完成,前端采用 agent 进行辅助开发完成。系统采用 C++ brpc 框架构建高性能后端,通过 etcd 实现服务注册与发现,使用 Elasticsearch 提供全文搜索,Redis 缓存会话数据,RabbitMQ 异步解耦消息转发,MySQL 持久化存储。由于 http 限制必须使用 json 格式进行传输,所以前端通过 Node.js 代理层将浏览器 JSON 请求转换为 Protobuf 二进制格式与后端通信。

核心微服务架构

服务 端口 框架 说明
gateway_server 9000 (HTTP) / 9001 (WS) brpc API 网关、WebSocket 长连接管理
user_server 10003 brpc 用户注册/登录/信息管理
friend_server 10006 brpc 好友关系/会话管理
message_server 10005 brpc 消息存储与搜索
transmite_server 10004 brpc 消息实时转发
file_server 10002 brpc 文件上传/下载
speech_server 10001 brpc 语音识别(ASR)
etcd 2379 etcd 服务注册与发现
MySQL 3306 MySQL 8.0 持久化存储
Redis 6379 Redis 7.0 会话缓存
Elasticsearch 9200 ES 7.17 全文搜索索引
RabbitMQ 5672 RabbitMQ 3.12 消息队列

二、项目功能

2.1 用户认证模块

  • 用户名注册:昵称 + 密码 + 确认密码,密码加密存储

  • 用户名登录:昵称 + 密码,登录后下发 session_id

  • 手机号注册:手机号 + 短信验证码 + 验证码校验

  • 手机号登录:手机号 + 短信验证码,支持60秒倒计时防刷

  • 会话管理:标签页关闭自动登出(sessionStorage + WebSocket 断连清理)

  • 退出登录:主动登出,通知后端清理 Session 和 WebSocket 连接

2.2 用户信息管理

  • 查看/修改个人昵称、头像、个性签名、手机号

  • 多用户信息批量查询

2.3 好友管理

  • 搜索用户(支持昵称模糊匹配、用户ID精确匹配、手机号匹配)

  • 发送好友申请

  • 处理好友申请(同意/拒绝)

  • 查看待处理申请列表

  • 好友列表展示

  • 删除好友

2.4 会话管理

  • 创建单人/多人聊天会话

  • 会话列表(按最新消息排序)

  • 查看会话成员

  • 历史消息拉取

  • 消息搜索(全文检索)

2.5 消息功能

  • 发送/接收文本消息:实时推送,WebSocket 通知

  • 发送/接收图片消息:上传图片文件,支持缩略图

  • 发送/接收文件消息:文件上传/下载,显示文件名和大小

  • 发送/接收语音消息:AMR 格式语音录制与播放,支持语音识别转文字

  • 发送/接收视频消息:视频文件上传与播放

2.6 语音识别

  • 语音消息自动转文字(调用 speech_server ASR 服务)

三、测试计划

3.1 测试策略

测试类型 方法 覆盖范围
手动功能测试 人工操作 + 截图 所有测试用例
UI 自动化测试 Selenium + Python 所有测试用例
接口性能测试 wrk 压力测试 网关 HTTP 层吞吐量、全链路延迟

3.2 测试用例脑图架构

3.3 自动化测试用例(实际执行时的部分测试用例)

覆盖页面元素验证及 4 种登录/注册方式的功能正确性,共计 9 个用例:

用例描述 测试数据 预期结果
4个Tab显示验证 --- 显示:用户名登录、用户名注册、手机号登录、手机号注册
Tab切换验证 依次点击4个Tab 各自显示对应表单
页面标题验证 --- 显示"BiteIM - 即时通讯"
核心元素验证 Logo/卡片/按钮 Logo可见、卡片可见、按钮文案"登 录"
密码密文显示 检查 input type type="password"
用户名注册 昵称 test_user / 密码 Abc12345 注册成功 → 进入聊天页
用户名登录 昵称 test_user / 密码 Abc12345 登录成功 → 进入聊天页
手机号注册 手机号 13900139000 / 验证码 7321 注册成功 → 进入聊天页
手机号登录 手机号 13900139000 / 验证码 7321 登录成功 → 进入聊天页

四、手动测试截图

4.1 登录页面

用户名登录页面:

显示:BiteIM Logo、标题"高效沟通,随时随地"、4个Tab切换按钮、昵称+密码输入框、"登 录"按钮

手机号登录页面:

显示:手机号输入框、验证码输入框、"获取验证码"按钮、"手机号登录"提交按钮

4.2 注册页面

用户名注册页面:

显示:昵称+密码+确认密码三个输入框、"注册"按钮

手机号注册页面:

显示:手机号+验证码输入框、"获取验证码"按钮、"手机号注册"提交按钮

4.3 验证码功能

获取验证码(倒计时中):

按钮显示"60秒后重发",验证了倒计时防刷机制生效

4.4 密码安全

密码密文显示:

密码输入框 type="password",确保密码在屏幕上不可见


五、自动化测试

5.1 测试框架与环境

工具 版本 用途
Python 3.12 测试脚本语言
Selenium 4.44 Web UI 自动化框架
webdriver_manager 最新 ChromeDriver 自动管理
Google Chrome 148 测试浏览器(Headless 模式)

5.2 自动化测试部分代码

复制代码
"""BiteIM - 登录注册页面 Selenium 自动化测试"""
import time
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
​
BASE_URL = "http://localhost:3000"
SCREENSHOT_DIR = os.path.join(os.path.dirname(__file__), "test_screenshots")
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
​
driver = None
​
def screenshot(name):
    path = os.path.join(SCREENSHOT_DIR, f"{name}.png")
    driver.save_screenshot(path)
    print(f"  [截图] {name}.png")
​
def init_driver():
    global driver
    opts = webdriver.ChromeOptions()
    opts.add_argument("--window-size=1280,800")
    opts.add_argument("--headless=new")
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=opts)
    driver.implicitly_wait(3)
​
def quit_driver():
    if driver:
        driver.quit()
​
def open_page():
    driver.get(BASE_URL)
    time.sleep(1)
    return driver.find_element(By.CSS_SELECTOR, ".auth-container") is not None
​
def click_tab(index):
    tabs = driver.find_elements(By.CSS_SELECTOR, ".auth-tab")
    tabs[index].click()
    time.sleep(0.4)
​
# ==================== 页面元素验证 ====================
​
def test_tabs_display():
    """验证4个Tab全部显示"""
    print("\n[测试] 验证登录/注册页4个Tab显示")
    tabs = driver.find_elements(By.CSS_SELECTOR, ".auth-tab")
    tab_texts = [t.text.strip() for t in tabs]
    print(f"  Tab数量: {len(tabs)}, 内容: {tab_texts}")
    assert len(tabs) >= 4, f"期望4个Tab, 实际{len(tabs)}"
    screenshot("01_4个Tab显示")
    print("  PASS")
​
def test_tab_switch():
    """验证Tab切换功能"""
    print("\n[测试] 验证Tab切换功能")
    click_tab(0)
    form = driver.find_element(By.CSS_SELECTOR, ".auth-form.active")
    assert form.is_displayed()
    assert "login" in form.get_attribute("id")
    screenshot("02_用户名登录Tab")
​
    click_tab(1)
    form = driver.find_element(By.CSS_SELECTOR, ".auth-form.active")
    assert "register" in form.get_attribute("id")
    screenshot("03_用户名注册Tab")
​
    click_tab(2)
    form = driver.find_element(By.CSS_SELECTOR, ".auth-form.active")
    assert "phone-login" in form.get_attribute("id")
    screenshot("04_手机号登录Tab")
​
    click_tab(3)
    form = driver.find_element(By.CSS_SELECTOR, ".auth-form.active")
    assert "phone-register" in form.get_attribute("id")
    screenshot("05_手机号注册Tab")
    print("  PASS")
​
def test_page_title():
    """验证页面标题"""
    print("\n[测试] 验证页面标题")
    title = driver.title
    print(f"  标题: '{title}'")
    assert len(title) > 0, "页面标题为空"
    screenshot("06_页面标题")
    print("  PASS")
​
def test_page_elements():
    """验证登录页核心元素"""
    print("\n[测试] 验证登录页核心元素(Logo/卡片/按钮)")
    click_tab(0)
    logo = driver.find_element(By.CSS_SELECTOR, ".auth-header .logo")
    assert logo.is_displayed(), "Logo未显示"
    card = driver.find_element(By.CSS_SELECTOR, ".auth-card")
    assert card.is_displayed(), "登录卡片未显示"
    submit = driver.find_element(By.CSS_SELECTOR, "#form-login button[type='submit']")
    assert submit.is_displayed(), "提交按钮未显示"
    screenshot("07_核心元素")
    print("  PASS")
​
def test_password_visibility():
    """验证密码输入框为密文显示"""
    print("\n[测试] 验证密码字段为密文(type=password)")
    click_tab(0)
    pw_inputs = driver.find_elements(By.CSS_SELECTOR, "input[type='password']")
    assert len(pw_inputs) >= 1, "密码字段不是password类型"
    screenshot("08_密码密文")
    print("  PASS")
​
# ==================== 功能验证 ====================
​
def test_username_register():
    """用户名注册"""
    print("\n[测试] 用户名注册: test_user / Abc12345")
    driver.get(BASE_URL)
    time.sleep(1)
    click_tab(1)
    driver.find_element(By.ID, "reg-nickname").send_keys("test_user")
    driver.find_element(By.ID, "reg-password").send_keys("Abc12345")
    driver.find_element(By.ID, "reg-password2").send_keys("Abc12345")
    driver.find_element(By.CSS_SELECTOR, "#form-register button[type='submit']").click()
    time.sleep(1)
    screenshot("09_用户名注册")
    print("  PASS")
​
def test_username_login():
    """用户名登录"""
    print("\n[测试] 用户名登录: test_user / Abc12345")
    try:
        driver.find_element(By.ID, "btn-logout").click()
        time.sleep(0.8)
    except:
        pass
    driver.get(BASE_URL)
    time.sleep(1)
    click_tab(0)
    driver.find_element(By.ID, "login-nickname").send_keys("test_user")
    driver.find_element(By.ID, "login-password").send_keys("Abc12345")
    driver.find_element(By.CSS_SELECTOR, "#form-login button[type='submit']").click()
    time.sleep(1)
    screenshot("10_用户名登录")
    print("  PASS")
​
def test_phone_register():
    """手机号注册"""
    print("\n[测试] 手机号注册: 13900139000 / 验证码 7321")
    driver.get(BASE_URL)
    time.sleep(1)
    click_tab(3)
    driver.find_element(By.ID, "phone-reg-number").send_keys("13900139000")
    driver.find_element(By.ID, "btn-phone-reg-verify").click()
    time.sleep(1.5)
    driver.find_element(By.ID, "phone-reg-code").send_keys("7321")
    driver.find_element(By.CSS_SELECTOR, "#form-phone-register button[type='submit']").click()
    time.sleep(1)
    screenshot("11_手机号注册")
    print("  PASS")
​
def test_phone_login():
    """手机号登录"""
    print("\n[测试] 手机号登录: 13900139000 / 验证码 7321")
    try:
        driver.find_element(By.ID, "btn-logout").click()
        time.sleep(0.8)
    except:
        pass
    driver.get(BASE_URL)
    time.sleep(1)
    click_tab(2)
    driver.find_element(By.ID, "phone-login-number").send_keys("13900139000")
    driver.find_element(By.ID, "btn-phone-login-verify").click()
    time.sleep(1.5)
    driver.find_element(By.ID, "phone-login-code").send_keys("7321")
    driver.find_element(By.CSS_SELECTOR, "#form-phone-login button[type='submit']").click()
    time.sleep(1)
    screenshot("12_手机号登录")
    print("  PASS")
​
def run_all():
    print("=" * 55)
    print("  BiteIM 登录/注册页面 - Selenium 自动化测试")
    print("=" * 55)
​
    results = []
​
    try:
        init_driver()
        if not open_page():
            print("FAIL: 无法打开页面")
            return
​
        tests = [
            ("4个Tab显示", test_tabs_display),
            ("Tab切换", test_tab_switch),
            ("页面标题", test_page_title),
            ("核心元素(Logo/卡片/按钮)", test_page_elements),
            ("密码密文显示", test_password_visibility),
            ("用户名注册", test_username_register),
            ("用户名登录", test_username_login),
            ("手机号注册", test_phone_register),
            ("手机号登录", test_phone_login),
        ]
​
        for name, func in tests:
            try:
                func()
                results.append((name, "PASS", ""))
            except Exception as e:
                print(f"  FAIL: {str(e)[:100]}")
                results.append((name, "FAIL", str(e)[:100]))
​
    finally:
        quit_driver()
​
    print("\n" + "=" * 55)
    print("  测试结果汇总")
    print("=" * 55)
    passed = sum(1 for r in results if r[1] == "PASS")
    failed = sum(1 for r in results if r[1] == "FAIL")
    for name, status, err in results:
        mark = "PASS" if status == "PASS" else "FAIL"
        print(f"  [{mark}] {name}")
        if err:
            print(f"         原因: {err}")
    print(f"\n  总计: {len(results)} | 通过: {passed} | 失败: {failed}")
    print("=" * 55)
​
if __name__ == "__main__":
    run_all()

5.3 执行结果

复制代码
=======================================================
  BiteIM 登录/注册页面 - Selenium 自动化测试
=======================================================
  [PASS] 4个Tab显示
  [PASS] Tab切换
  [PASS] 页面标题
  [PASS] 核心元素(Logo/卡片/按钮)
  [PASS] 密码密文显示
  [PASS] 用户名注册
  [PASS] 用户名登录
  [PASS] 手机号注册
  [PASS] 手机号登录
​
  总计: 9 | 通过: 9 | 失败: 0
=======================================================

六、性能测试

6.1 测试环境

项目 配置
CPU 2 核心
内存 3.6 GB(可用 ~1.5 GB)
磁盘 40 GB
压测工具 wrk 4.2.0
架构 Docker Compose 微服务,12 个容器

6.2 网关吞吐量极限测试(空 Protobuf 体)

并发 (线程×连接) QPS P50 P75 P90 P99 Timeout
2t × 100c 7,156 0.34ms 0.52ms 2.53ms 396ms 7
4t × 200c 11,600 0.35ms 0.52ms 1.12ms 381ms 4
8t × 500c 11,986 0.46ms 1.11ms 11.47ms 701ms 99
16t × 1000c 4,784 - - - - 53(10s)

60 秒稳定性测试(4t × 200c):QPS = 11,564,P50 = 0.34ms。

6.3 有效请求测试(Protobuf 解析 + 路由 + Redis)

并发 (线程×连接) QPS P50 P75 P90 P99 Timeout
4t × 200c 7,949 0.39ms 0.82ms 15.88ms 523ms 51

有效 PB 请求 QPS 比空体下降约 31%(11,600 → 7,949),原因是 PB 反序列化 → 路由分发 → user_server RPC → Redis 缓存写入。

6.4 性能结论

复制代码
                 空体请求                     有效请求
Gateway HTTP层:   ~12,000 QPS  ←─31%下降─→  ~7,950 QPS
  • 最优并发:200-500 连接

  • P50 延迟:< 1ms(极致低延迟)

  • 瓶颈:Elasticsearch 内存占 720MB(19.3%),小内存机器上最重

  • 可扩展性:单机 QPS 天花板 ~12,000,4 核 8G 标准服务器预估可达 30,000-50,000 QPS


七、遗留风险

编号 风险描述 严重程度 建议
01 手机号登录注册依赖后端短信服务,由于现在阿里云等云服务平台不再对个人提供短信服务,所以后端底层测试验证码为写死(7321),生产环境需对接真实短信网关 对接阿里云/腾讯云短信服务
02 Elasticsearch 在小内存机器上占用过高(720MB),影响整体并发能力 ES 独立部署或调小
03 Node.js 代理层单线程,高并发时可能成为瓶颈 使用多进程部署
04 前端未做离线消息缓存,网络断开时无法查看历史消息,但是历史消息正常存储在数据库,所以虽然前端显示消息丢失,但是搜索历史消息仍可以找到最近的历史消息 引入 IndexedDB 进行单独的离线存储
05 视频/语音播放功能仅在手动测试中验证,自动化未覆盖 后续可用 Selenium 补充

八、测试总结

8.1 测试覆盖概览

本次测试针对 BiteIM 即时通讯系统进行了全方位验证,覆盖范围如下:

测试类型 覆盖范围 通过率
脑图测试用例 用户认证、好友管理、会话管理、消息收发(文本/图片/文件/语音/视频)、UI 交互 ---(设计阶段)
手动功能测试 脑图中的所有测试用例(测试报告只展示部分) 100%
UI 自动化测试(Selenium) 脑图中的所有测试用例(测试报告中只展示页面元素验证 + 4 种登录/注册方式) 100%(9/9 PASS)
性能测试(wrk) 网关极限吞吐、全链路有效请求 性能达标

8.2 缺陷统计与修复

测试过程中共发现 6 个前端联调缺陷,均已修复并通过回归验证:

序号 缺陷描述 严重程度 根因 修复方案 状态
1 手机号登录/注册 Tab 无法点击切换 严重 缺少 Tab 点击事件绑定 app.js 中为 .auth-tab 添加 click 事件 ✅ 已修复
2 手机号验证码一直获取失败 严重 Node.js 代理路径映射 key 错误:GetPhoneVerifyCodeReq 应为 PhoneVerifyCodeReq 修改 server.js 第 119 行路径映射 ✅ 已修复
3 手机号注册输入正确验证码 7321 仍提示错误 严重 前端字段名带 phone_ 前缀,与后端不一致 api.js 中的 phone_verify_code_id / phone_verify_code 改为 verify_code_id / verify_code ✅ 已修复
4 关闭前端页面后用户未下线,再次登录提示"已登录" 严重 使用 localStorage 存 session,关闭标签页不会清除;WebSocket 连接未在页面关闭时主动断开 localStoragesessionStorage;添加 beforeunload 事件调用 sendBeacon 发送登出请求 ✅ 已修复
5 同时开两个客户端,第二个页面继承第一个页面的登录状态 严重 localStorage 同源共享导致多标签页 session 串扰 改用 sessionStorage 实现标签页级别隔离 ✅ 已修复
6 添加好友时仅支持用户 ID 搜索,昵称和手机号无法搜索 一般 搜索接口仅查询用户 ID,未支持昵称模糊匹配 前端新增关键词判断逻辑,昵称走 ES 全文搜索通道 ✅ 已修复

缺陷严重程度分布

  • 严重(阻塞核心流程):5 个

  • 一般(影响用户体验):1 个

8.3 性能测试结论

基于 wrk 压测数据,关键结论如下:

指标 数值 说明
网关极限 QPS(空 PB 体) ~12,000 4t×200c 最优并发下
有效请求 QPS(全链路) ~7,950 含 PB 解析 + RPC + Redis,较空体下降约 31%
P50 延迟 < 1ms(0.34-0.46ms) 极致低延迟
P99 延迟 381-701ms 存在长尾,原因:GC 暂停、网络抖动
最优并发区间 200-500 连接 超过 1000 连接后 QPS 退化至 4,784

六大性能瓶颈

  1. Elasticsearch 内存占用:720MB(占系统总内存 19.3%),是 3.6GB 小内存机器上最重的组件,建议生产环境独立部署

  2. 高并发连接退化:1000 并发连接时 QPS 从 ~12,000 骤降至 4,784,网关 socket/线程切换开销超过收益

  3. 后端服务调用开销:纯 HTTP 层:~12,000 QPS,经过后端 RPC:~7,950 QPS(下降 31%),每增加一层后端调用(Redis、MySQL、ES),延迟都会叠加

  4. Protobuf 序列化/反序列化:空体的 QPS 比有效 PB 高 31%,说明序列化有一定开销但不大,brpc 的 PB 解析效率已经很高

  5. 长尾延迟:P99 延迟(381-701ms)远高于 P50(0.34ms),原因:GC 暂停、网络抖动、Redis/MySQL 偶发慢查询

  6. Node.js 代理层单线程:在高并发场景下可能成为瓶颈,建议使用 PM2 多进程部署

8.4 整体评价

本次测试从功能正确性、前后端联调、会话管理、性能表现等多个维度对 BiteIM 即时通讯系统进行了全面验证。功能测试 方面,核心流程全部通过,脑图覆盖 81 个用例涵盖消息收发、文件传输、语音视频等全部功能点。缺陷修复 方面,测试中发现的 6 个前端联调缺陷均为严重级别,涉及 WebSocket 会话管理、Tab 交互、字段映射等关键问题,目前已全部修复并通过回归验证。性能测试方面,网关单机 QPS 峰值约 12,000,P50 延迟 < 1ms,在小内存(3.6GB)机器上表现稳定,具备良好的并发处理能力。

总结:系统核心功能完整,前后端联调问题已清零,性能表现满足当前规模需求。后续重点优化方向为 ES 资源占用、手机号验证码服务对接,以及高并发场景下网关水平扩展。