Xime 测试指南
测试架构
本项目采用多层测试策略,确保代码质量和稳定性。
测试类型
1. 单元测试 (Unit Tests)
- 位置:
app/src/test/java/、plugin-core/src/test/java/ - 目的: 测试纯 Kotlin 逻辑,不依赖 Android 框架
- 适用场景:
- 工具类方法测试
- 业务逻辑测试
- 算法测试
- JS 插件沙箱与适配器测试
示例测试类:
SchemaConfigHelperTest- 测试配置解析逻辑ExtensionManagerTest- 测试插件管理逻辑JsScriptRuntimeTest- 测试 JS 沙箱运行环境JsAsrPluginAdapterTest/JsAsrPluginTest- 测试 ASR 插件适配器JsKaomojiTest/JsMemeBunnyTest/JsVolcAsrPluginTest- 测试真实插件脚本ManifestParseTest- 测试插件 manifest 解析VersionUtilTest- 测试插件宿主版本兼容性检查PluginSignatureUtilTest- 测试插件信任等级判定NetworkPolicyTest- 测试插件网络策略
2. T9 核心 C++ 单元测试
- 位置:
app/src/main/jni/librime-t9/test/ - 框架: GoogleTest
- 目的: 测试 T9 输入法 C++ 插件核心算法(缓冲区、状态机、撤销模型、右侧选词、拼音映射、音节对齐等)
- 运行方式:
bash
# 在 app/src/main/jni/ 下执行(需要 CMake + gtest)
cd app/src/main/jni/librime-t9
./run_t9_tests.sh共 11 个测试文件、约 266 个用例,覆盖 t9_buffer、t9_state_machine、t9_undo_model、t9_right_commit_handler、t9_pinyin_map、t9_syllable_alignment、t9_patch_utils 等模块。
3. 仪器测试 (Instrumented Tests)
- 位置:
app/src/androidTest/java/ - 目的: 测试与 Android 框架交互的组件
- 运行速度: 较慢(需要在设备/模拟器上运行)
- 适用场景:
- 数据库操作测试
- SharedPreferences 测试
- Context 相关测试
- Native 库交互测试
示例测试类:
RimeEngineTest- 测试 RIME 引擎初始化和基本操作ExtensionManagerInstrumentedTest- 测试插件管理器的实际加载
4. UI 测试 (UI Tests)
- 位置:
app/src/androidTest/java/ - 框架: Jetpack Compose Testing
- 目的: 测试 UI 组件的行为和外观
- 运行速度: 中等
- 适用场景:
- Compose 组件测试
- 用户交互测试
- 界面状态测试
示例测试类:
CandidateBarTest- 测试候选词栏 UI 组件
运行测试
运行所有单元测试
bash
./gradlew test运行所有仪器测试
bash
./gradlew connectedAndroidTest运行特定测试类
bash
# 单元测试
./gradlew test --tests "com.kingzcheung.xime.settings.SchemaConfigHelperTest"
./gradlew :plugin-core:test --tests "com.kingzcheung.xime.plugin.core.js.JsAsrPluginTest"
# 仪器测试
./gradlew connectedAndroidTest --tests "com.kingzcheung.xime.rime.RimeEngineTest"运行 T9 C++ 测试
bash
cd app/src/main/jni/librime-t9
./run_t9_tests.sh # 构建并运行全部测试
./run_t9_tests.sh -f T9UndoModel* # 运行指定测试(gtest filter)
./run_t9_tests.sh --no-build # 跳过构建,直接运行运行所有测试
bash
./gradlew connectedCheck插件脚本测试(xipm test)
插件作者通常不需要跑整个宿主测试套件:xipm 内嵌与真机同款的 QuickJS 引擎和 v3 契约的 mock host,xipm test 可直接在本地运行插件的 main.test.ts,无需真机或 Node:
bash
# 先编译(测试基于 build/plugin-js 下的产物运行)
cd tools/xime-plugin
cargo run -- build --all --plugins-dir ../../plugins --out ../../build/plugin-js
# 批量测试(无测试文件的插件跳过)
cargo run -- test
# 单插件(也可在插件目录内零参数运行)
cargo run -- test ../../plugins/my-plugin
# 无 main.test.ts 时仅做加载冒烟
cargo run -- test ../../plugins/my-plugin --smoke测试环境要点:
- 网络 / WS / SSE 必须显式 stub(
__ximeMock.addHttpResponse/addWs/addSse),未注册的请求 rejectE_NETWORK,绝不真实联网 - 可断言调用记录:
httpRequests/sentWs/asrEvents/quickSend/clipboard - 可控时钟:
__ximeMock.setClock(epochSeconds) - 插件
console.log/error前缀化输出到终端
测试 API 与完整说明见 插件开发指南 与 插件开发教程。
测试最佳实践
1. 测试命名规范
使用描述性测试名称,使用反引号包含空格:
kotlin
@Test
fun `parseSchemaYaml should parse valid schema`() {
// 测试代码
}2. 测试结构
遵循 AAA 模式:
- Arrange - 准备测试数据
- Act - 执行被测试的代码
- Assert - 验证结果
kotlin
@Test
fun `getCandidates should return empty array when not initialized`() {
// Arrange
val engine = RimeEngine.getInstance()
// Act
val candidates = engine.getCandidates()
// Assert
assertTrue(candidates.isEmpty())
}3. 使用测试规则
kotlin
class MyTest {
@get:Rule
val coroutineTestRule = CoroutineTestRule()
@get:Rule
val sharedPreferencesRule = SharedPreferencesTestRule()
}4. 测试隔离
每个测试应该独立运行,不依赖其他测试的结果:
kotlin
@Before
fun setup() {
// 重置状态
ExtensionManager.release()
}
@After
fun tearDown() {
// 清理资源
ExtensionManager.release()
}5. Mock 外部依赖
使用 Mockito 模拟外部依赖:
kotlin
@Test
fun `plugin should return emojis correctly`() {
val mockPlugin = mock<EmojiPlugin>()
`when`(mockPlugin.getEmojis(null, null, 10)).thenReturn(
listOf(EmojiItem("1", "(^_^)", "(^_^)", null, "kaomoji"))
)
val result = mockPlugin.getEmojis(null, null, 10)
assertEquals(1, result.size)
}测试覆盖率
生成测试覆盖率报告
bash
./gradlew createDebugCoverageReport报告位置:app/build/reports/coverage/debug/index.html
持续集成 (CI)
在 CI 环境中运行测试:
yaml
# .github/workflows/test.yml
- name: Run Unit Tests
run: ./gradlew test
- name: Run Instrumented Tests
run: ./gradlew connectedAndroidTest测试工具类
CoroutineTestRule
用于测试协程代码:
kotlin
class MyViewModelTest {
@get:Rule
val coroutineTestRule = CoroutineTestRule()
@Test
fun testCoroutine() = runTest {
// 测试协程代码
}
}SharedPreferencesTestRule
用于测试 SharedPreferences:
kotlin
class SettingsTest {
@get:Rule
val prefsRule = SharedPreferencesTestRule()
@Test
fun testSettings() {
val prefs = prefsRule.getSharedPreferences()
// 测试代码
}
}常见测试场景
1. 测试 Singleton
kotlin
@Test
fun `getInstance should return singleton`() {
val instance1 = RimeEngine.getInstance()
val instance2 = RimeEngine.getInstance()
assertSame(instance1, instance2)
}2. 测试异步操作
kotlin
@Test
fun `predict should return results`() = runTest {
val result = ExtensionManager.predict(context, "测试", 5)
assertNotNull(result)
assertTrue(result.isNotEmpty())
}3. 测试 Compose UI
kotlin
@Test
fun `CandidateBar should display candidates`() {
val candidates = listOf("你好", "世界")
composeTestRule.setContent {
CandidateBar(
candidates = candidates,
selectedIndex = 0,
onCandidateClick = {},
onCandidateLongClick = {}
)
}
composeTestRule.onNodeWithText("你好").assertIsDisplayed()
}故障排查
测试失败常见原因
Native 库未加载
- 确保在仪器测试中正确初始化 RimeEngine
- 使用
@Before注解的 setup 方法初始化
Context 为空
- 使用
InstrumentationRegistry.getInstrumentation().targetContext
- 使用
协程测试超时
- 使用
runTest而不是runBlocking - 配置
TestDispatcher
- 使用
Compose 测试找不到节点
- 检查是否使用了正确的语义属性
- 使用
printToLog()调试 UI 树