mixpanel-react-native
Version:
Official React Native Tracking Library for Mixpanel Analytics
469 lines (382 loc) ⢠13.3 kB
Markdown
# Jest Testing Patterns
## Overview
The mixpanel-react-native library demonstrates sophisticated Jest testing patterns for React Native libraries, including comprehensive mocking strategies, async testing patterns, and integration testing approaches.
## šÆ Testing Architecture
### Test Organization Strategy
```
__tests__/
āāā jest_setup.js # Global test configuration & mocks
āāā core.test.js # Core functionality tests
āāā index.test.js # Main API surface tests
āāā main.test.js # JavaScript implementation tests
āāā network.test.js # Network layer tests
āāā queue.test.js # Queue management tests
__mocks__/
āāā -native-async-storage/
āāā async-storage.js # AsyncStorage mock
```
**Organization Principles**:
- One test file per major module
- Centralized mocking in `jest_setup.js`
- Mock directory mirrors node_modules structure
- Clear separation of concerns
### Jest Configuration
```javascript
// package.json
"jest": {
"modulePathIgnorePatterns": [
"<rootDir>/Samples/" // Exclude sample apps from tests
],
"testMatch": [
"<rootDir>/__tests__/*.test.js" // Explicit test file pattern
],
"setupFiles": [
"<rootDir>/__tests__/jest_setup.js" // Global setup
],
"verbose": true, // Detailed test output
"preset": "react-native", // RN-specific configuration
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/react-native/jest/preprocessor.js"
}
}
```
**Configuration Insights**:
- React Native preset provides essential mocks
- Custom preprocessor for JS transformation
- Verbose output for debugging
- Sample apps excluded to avoid noise
## š Advanced Mocking Strategies
### 1. Comprehensive Native Module Mocking
```javascript
// __tests__/jest_setup.js
jest.doMock("react-native", () => {
return Object.setPrototypeOf({
// Mock the complete native module API
NativeModules: {
MixpanelReactNative: {
// Core methods
initialize: jest.fn(),
setServerURL: jest.fn(),
setLoggingEnabled: jest.fn(),
// Tracking methods
track: jest.fn(),
trackWithGroups: jest.fn(),
// Identity methods
identify: jest.fn(),
alias: jest.fn(),
reset: jest.fn(),
getDistinctId: jest.fn(),
// People Analytics methods
set: jest.fn(),
setOnce: jest.fn(),
increment: jest.fn(),
append: jest.fn(),
union: jest.fn(),
remove: jest.fn(),
unset: jest.fn(),
trackCharge: jest.fn(),
clearCharges: jest.fn(),
deleteUser: jest.fn(),
// Group Analytics methods
groupSetProperties: jest.fn(),
groupSetPropertyOnce: jest.fn(),
groupUnsetProperty: jest.fn(),
groupRemovePropertyValue: jest.fn(),
groupUnionProperty: jest.fn(),
// Configuration methods
setFlushOnBackground: jest.fn(),
setUseIpAddressForGeolocation: jest.fn(),
setFlushBatchSize: jest.fn(),
hasOptedOutTracking: jest.fn(),
optInTracking: jest.fn(),
optOutTracking: jest.fn(),
// Super properties
registerSuperProperties: jest.fn(),
registerSuperPropertiesOnce: jest.fn(),
unregisterSuperProperty: jest.fn(),
getSuperProperties: jest.fn(),
clearSuperProperties: jest.fn(),
// Event timing
timeEvent: jest.fn(),
eventElapsedTime: jest.fn(),
// Groups
setGroup: jest.fn(),
getGroup: jest.fn(),
addGroup: jest.fn(),
removeGroup: jest.fn(),
deleteGroup: jest.fn(),
},
},
}, ReactNative);
});
```
**Mocking Philosophy**:
- **Complete API coverage**: Every native method mocked
- **Consistent interface**: Mocks match actual native API
- **Test isolation**: Each test gets fresh mock state
- **Debugging support**: Verbose method tracking
### 2. External Dependency Mocking
```javascript
// __tests__/jest_setup.js
// UUID generation mocking
jest.mock("uuid", () => ({
v4: jest.fn(() => "mocked-uuid-12345"),
}));
// Expo crypto mocking with dynamic IDs
jest.mock("expo-crypto", () => ({
randomUUID: jest.fn(
() => "mocked-uuid-string-" + Math.random().toString(36).substring(2, 15)
),
}));
// AsyncStorage comprehensive mocking
jest.mock("@react-native-async-storage/async-storage", () => ({
getItem: jest.fn().mockResolvedValue(null),
setItem: jest.fn().mockResolvedValue(undefined),
removeItem: jest.fn().mockResolvedValue(undefined),
}));
```
**External Mock Strategies**:
- **Deterministic UUIDs**: Predictable test behavior
- **Async storage**: Promise-based mocks
- **Realistic responses**: Match actual API behavior
### 3. Internal Module Mocking
```javascript
// __tests__/core.test.js
// Mock internal modules with factory functions
jest.mock("mixpanel-react-native/javascript/mixpanel-queue", () => ({
MixpanelQueueManager: {
initialize: jest.fn(),
enqueue: jest.fn(),
getQueue: jest.fn(() => []),
spliceQueue: jest.fn(),
clearQueue: jest.fn(),
},
}));
jest.mock("mixpanel-react-native/javascript/mixpanel-network", () => ({
MixpanelNetwork: {
sendRequest: jest.fn(),
},
}));
jest.mock("mixpanel-react-native/javascript/mixpanel-config", () => ({
MixpanelConfig: {
getInstance: jest.fn().mockReturnValue({
getFlushInterval: jest.fn().mockReturnValue(1000),
getFlushBatchSize: jest.fn().mockReturnValue(50),
getServerURL: jest.fn(),
getUseIpAddressForGeolocation: jest.fn(),
}),
},
}));
```
**Internal Mock Benefits**:
- **Unit test isolation**: Test one module at a time
- **Fast execution**: No real network or storage I/O
- **Predictable behavior**: Controlled responses
- **Error simulation**: Easy to test error conditions
## ā
Test Patterns & Best Practices
### 1. Async Testing with Promises
```javascript
// Example from network.test.js (inferred pattern)
describe('MixpanelNetwork', () => {
beforeEach(() => {
// Reset all mocks before each test
jest.clearAllMocks();
});
it('should handle successful requests', async () => {
// Setup
const mockResponse = { status: 200, json: () => Promise.resolve(1) };
global.fetch = jest.fn().mockResolvedValue(mockResponse);
// Execute
await MixpanelNetwork.sendRequest({
token: 'test-token',
endpoint: '/track/',
data: [{ event: 'test' }],
serverURL: 'https://api.mixpanel.com',
useIPAddressForGeoLocation: true
});
// Verify
expect(global.fetch).toHaveBeenCalledWith(
'https://api.mixpanel.com/track/?ip=1',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
})
);
});
it('should retry on failure with exponential backoff', async () => {
// Setup - simulate network failure then success
global.fetch = jest.fn()
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({ status: 200, json: () => Promise.resolve(1) });
// Execute
await MixpanelNetwork.sendRequest({...});
// Verify retry behavior
expect(global.fetch).toHaveBeenCalledTimes(2);
});
});
```
### 2. State Testing Patterns
```javascript
// Example from queue.test.js (inferred pattern)
describe('MixpanelQueueManager', () => {
beforeEach(() => {
// Reset internal state
MixpanelQueueManager._queues = {};
});
it('should maintain separate queues per token', async () => {
const token1 = 'token1';
const token2 = 'token2';
const eventData = { event: 'test' };
// Initialize queues
await MixpanelQueueManager.initialize(token1, MixpanelType.EVENTS);
await MixpanelQueueManager.initialize(token2, MixpanelType.EVENTS);
// Add events to different tokens
await MixpanelQueueManager.enqueue(token1, MixpanelType.EVENTS, eventData);
await MixpanelQueueManager.enqueue(token2, MixpanelType.EVENTS, eventData);
// Verify isolation
const queue1 = MixpanelQueueManager.getQueue(token1, MixpanelType.EVENTS);
const queue2 = MixpanelQueueManager.getQueue(token2, MixpanelType.EVENTS);
expect(queue1).toHaveLength(1);
expect(queue2).toHaveLength(1);
expect(queue1[0]).toBe(eventData);
expect(queue2[0]).toBe(eventData);
});
});
```
### 3. Error Handling Testing
```javascript
// Example error testing pattern
describe('Error Handling', () => {
it('should throw descriptive errors for invalid inputs', () => {
expect(() => {
mixpanel.track('', {}); // Empty event name
}).toThrow('eventName is not a valid string');
expect(() => {
mixpanel.track('event', 'invalid'); // Non-object properties
}).toThrow('properties is not a valid json object');
});
it('should handle storage failures gracefully', async () => {
// Mock storage failure
AsyncStorage.setItem.mockRejectedValue(new Error('Storage full'));
// Should not throw, but log error
await expect(
MixpanelPersistent.getInstance().persistSuperProperties('token', {})
).resolves.toBeUndefined();
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining('error setting item in storage')
);
});
});
```
## š§ Testing Utilities
### 1. Mock Factory Pattern
```javascript
// __tests__/jest_setup.js
const createMockStorage = () => ({
AsyncStorageAdapter: jest.fn().mockImplementation(() => ({
getItem: jest.fn().mockResolvedValue(null),
setItem: jest.fn().mockResolvedValue(undefined),
removeItem: jest.fn().mockResolvedValue(undefined),
})),
});
jest.mock("mixpanel-react-native/javascript/mixpanel-storage", createMockStorage);
```
### 2. Test Data Factories
```javascript
// Example test data patterns
const createTestEvent = (overrides = {}) => ({
event: 'Test Event',
properties: { testProp: 'testValue' },
token: 'test-token',
distinct_id: 'test-user',
time: Date.now(),
...overrides
});
const createTestUser = (overrides = {}) => ({
$distinct_id: 'test-user',
$set: { name: 'Test User' },
$token: 'test-token',
...overrides
});
```
## š Test Coverage Strategy
### Core Functionality Coverage
- ā
**API validation**: All input validation paths
- ā
**Mode switching**: Native vs JavaScript fallback
- ā
**Queue management**: Enqueue, batch processing, persistence
- ā
**Network handling**: Success, failure, retry logic
- ā
**State management**: Identity, super properties, configuration
- ā
**Error scenarios**: Invalid inputs, storage failures, network errors
### Integration Testing Approach
```javascript
// Example integration test pattern
describe('End-to-End Tracking Flow', () => {
it('should track event through complete pipeline', async () => {
// Setup complete system
const mixpanel = new Mixpanel('test-token', true, false);
await mixpanel.init();
// Execute tracking
mixpanel.track('Purchase', { amount: 99.99 });
// Verify pipeline steps
expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith(
'test-token',
MixpanelType.EVENTS,
expect.objectContaining({
event: 'Purchase',
properties: expect.objectContaining({ amount: 99.99 })
})
);
});
});
```
## š Performance Testing Patterns
### 1. Batch Processing Tests
```javascript
describe('Batch Processing Performance', () => {
it('should process large queues efficiently', async () => {
const events = Array(1000).fill().map((_, i) => ({ event: `Event${i}` }));
// Add all events
for (const event of events) {
await MixpanelQueueManager.enqueue('token', MixpanelType.EVENTS, event);
}
// Process in batches
const startTime = Date.now();
await MixpanelCore.flush('token');
const endTime = Date.now();
// Verify performance
expect(endTime - startTime).toBeLessThan(1000); // Under 1 second
});
});
```
### 2. Memory Leak Testing
```javascript
describe('Memory Management', () => {
it('should cleanup resources on reset', async () => {
// Add data
await mixpanel.track('event', {});
await mixpanel.registerSuperProperties({ prop: 'value' });
// Reset
await mixpanel.reset();
// Verify cleanup
expect(await mixpanel.getSuperProperties()).toEqual({});
expect(MixpanelQueueManager.getQueue('token', MixpanelType.EVENTS)).toEqual([]);
});
});
```
## š Jest Best Practices Discovered
### 1. **Comprehensive Mocking**
Mock all external dependencies completely to ensure test isolation
### 2. **Async Testing**
Use proper async/await patterns for testing Promise-based APIs
### 3. **State Reset**
Clear all mocks and reset state between tests
### 4. **Error Path Testing**
Test both success and failure scenarios thoroughly
### 5. **Integration Testing**
Test module interactions, not just individual units
### 6. **Performance Awareness**
Include basic performance assertions in tests
### 7. **Realistic Mocks**
Mocks should behave like real implementations
### 8. **Test Organization**
Group related tests and use descriptive names