|
| 1 | +# Module Mocking |
| 2 | + |
| 3 | +Harness provides powerful module mocking capabilities that allow you to replace entire modules or parts of modules with mock implementations. This is particularly useful for testing React Native code that depends on native modules or third-party libraries. |
| 4 | + |
| 5 | +## mock() |
| 6 | + |
| 7 | +Mock a module by providing a factory function that returns the mock implementation. |
| 8 | + |
| 9 | +```typescript |
| 10 | +import { describe, test, expect, mock, fn } from 'react-native-harness' |
| 11 | + |
| 12 | +describe('module mocking', () => { |
| 13 | + test('complete module mock', () => { |
| 14 | + const mockFactory = () => ({ |
| 15 | + formatString: fn().mockReturnValue('mocked string'), |
| 16 | + calculateSum: fn().mockImplementation( |
| 17 | + (a: number, b: number) => a + b + 1000 |
| 18 | + ), |
| 19 | + constants: { |
| 20 | + VERSION: '999.0.0', |
| 21 | + DEBUG: true, |
| 22 | + }, |
| 23 | + }) |
| 24 | + |
| 25 | + mock('react-native', mockFactory) |
| 26 | + |
| 27 | + const mockedModule = require('react-native') |
| 28 | + |
| 29 | + expect(mockedModule.formatString()).toBe('mocked string') |
| 30 | + expect(mockedModule.calculateSum(5, 10)).toBe(1015) |
| 31 | + expect(mockedModule.constants.VERSION).toBe('999.0.0') |
| 32 | + expect(mockedModule.formatString).toHaveBeenCalledTimes(1) |
| 33 | + }) |
| 34 | +}) |
| 35 | +``` |
| 36 | + |
| 37 | +## requireActual() |
| 38 | + |
| 39 | +Get the actual (unmocked) implementation of a module. This is useful for partial mocking where you want to preserve some exports while replacing others. |
| 40 | + |
| 41 | +```typescript |
| 42 | +import { describe, test, expect, mock, requireActual, fn } from 'react-native-harness' |
| 43 | + |
| 44 | +describe('partial module mocking', () => { |
| 45 | + test('mock only Platform while keeping other exports', () => { |
| 46 | + const mockFactory = () => { |
| 47 | + // Get the actual react-native module |
| 48 | + const actualRN = requireActual('react-native') |
| 49 | + |
| 50 | + // Copy without invoking getters to avoid triggering lazy initialization |
| 51 | + const proto = Object.getPrototypeOf(actualRN) |
| 52 | + const descriptors = Object.getOwnPropertyDescriptors(actualRN) |
| 53 | + |
| 54 | + const mockedRN = Object.create(proto, descriptors) |
| 55 | + const mockedPlatform = { |
| 56 | + OS: 'mockOS', |
| 57 | + Version: 999, |
| 58 | + select: fn().mockImplementation((options: Record<string, any>) => { |
| 59 | + return options.mockOS || options.default |
| 60 | + }), |
| 61 | + isPad: false, |
| 62 | + isTesting: true, |
| 63 | + } |
| 64 | + |
| 65 | + Object.defineProperty(mockedRN, 'Platform', { |
| 66 | + get() { |
| 67 | + return mockedPlatform |
| 68 | + }, |
| 69 | + }) |
| 70 | + |
| 71 | + return mockedRN |
| 72 | + } |
| 73 | + |
| 74 | + mock('react-native', mockFactory) |
| 75 | + |
| 76 | + const mockedRN = require('react-native') |
| 77 | + |
| 78 | + // Verify Platform is mocked |
| 79 | + expect(mockedRN.Platform.OS).toBe('mockOS') |
| 80 | + expect(mockedRN.Platform.Version).toBe(999) |
| 81 | + |
| 82 | + // Verify other React Native exports are preserved |
| 83 | + expect(mockedRN).toHaveProperty('View') |
| 84 | + expect(mockedRN).toHaveProperty('Text') |
| 85 | + expect(mockedRN).toHaveProperty('StyleSheet') |
| 86 | + }) |
| 87 | +}) |
| 88 | +``` |
| 89 | + |
| 90 | +## unmock() |
| 91 | + |
| 92 | +Remove a mock for a specific module, restoring it to its original implementation. |
| 93 | + |
| 94 | +```typescript |
| 95 | +import { describe, test, expect, mock, unmock } from 'react-native-harness' |
| 96 | + |
| 97 | +describe('unmocking modules', () => { |
| 98 | + test('unmock a previously mocked module', () => { |
| 99 | + // Mock a module |
| 100 | + const mockFactory = () => ({ mockProperty: 'mocked' }) |
| 101 | + mock('react-native', mockFactory) |
| 102 | + |
| 103 | + // Verify it's mocked |
| 104 | + let module = require('react-native') |
| 105 | + expect(module.mockProperty).toBe('mocked') |
| 106 | + |
| 107 | + // Unmock it |
| 108 | + unmock('react-native') |
| 109 | + |
| 110 | + // Verify it's back to actual |
| 111 | + module = require('react-native') |
| 112 | + expect(module).not.toHaveProperty('mockProperty') |
| 113 | + expect(module).toHaveProperty('Platform') // Should have actual RN properties |
| 114 | + }) |
| 115 | +}) |
| 116 | +``` |
| 117 | + |
| 118 | +## resetModules() |
| 119 | + |
| 120 | +Clear all module mocks and the module cache. This is useful in `afterEach` hooks to ensure tests don't interfere with each other. |
| 121 | + |
| 122 | +```typescript |
| 123 | +import { describe, test, expect, mock, resetModules, afterEach } from 'react-native-harness' |
| 124 | + |
| 125 | +describe('module reset', () => { |
| 126 | + afterEach(() => { |
| 127 | + resetModules() |
| 128 | + }) |
| 129 | + |
| 130 | + test('reinitialize module after reset', () => { |
| 131 | + const mockFactory = () => ({ now: Math.random() }) |
| 132 | + |
| 133 | + mock('react-native', mockFactory) |
| 134 | + |
| 135 | + // Verify mock is active |
| 136 | + const oldNow = require('react-native').now |
| 137 | + |
| 138 | + // Reset all modules |
| 139 | + resetModules() |
| 140 | + |
| 141 | + // Require again, should reinitialize the module |
| 142 | + const newNow = require('react-native').now |
| 143 | + expect(newNow).not.toBe(oldNow) |
| 144 | + }) |
| 145 | +}) |
| 146 | +``` |
| 147 | + |
| 148 | +## Best Practices |
| 149 | + |
| 150 | +1. **Always reset modules in `afterEach`**: Use `resetModules()` in your test cleanup to prevent mocks from leaking between tests. |
| 151 | + |
| 152 | +2. **Use `requireActual` for partial mocks**: When you only need to mock specific exports, use `requireActual()` to preserve the rest of the module. |
| 153 | + |
| 154 | +3. **Factory functions are called lazily**: The factory function is only called when the module is first required, not when `mock()` is called. |
| 155 | + |
| 156 | +4. **Module caching**: Modules are cached after first require. Use `resetModules()` if you need to reinitialize a mocked module. |
| 157 | + |
| 158 | +## API Reference |
| 159 | + |
| 160 | +- **`mock(moduleId: string, factory: () => unknown): void`** - Mock a module with a factory function |
| 161 | +- **`unmock(moduleId: string): void`** - Remove a mock for a specific module |
| 162 | +- **`requireActual<T = any>(moduleId: string): T`** - Get the actual (unmocked) implementation of a module |
| 163 | +- **`resetModules(): void`** - Clear all module mocks and the module cache |
| 164 | + |
0 commit comments