React Native Keychain 的 Jest 单元测试指南
前言:为什么需要专业的Keychain测试?
在移动应用开发中,安全存储用户凭证(Credentials)是至关重要的功能。React Native Keychain作为业界领先的安全存储解决方案,提供了跨平台的密钥链(Keychain)访问能力。然而,由于其依赖原生模块的特性,在Jest单元测试环境中会遇到原生代码无法执行的挑战。
读完本文,你将掌握:
- ✅ Keychain模块的完整Mocking策略
- ✅ 不同测试场景的实战代码示例
- ✅ 错误处理和边界条件的测试方法
- ✅ 性能优化和最佳实践建议
- ✅ 与E2E测试的协同工作流
一、理解Keychain的架构与测试挑战
1.1 Keychain核心API概览
React Native Keychain提供了丰富的API接口,主要分为以下几类:
| API类别 | 核心方法 | 功能描述 |
|---|---|---|
| 通用密码管理 | setGenericPassword, getGenericPassword, resetGenericPassword | 管理应用内的用户名密码 |
| 网络凭证管理 | setInternetCredentials, getInternetCredentials, resetInternetCredentials | 管理网站登录凭证 |
| 生物识别 | getSupportedBiometryType, canImplyAuthentication | 生物识别能力检测 |
| 安全检查 | getSecurityLevel, isPasscodeAuthAvailable | 安全级别和设备能力检查 |
| 共享凭证 | requestSharedWebCredentials, setSharedWebCredentials | iOS共享Web凭证 |
1.2 Jest测试的核心挑战
二、完整的Mock实现方案
2.1 基础Mock对象结构
创建 __mocks__/react-native-keychain/index.ts:
// 完整的Keychain Mock实现
const keychainMock = {
// 枚举常量
SECURITY_LEVEL: {
SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE',
SECURE_HARDWARE: 'MOCK_SECURITY_LEVEL_SECURE_HARDWARE',
ANY: 'MOCK_SECURITY_LEVEL_ANY',
},
ACCESSIBLE: {
WHEN_UNLOCKED: 'MOCK_AccessibleWhenUnlocked',
AFTER_FIRST_UNLOCK: 'MOCK_AccessibleAfterFirstUnlock',
ALWAYS: 'MOCK_AccessibleAlways',
WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: 'MOCK_AccessibleWhenPasscodeSetThisDeviceOnly',
WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'MOCK_AccessibleWhenUnlockedThisDeviceOnly',
AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: 'MOCK_AccessibleAfterFirstUnlockThisDeviceOnly',
},
ACCESS_CONTROL: {
USER_PRESENCE: 'MOCK_UserPresence',
BIOMETRY_ANY: 'MOCK_BiometryAny',
BIOMETRY_CURRENT_SET: 'MOCK_BiometryCurrentSet',
DEVICE_PASSCODE: 'MOCK_DevicePasscode',
APPLICATION_PASSWORD: 'MOCK_ApplicationPassword',
BIOMETRY_ANY_OR_DEVICE_PASSCODE: 'MOCK_BiometryAnyOrDevicePasscode',
BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE: 'MOCK_BiometryCurrentSetOrDevicePasscode',
},
// 核心API方法
setGenericPassword: jest.fn().mockImplementation((username, password, options) => {
return Promise.resolve({
service: options?.service || 'defaultService',
storage: 'MOCK_KeystoreAESGCM'
});
}),
getGenericPassword: jest.fn().mockImplementation((options) => {
return Promise.resolve({
username: 'mockUser',
password: 'mockPassword123',
service: options?.service || 'defaultService',
storage: 'MOCK_KeystoreAESGCM'
});
}),
resetGenericPassword: jest.fn().mockResolvedValue(true),
// 错误场景Mock
setGenericPasswordWithError: jest.fn().mockRejectedValue(
new Error('Keychain access failed')
),
// 其他方法...
};
export default keychainMock;
2.2 Jest配置设置
在 jest.config.js 中配置自动Mock:
module.exports = {
preset: 'react-native',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapping: {
'react-native-keychain': '<rootDir>/__mocks__/react-native-keychain',
},
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|react-native-keychain)/)',
],
};
三、实战测试用例详解
3.1 基础功能测试套件
import Keychain from 'react-native-keychain';
describe('Keychain基础功能测试', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('保存和读取通用密码', async () => {
// 设置Mock返回值
(Keychain.setGenericPassword as jest.Mock).mockResolvedValueOnce({
service: 'testService',
storage: 'MOCK_KeystoreAESGCM'
});
(Keychain.getGenericPassword as jest.Mock).mockResolvedValueOnce({
username: 'testUser',
password: 'testPass123',
service: 'testService',
storage: 'MOCK_KeystoreAESGCM'
});
// 执行保存操作
const saveResult = await Keychain.setGenericPassword(
'testUser',
'testPass123',
{ service: 'testService' }
);
// 验证保存结果
expect(saveResult).toEqual({
service: 'testService',
storage: 'MOCK_KeystoreAESGCM'
});
expect(Keychain.setGenericPassword).toHaveBeenCalledWith(
'testUser',
'testPass123',
{ service: 'testService' }
);
// 执行读取操作
const credentials = await Keychain.getGenericPassword({
service: 'testService'
});
// 验证读取结果
expect(credentials).toEqual({
username: 'testUser',
password: 'testPass123',
service: 'testService',
storage: 'MOCK_KeystoreAESGCM'
});
});
});
3.2 错误处理测试
describe('Keychain错误处理测试', () => {
test('密码保存失败场景', async () => {
// Mock失败场景
(Keychain.setGenericPassword as jest.Mock).mockRejectedValueOnce(
new Error('Keychain access denied')
);
await expect(
Keychain.setGenericPassword('user', 'pass')
).rejects.toThrow('Keychain access denied');
});
test('密码不存在场景', async () => {
// Mock密码不存在
(Keychain.getGenericPassword as jest.Mock).mockResolvedValueOnce(false);
const result = await Keychain.getGenericPassword();
expect(result).toBe(false);
});
test('生物识别不支持场景', async () => {
(Keychain.getSupportedBiometryType as jest.Mock).mockResolvedValueOnce(null);
const biometryType = await Keychain.getSupportedBiometryType();
expect(biometryType).toBeNull();
});
});
3.3 高级功能测试
describe('Keychain高级功能测试', () => {
test('带生物识别的密码存储', async () => {
const authPrompt = {
title: '请进行生物识别验证',
cancel: '取消'
};
await Keychain.setGenericPassword('user', 'pass', {
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
authenticationPrompt: authPrompt
});
expect(Keychain.setGenericPassword).toHaveBeenCalledWith(
'user',
'pass',
{
accessControl: 'MOCK_BiometryAny',
authenticationPrompt: authPrompt
}
);
});
test('多服务密码管理', async () => {
// Mock多个服务
(Keychain.getAllGenericPasswordServices as jest.Mock).mockResolvedValueOnce([
'service1', 'service2', 'service3'
]);
const services = await Keychain.getAllGenericPasswordServices();
expect(services).toEqual(['service1', 'service2', 'service3']);
});
});
四、测试策略与最佳实践
4.1 测试金字塔策略
4.2 Mock数据管理策略
| 场景类型 | Mock策略 | 示例 |
|---|---|---|
| 成功场景 | mockResolvedValue | 返回模拟的成功数据 |
| 失败场景 | mockRejectedValue | 抛出特定错误 |
| 空数据场景 | mockResolvedValue(false) | 返回false或空值 |
| 异步延迟 | mockImplementation | 模拟网络延迟 |
4.3 性能优化建议
// 使用jest.fn().mockImplementation()避免重复代码
const createKeychainMock = (defaultResponse = {}) => ({
setGenericPassword: jest.fn().mockImplementation((username, password, options) =>
Promise.resolve({
service: options?.service || 'default',
storage: 'MOCK_Storage',
...defaultResponse
})
),
// 其他方法...
});
// 在测试中重用Mock
const keychainMock = createKeychainMock();
五、常见问题与解决方案
5.1 问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
TypeError: undefined is not a function | Mock未正确设置 | 检查__mocks__目录结构 |
| Promise一直pending | Mock未返回Promise | 确保使用mockResolvedValue |
| 类型错误 | TypeScript类型不匹配 | 完善Mock对象的类型定义 |
| 测试相互影响 | Mock状态未清理 | 在beforeEach中清理Mock |
5.2 调试技巧
// 添加调试信息
Keychain.setGenericPassword.mockImplementation((...args) => {
console.log('setGenericPassword called with:', args);
return Promise.resolve({ service: 'debug', storage: 'debug' });
});
// 检查Mock调用历史
console.log('Mock call history:', Keychain.setGenericPassword.mock.calls);
六、完整测试示例项目
6.1 测试文件结构
src/
__tests__/
services/
authService.test.ts
keychainWrapper.test.ts
__mocks__/
react-native-keychain/
index.ts
jest.setup.js
6.2 封装测试工具类
// test/utils/keychainTestUtils.ts
export const mockKeychainSuccess = (response = {}) => {
const mock = {
setGenericPassword: jest.fn().mockResolvedValue({
service: 'testService',
storage: 'MOCK_Storage',
...response
}),
getGenericPassword: jest.fn().mockResolvedValue({
username: 'testUser',
password: 'testPass',
service: 'testService',
storage: 'MOCK_Storage',
...response
}),
resetGenericPassword: jest.fn().mockResolvedValue(true)
};
jest.mock('react-native-keychain', () => mock);
return mock;
};
export const mockKeychainFailure = (errorMessage: string) => {
const mock = {
setGenericPassword: jest.fn().mockRejectedValue(new Error(errorMessage)),
getGenericPassword: jest.fn().mockRejectedValue(new Error(errorMessage))
};
jest.mock('react-native-keychain', () => mock);
return mock;
};
结语:构建可靠的Keychain测试体系
通过本文的指南,你应该已经掌握了React Native Keychain的完整Jest测试策略。记住良好的测试不仅仅是让测试通过,更是要确保:
- 覆盖所有业务场景 - 成功、失败、边界情况
- 验证正确的参数传递 - 确保Options配置正确
- 模拟真实的异步行为 - 包括延迟和错误
- 保持测试的独立性 - 每个测试都是独立的
- 定期维护Mock数据 - 随着API更新而更新
现在,你可以自信地为你的React Native应用构建安全可靠的凭证存储测试体系了!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



