Android语音识别功能的测试通常比普通UI交互更复杂,因为它同时依赖硬件麦克风、系统语音服务、网络状态和语言包资源。测试时如果只关注最终识别文本是否正确,很容易漏掉权限被拒绝、服务未安装、部分结果未回调等问题。有效的测试方法应当把识别链路拆开,分别验证前置条件、识别过程、结果回调和错误分支。

一、测试前需要确认的权限与服务状态
在AndroidManifest.xml中声明录音权限是第一步。从Android 6.0开始,RECORD_AUDIO属于运行时权限,测试时不能只检查Manifest,还要在代码中动态申请。自动化脚本在CI环境里经常跳过权限弹窗,导致识别任务无法启动。除了录音权限,Android 11及以上版本还需要在Manifest中通过<queries>声明对语音识别服务的可见性,否则调用SpeechRecognizer.isRecognitionAvailable()可能返回false,但错误容易被误判为设备不支持。
可以通过adb命令快速检查语音识别服务是否可用:
adb shell dumpsys package com.google.android.googlequicksearchbox | grep -i recognition adb shell pm list packages | grep -i recognition
上面的grep只是示例,不同ROM语音服务包名可能不同。真机测试时,如果服务不可用,需要检查系统设置中的语音输入选项,或者更换设备。模拟器通常没有完整的Google语音服务,更适合用自定义RecognitionService做离线测试。
二、基于SpeechRecognizer编写可测试的识别代码
SpeechRecognizer提供了比Intent方式更灵活的控制能力,适合在测试中观察识别状态变化。核心步骤包括创建实例、设置RecognitionListener、构造RecognizerIntent参数、调用startListening。测试代码建议封装成一个独立类,把回调事件通过接口暴露出来,方便在单元测试或仪器测试中替换。
下面是一个最小可用的Java测试封装:
public class VoiceRecognitionTester {
public interface ResultCallback {
void onResult(String text);
void onError(int errorCode);
void onPartialResult(String partialText);
}
private SpeechRecognizer speechRecognizer;
private Intent recognizerIntent;
private ResultCallback callback;
public VoiceRecognitionTester(Context context, ResultCallback callback) {
this.callback = callback;
this.speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
this.recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
this.recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
this.recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-CN");
this.recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
this.recognizerIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 3);
}
public void start() {
speechRecognizer.setRecognitionListener(new RecognitionListener() {
@Override
public void onReadyForSpeech(Bundle params) { }
@Override
public void onBeginningOfSpeech() { }
@Override
public void onRmsChanged(float rmsdB) { }
@Override
public void onBufferReceived(byte[] buffer) { }
@Override
public void onEndOfSpeech() { }
@Override
public void onError(int error) {
if (callback != null) {
callback.onError(error);
}
}
@Override
public void onResults(Bundle results) {
ArrayList<String> matches = results.getStringArrayList(
SpeechRecognizer.RESULTS_RECOGNITION);
String text = matches != null && !matches.isEmpty() ? matches.get(0) : "";
if (callback != null) {
callback.onResult(text);
}
}
@Override
public void onPartialResults(Bundle partialResults) {
ArrayList<String> partial = partialResults.getStringArrayList(
SpeechRecognizer.RESULTS_RECOGNITION);
if (partial != null && !partial.isEmpty() && callback != null) {
callback.onPartialResult(partial.get(0));
}
}
@Override
public void onEvent(int eventType, Bundle params) { }
});
speechRecognizer.startListening(recognizerIntent);
}
public void stop() {
if (speechRecognizer != null) {
speechRecognizer.stopListening();
}
}
public void destroy() {
if (speechRecognizer != null) {
speechRecognizer.destroy();
}
}
}
测试时不要忘记调用destroy释放资源,否则连续多次测试可能触发ERROR_INSUFFICIENT_PERMISSIONS之外的奇怪状态。针对部分结果,可以设置EXTRA_PARTIAL_RESULTS为true,在onPartialResults中持续记录中间文本,用来分析识别引擎在噪声环境下的逐步修正过程。如果只需要最终结果,可以关闭部分结果,减少回调频率。
三、常见错误码与测试用例设计
识别过程最常见的错误码包括ERROR_NO_MATCH、ERROR_SPEECH_TIMEOUT、ERROR_NETWORK_TIMEOUT、ERROR_AUDIO和ERROR_INSUFFICIENT_PERMISSIONS。测试时不能只记录错误码,还要结合当时的输入音频、环境噪声和网络状态。比如ERROR_NO_MATCH在离线识别中经常因为语言包不匹配或发音太短,需要准备不同口音和语速的音频样本。
设计用例时建议覆盖以下场景:静音输入、单字指令、长句输入、连续对话、背景音乐、多人说话、飞行模式、弱网环境、横竖屏切换以及来电打断。每个场景需要记录识别耗时、返回文本是否包含扩展结果、部分结果是否被多次回调。可以使用真机云测平台批量执行,但要注意云真机的麦克风可能被禁用,导致ERROR_AUDIO。
| 错误码 | 常见触发条件 | 测试重点 |
|---|---|---|
| ERROR_NO_MATCH | 无匹配结果 | 语言设置、音频清晰度 |
| ERROR_SPEECH_TIMEOUT | 未检测到有效语音 | 麦克风音量、输入音频时长 |
| ERROR_NETWORK_TIMEOUT | 在线识别网络不佳 | 切换离线模式、弱网模拟 |
| ERROR_AUDIO | 音频录制失败 | 权限、录音焦点、设备兼容性 |
| ERROR_INSUFFICIENT_PERMISSIONS | 录音权限未授予 | 安装后首次启动、权限弹窗拒绝 |
对于自动化测试,可以通过adb shell settings put secure enabled_input_methods类命令配置输入法,但语音识别服务通常由系统组件提供,直接模拟点击比较困难。建议把语音识别封装成可注入的接口,在UI自动化中使用假识别器返回固定文本,在专项测试中再调用真实服务。这样既能保证UI流程稳定,又能隔离识别服务波动带来的用例失败。
四、使用Intent方式快速验证识别链路
如果只是快速验证语音识别是否可用,可以使用RecognizerIntent.ACTION_RECOGNIZE_SPEECH启动系统语音识别界面,通过startActivityForResult接收结果。这种方式不需要自己管理SpeechRecognizer生命周期,适合验证系统语音服务是否正常。但它的缺点是不方便注入测试数据,也不适合自动化测试。
private void startSystemVoiceRecognition() {
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-CN");
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "请说出测试指令");
try {
startActivityForResult(intent, 1001);
} catch (ActivityNotFoundException e) {
Log.e("VoiceTest", "设备未安装语音识别服务", e);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1001 && resultCode == RESULT_OK && data != null) {
ArrayList<String> results = data.getStringArrayListExtra(
RecognizerIntent.EXTRA_RESULTS);
if (results != null && !results.isEmpty()) {
Log.i("VoiceTest", "识别结果: " + results.get(0));
}
}
}
Intent方式在测试时有明显局限:它依赖系统UI,无法直接控制识别开始和结束时间,也无法获取部分结果。因此正式测试建议以SpeechRecognizer为主,Intent方式作为冒烟测试的补充。如果系统没有语音识别服务,startActivityForResult会抛出ActivityNotFoundException,测试脚本需要捕获该异常并记录为环境不满足。
最后,测试报告应包含设备型号、系统版本、语音服务包名和版本、网络类型、音频源文件信息。一次完整的语音识别测试不是简单比对最终文本,而是要把环境变量和识别输出关联起来,才能定位准确率波动的原因。

