mixpanel-react-native
Version:
Official React Native Tracking Library for Mixpanel Analytics
567 lines (455 loc) โข 14.8 kB
Markdown
# 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.