UNPKG

mixpanel-react-native

Version:

Official React Native Tracking Library for Mixpanel Analytics

469 lines (382 loc) • 13.3 kB
# 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__/ └── @react-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