UNPKG

mixpanel-react-native

Version:

Official React Native Tracking Library for Mixpanel Analytics

567 lines (455 loc) โ€ข 14.8 kB
# Workflow: Testing Changes ## Overview This workflow demonstrates how to comprehensively test changes in the mixpanel-react-native library, covering both native and JavaScript implementations, edge cases, and integration scenarios. ## ๐Ÿงช Testing Strategy Overview The library uses a multi-layered testing approach: - **Unit Tests**: Individual module testing with mocks - **Integration Tests**: Cross-module functionality - **Manual Testing**: Sample app verification - **Platform Testing**: Native iOS/Android validation ## ๐Ÿš€ Quick Test Commands ```bash # Run all tests npm test # Run specific test file npm test -- __tests__/core.test.js # Run tests in watch mode (during development) npm test -- --watch # Run tests with coverage npm test -- --coverage ``` ## ๐Ÿ“ Unit Testing Workflow ### Step 1: Set Up Test Environment ```javascript // __tests__/new-feature.test.js import { jest } from "@jest/globals"; // Import required mocks (automatically loaded from jest_setup.js) const { MixpanelQueueManager } = require("mixpanel-react-native/javascript/mixpanel-queue"); const { MixpanelNetwork } = require("mixpanel-react-native/javascript/mixpanel-network"); describe('New Feature Tests', () => { beforeEach(() => { // Reset all mocks before each test jest.clearAllMocks(); // Reset any internal state MixpanelQueueManager._queues = {}; }); afterEach(() => { // Clean up any timers or async operations jest.clearAllTimers(); }); }); ``` ### Step 2: Test Input Validation ```javascript describe('Input Validation', () => { let mixpanel; beforeEach(() => { mixpanel = new Mixpanel('test-token', true); }); it('should validate required string parameters', () => { // Test empty string expect(() => { mixpanel.track('', {}); }).toThrow('eventName is not a valid string'); // Test null parameter expect(() => { mixpanel.track(null, {}); }).toThrow('eventName is not a valid string'); // Test undefined parameter expect(() => { mixpanel.track(undefined, {}); }).toThrow('eventName is not a valid string'); // Test whitespace-only string expect(() => { mixpanel.track(' ', {}); }).toThrow('eventName is not a valid string'); }); it('should validate object parameters', () => { // Test non-object properties expect(() => { mixpanel.track('event', 'invalid'); }).toThrow('properties is not a valid json object'); // Test function properties (should fail) expect(() => { mixpanel.track('event', { callback: () => {} }); }).toThrow(); // Test valid object (should not throw) expect(() => { mixpanel.track('event', { prop: 'value' }); }).not.toThrow(); // Test null properties (should be allowed) expect(() => { mixpanel.track('event', null); }).not.toThrow(); }); }); ``` ### Step 3: Test Native Mode Implementation ```javascript describe('Native Mode', () => { let mixpanel; beforeEach(() => { // Ensure native mode is active mixpanel = new Mixpanel('test-token', true, true); }); it('should call native implementation for tracking', () => { const eventName = 'Test Event'; const properties = { testProp: 'testValue' }; mixpanel.track(eventName, properties); expect(MixpanelReactNative.track).toHaveBeenCalledWith( 'test-token', eventName, expect.objectContaining({ ...properties, // Should include metadata mp_lib: 'react-native', $lib_version: expect.any(String) }) ); }); it('should handle native implementation promises', async () => { // Mock native method to return promise MixpanelReactNative.identify.mockResolvedValue(); await expect( mixpanel.identify('user123') ).resolves.toBeUndefined(); expect(MixpanelReactNative.identify).toHaveBeenCalledWith( 'test-token', 'user123' ); }); it('should handle native implementation errors', async () => { // Mock native method to reject const error = new Error('Native error'); MixpanelReactNative.identify.mockRejectedValue(error); await expect( mixpanel.identify('user123') ).rejects.toThrow('Native error'); }); }); ``` ### Step 4: Test JavaScript Mode Implementation ```javascript describe('JavaScript Mode', () => { let mixpanel; beforeEach(() => { // Force JavaScript mode mixpanel = new Mixpanel('test-token', true, false); }); it('should use JavaScript implementation when native unavailable', () => { expect(mixpanel.mixpanelImpl).toBeInstanceOf(MixpanelMain); expect(mixpanel.mixpanelImpl).not.toBe(MixpanelReactNative); }); it('should queue events for batch processing', async () => { const eventData = { event: 'Test Event', properties: { prop: 'value' } }; mixpanel.track(eventData.event, eventData.properties); // Verify event was added to queue expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith( 'test-token', MixpanelType.EVENTS, expect.objectContaining({ event: eventData.event, properties: expect.objectContaining(eventData.properties) }) ); }); it('should respect opt-out status', async () => { // Mock opt-out status const mockPersistent = { getOptedOut: jest.fn().mockReturnValue(true) }; MixpanelPersistent.getInstance.mockReturnValue(mockPersistent); mixpanel.track('Test Event', {}); // Should not queue event when opted out expect(MixpanelQueueManager.enqueue).not.toHaveBeenCalled(); }); }); ``` ### Step 5: Test Error Handling ```javascript describe('Error Handling', () => { it('should handle storage failures gracefully', async () => { // Mock storage failure const storageError = new Error('Storage unavailable'); AsyncStorage.setItem.mockRejectedValue(storageError); // Should not throw error await expect( mixpanel.registerSuperProperties({ prop: 'value' }) ).resolves.toBeUndefined(); // Should log error expect(console.error).toHaveBeenCalledWith( expect.stringContaining('error setting item in storage') ); }); it('should handle network failures with retry', async () => { // Mock network failure then success const networkError = new Error('Network timeout'); global.fetch .mockRejectedValueOnce(networkError) .mockResolvedValueOnce({ status: 200, json: () => Promise.resolve(1) }); await MixpanelNetwork.sendRequest({ token: 'test-token', endpoint: '/track/', data: [{ event: 'test' }], serverURL: 'https://api.mixpanel.com', useIPAddressForGeoLocation: true }); // Should retry on failure expect(global.fetch).toHaveBeenCalledTimes(2); }); }); ``` ## ๐Ÿ”— Integration Testing Workflow ### Step 1: End-to-End Flow Testing ```javascript // __tests__/integration/complete-flow.test.js describe('Complete Tracking Flow', () => { let mixpanel; beforeEach(async () => { mixpanel = new Mixpanel('test-token', true, false); // JavaScript mode await mixpanel.init(); }); it('should track event through complete pipeline', async () => { // Set up user identity await mixpanel.identify('user123'); // Add super properties await mixpanel.registerSuperProperties({ user_segment: 'premium' }); // Track event mixpanel.track('Purchase', { amount: 99.99, currency: 'USD' }); // Verify complete event structure expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith( 'test-token', MixpanelType.EVENTS, expect.objectContaining({ event: 'Purchase', properties: expect.objectContaining({ // Event properties amount: 99.99, currency: 'USD', // Super properties user_segment: 'premium', // Identity properties distinct_id: 'user123', // Metadata mp_lib: 'react-native', $lib_version: expect.any(String), // Session metadata $mp_metadata: expect.objectContaining({ $mp_session_id: expect.any(String), $mp_event_id: expect.any(String) }) }) }) ); }); it('should handle queue processing and network requests', async () => { // Add multiple events to queue for (let i = 0; i < 5; i++) { mixpanel.track(`Event ${i}`, { index: i }); } // Mock successful network response global.fetch.mockResolvedValue({ status: 200, json: () => Promise.resolve(1) }); // Trigger queue processing await mixpanel.flush(); // Verify network request was made expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining('https://api.mixpanel.com/track/'), expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: expect.stringContaining('data=') }) ); // Verify queue was cleared after successful send expect(MixpanelQueueManager.spliceQueue).toHaveBeenCalledWith( 'test-token', MixpanelType.EVENTS, 0, 5 // All 5 events should be removed ); }); }); ``` ### Step 2: Multi-Token Testing ```javascript describe('Multi-Token Support', () => { it('should isolate data between different tokens', async () => { const mixpanel1 = new Mixpanel('token1', true, false); const mixpanel2 = new Mixpanel('token2', true, false); await mixpanel1.init(); await mixpanel2.init(); // Set different properties for each token await mixpanel1.registerSuperProperties({ source: 'app1' }); await mixpanel2.registerSuperProperties({ source: 'app2' }); // Track events mixpanel1.track('Event1', {}); mixpanel2.track('Event2', {}); // Verify token isolation in queue calls expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith( 'token1', MixpanelType.EVENTS, expect.objectContaining({ properties: expect.objectContaining({ source: 'app1' }) }) ); expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith( 'token2', MixpanelType.EVENTS, expect.objectContaining({ properties: expect.objectContaining({ source: 'app2' }) }) ); }); }); ``` ## ๐Ÿ“ฑ Manual Testing with Sample Apps ### Step 1: Test in SimpleMixpanel Sample ```bash # Navigate to sample app cd Samples/SimpleMixpanel # Install dependencies npm install # For iOS testing cd ios && pod install && cd .. npx react-native run-ios # For Android testing npx react-native run-android ``` ### Step 2: Verify Native Integration ```javascript // Test in sample app const testNativeIntegration = () => { // Test basic tracking mixpanel.track('Sample Test Event', { test_property: 'sample_value', timestamp: Date.now() }); // Test identity mixpanel.identify('sample_user_' + Date.now()); // Test super properties mixpanel.registerSuperProperties({ app_version: '1.0.0', test_mode: true }); // Test People Analytics mixpanel.getPeople().set('name', 'Sample User'); console.log('Manual test completed'); }; ``` ### Step 3: Test JavaScript Fallback ```javascript // Force JavaScript mode for testing const testJavaScriptMode = async () => { const mixpanel = new Mixpanel('test-token', true, false); await mixpanel.init(); // Same tests as native mode mixpanel.track('JS Mode Test', { mode: 'javascript' }); // Verify console logs show JavaScript mode console.log('Check logs for JavaScript mode indicators'); }; ``` ## ๐Ÿ” Debugging Test Issues ### Common Test Debugging Steps ```javascript describe('Debug Tests', () => { it('should provide debugging information', () => { // Enable verbose logging in tests console.log('Current mock state:', { nativeModuleCalls: MixpanelReactNative.track.mock.calls, queueManagerCalls: MixpanelQueueManager.enqueue.mock.calls, asyncStorageCalls: AsyncStorage.setItem.mock.calls }); // Test with debug data mixpanel.track('Debug Event', { debug: true }); // Verify what actually happened expect(MixpanelReactNative.track).toHaveBeenCalledTimes(1); expect(MixpanelReactNative.track.mock.calls[0]).toEqual([ 'test-token', 'Debug Event', expect.objectContaining({ debug: true }) ]); }); }); ``` ### Mock Verification ```javascript // Verify mocks are working correctly beforeEach(() => { // Ensure mocks are properly reset expect(jest.isMockFunction(MixpanelReactNative.track)).toBe(true); expect(jest.isMockFunction(AsyncStorage.getItem)).toBe(true); expect(jest.isMockFunction(global.fetch)).toBe(true); }); ``` ## โœ… Testing Checklist ### Unit Testing - [ ] Input validation for all parameters - [ ] Error handling for invalid inputs - [ ] Native mode implementation calls - [ ] JavaScript mode implementation logic - [ ] Opt-out behavior respect - [ ] Promise handling (resolve/reject) - [ ] Mock verification and reset ### Integration Testing - [ ] End-to-end tracking flow - [ ] Queue management and processing - [ ] Network request handling - [ ] Multi-token isolation - [ ] Storage persistence - [ ] Error recovery scenarios ### Manual Testing - [ ] Sample app functionality (iOS) - [ ] Sample app functionality (Android) - [ ] Native module integration - [ ] JavaScript fallback mode - [ ] Console logging verification - [ ] Performance under load ### Platform Testing - [ ] iOS pod install success - [ ] Android gradle build success - [ ] React Native autolinking - [ ] TypeScript compilation - [ ] Expo compatibility ### Edge Case Testing - [ ] Storage failures - [ ] Network failures - [ ] Invalid JSON in storage - [ ] Memory pressure scenarios - [ ] App backgrounding/foregrounding - [ ] Multiple simultaneous instances ## ๐Ÿš€ Performance Testing ### Load Testing Pattern ```javascript describe('Performance Tests', () => { it('should handle high-volume event tracking', async () => { const startTime = Date.now(); // Track 1000 events for (let i = 0; i < 1000; i++) { mixpanel.track(`Event ${i}`, { index: i }); } const endTime = Date.now(); const duration = endTime - startTime; // Should complete within reasonable time expect(duration).toBeLessThan(5000); // 5 seconds // Verify all events were queued expect(MixpanelQueueManager.enqueue).toHaveBeenCalledTimes(1000); }); }); ``` This comprehensive testing workflow ensures all changes maintain the library's reliability and compatibility across both native and JavaScript implementations.