React Native Keychain 的 Jest 单元测试指南

React Native Keychain 的 Jest 单元测试指南

【免费下载链接】react-native-keychain :key: Keychain Access for React Native 【免费下载链接】react-native-keychain 项目地址: https://gitcode.com/gh_mirrors/re/react-native-keychain

前言:为什么需要专业的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, setSharedWebCredentialsiOS共享Web凭证

1.2 Jest测试的核心挑战

mermaid

二、完整的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 测试金字塔策略

mermaid

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 functionMock未正确设置检查__mocks__目录结构
Promise一直pendingMock未返回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测试策略。记住良好的测试不仅仅是让测试通过,更是要确保:

  1. 覆盖所有业务场景 - 成功、失败、边界情况
  2. 验证正确的参数传递 - 确保Options配置正确
  3. 模拟真实的异步行为 - 包括延迟和错误
  4. 保持测试的独立性 - 每个测试都是独立的
  5. 定期维护Mock数据 - 随着API更新而更新

现在,你可以自信地为你的React Native应用构建安全可靠的凭证存储测试体系了!

【免费下载链接】react-native-keychain :key: Keychain Access for React Native 【免费下载链接】react-native-keychain 项目地址: https://gitcode.com/gh_mirrors/re/react-native-keychain

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值