Testing & Code Coverage
OpenCloud Mobile uses Jest for unit and integration testing along with @testing-library/react-native for component testing. This document outlines our testing approach, best practices, and how to run tests efficiently.
Running Tests
We have several npm scripts for different testing scenarios:
# Run tests in watch mode (default for development)
npm test
# Run tests in fast mode (optimized performance)
npm run test:fast -- <file-pattern>
# Run tests with coverage report
npm run test:coverage
# Run HttpUtil tests only
npm run test:http
# Run tests in CI mode (for continuous integration)
npm run test:ci
Test Structure
Tests are organized in __tests__
directories next to the files they test:
services/
__tests__/
AuthService-test.ts
OidcService-test.ts
WebFingerService-test.ts
api/
__tests__/
ApiService-test.ts
HttpUtil-test.ts
AuthService.ts
OidcService.ts
WebFingerService.ts
Writing Effective Tests
Test File Naming
- Component tests:
ComponentName-test.tsx
- Service/utility tests:
ServiceName-test.ts
Component Testing
For React component testing, we use @testing-library/react-native:
import React from 'react';
import { render } from '@testing-library/react-native';
import MyComponent from '../MyComponent';
describe('MyComponent', () => {
it('renders correctly', () => {
const { getByText } = render(<MyComponent />);
// Check for specific text elements
expect(getByText('Expected Text')).toBeTruthy();
});
});
Test Structure Example
describe('ServiceName', () => {
// Setup before tests
beforeEach(() => {
// Initialize or reset test state
});
// Group related tests
describe('methodName', () => {
it('should do something specific', () => {
// Arrange - set up test preconditions
const input = 'some input';
// Act - perform the action
const result = ServiceName.methodName(input);
// Assert - check the results
expect(result).toBe('expected output');
});
});
});
Mocking
Create helper functions for repeated mocks:
// Helper functions for creating mock responses
function createMockHeaders(requestId = "test-request-id") {
return {
get: (name: string) => {
if (name.toLowerCase() === 'x-request-id') {
return requestId;
}
return null;
},
entries: () => {
return [['content-type', 'application/json'], ['x-request-id', requestId]];
}
};
}
function createSuccessResponse(data: any, status = 200, statusText = "OK") {
return {
ok: true,
status,
statusText,
json: async () => data,
headers: createMockHeaders(),
};
}
Code Coverage
Running Coverage Report
npm run test:coverage
This will generate a coverage report showing:
- Statement coverage
- Branch coverage
- Function coverage
- Line coverage
Coverage Goals
We aim for the following coverage metrics:
- Core services: 90%+ line coverage
- Utility functions: 80%+ line coverage
- UI components: 70%+ line coverage
Configuration
Our coverage settings are configured in jest.config.js
:
collectCoverageFrom: [
'services/**/*.ts',
'!services/**/__tests__/**/*.ts',
'config/**/*.ts',
'!config/**/__tests__/**/*.ts'
],
coverageReporters: ['text', 'lcov'],
Testing HTTP Services
When testing HTTP services:
- Mock the
fetch
API using Jest mocks - Create helper functions for standard responses
- Verify proper headers, request parameters, and error handling
- Test edge cases like network errors, timeout, and unexpected responses
- Verify that Request IDs are properly included in error objects
Example:
// Mock fetch
global.fetch = jest.fn();
// In test setup
(global.fetch as jest.Mock).mockResolvedValueOnce(
createSuccessResponse({ name: "test" })
);
// Then test the service
const result = await apiService.fetchData();
expect(result.name).toBe("test");
// Verify fetch was called correctly
expect(global.fetch).toHaveBeenCalledWith(
"https://api.example.com/data",
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
"X-Request-ID": expect.any(String)
})
})
);
Testing Error Handling with Request IDs
When testing API error handling, be sure to test that Request IDs are properly captured and included in error objects:
describe('handleApiResponse', () => {
it('should throw ApiError with request ID for failed responses', () => {
// Arrange
const response = {
ok: false,
status: 404,
statusText: "Not Found",
headers: new Headers({
"x-request-id": "test-request-id-123"
})
} as Response;
// Act & Assert
try {
ApiService.handleApiResponse(response, "Test operation");
fail("Expected error was not thrown");
} catch (error) {
// Check that the error is an ApiError with the correct properties
expect(error).toBeInstanceOf(Error);
expect((error as ApiError).requestId).toBe("test-request-id-123");
expect(error.message).toBe("Test operation: 404 Not Found");
}
});
it('should use "unknown" as fallback when request ID is missing', () => {
// Arrange
const response = {
ok: false,
status: 500,
statusText: "Internal Server Error",
headers: new Headers() // No request ID header
} as Response;
// Act & Assert
try {
ApiService.handleApiResponse(response, "Test operation");
fail("Expected error was not thrown");
} catch (error) {
// Check that the error has a fallback request ID
expect((error as ApiError).requestId).toBe("unknown");
}
});
});
Optimizing Test Performance
We've implemented several optimizations to improve test speed:
maxWorkers: '50%'
- Uses 50% of available CPU cores--no-watchman
flag for fast runs- Fake timers for timer-heavy tests
- Caching for faster repeated runs
Code Quality and Linting
We use ESLint to maintain code quality standards. To run the linter:
npx expo lint
ESLint Guidelines
- Fix all errors and warnings when possible
- When ESLint indicates unused variables or imports:
- If they'll be needed in the future, keep them with appropriate comments
- Use
// eslint-disable-next-line
with explanation comments
Example for documenting intentionally unused imports:
// WebFingerService is used for discovery through OidcService's discoverConfiguration function
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { WebFingerService } from './WebFingerService';
Example for documenting intentionally excluded useEffect dependencies:
useEffect(() => {
// Effect implementation...
// loadData is intentionally excluded from deps to only load data on mount
// and prevent unnecessary API requests. Manual refresh is available via pull-to-refresh.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
For large test suites, use the test:fast
command with a specific pattern:
npm run test:fast -- services/api
Continuous Integration
The test:ci
command is optimized for continuous integration environments. It provides:
- No watch mode
- Fail fast behavior
- JUnit report output for CI tools
- Coverage enforcement
This is typically used in GitHub Actions or other CI workflows.