Agent skill
rn-testing
Testing patterns for React Native with Jest and React Native Testing Library. Use when writing tests, mocking Expo modules, testing Zustand stores, or debugging test failures.
Install this agent skill to your Project
npx add-skill https://github.com/aiskillstore/marketplace/tree/main/skills/cjharmath/rn-testing
SKILL.md
React Native Testing
Problem Statement
React Native testing requires extensive mocking of native modules, careful handling of async operations, and understanding of Zustand store testing patterns. This codebase has 30+ test files with established patterns.
Pattern: Zustand Store Testing
Problem: Store state persists between tests, causing flaky tests.
import { useAssessmentStore } from '@/stores/assessmentStore';
const initialState = {
userAnswers: {},
completedAssessmentAnswers: {},
retakeAreas: new Set<string>(),
loading: false,
};
describe('Assessment Store', () => {
// Reset store before each test
beforeEach(() => {
useAssessmentStore.setState(initialState, true); // true = replace entire state
});
it('saves answer to store', async () => {
const store = useAssessmentStore.getState();
await store.saveAnswer('q1', 4);
expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);
});
it('enables retake for skill area', async () => {
const store = useAssessmentStore.getState();
await store.enableSkillAreaRetake('fundamentals');
expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);
});
});
Key points:
- Use
setState(initialState, true)to replace (not merge) state - Get fresh state with
getState()after async operations - Don't rely on component re-renders in store tests
Pattern: Async Store Operations
Problem: Testing async Zustand actions with proper waiting.
import { act, waitFor } from '@testing-library/react-native';
it('loads completed answers', async () => {
const store = useAssessmentStore.getState();
// Wrap async store operations in act
await act(async () => {
await store.loadCompletedAssessmentAnswers('assessment-123');
});
// Verify state after async completes
await waitFor(() => {
const state = useAssessmentStore.getState();
expect(Object.keys(state.completedAssessmentAnswers).length).toBeGreaterThan(0);
});
});
// For complex flows, verify each step
it('completes retake flow', async () => {
const store = useAssessmentStore.getState();
// Step 1
await act(async () => {
await store.loadCompletedAssessmentAnswers('assessment-123');
});
expect(useAssessmentStore.getState().completedAssessmentAnswers).toBeDefined();
// Step 2
await act(async () => {
await store.enableSkillAreaRetake('fundamentals');
});
expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);
// Step 3
await act(async () => {
await store.saveAnswer('q1', 4);
});
expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);
});
Pattern: Expo Module Mocking
Problem: Expo modules require mocks for Jest.
// __mocks__/expo-router.ts (or in jest.setup.js)
jest.mock('expo-router', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
dismiss: jest.fn(),
}),
useLocalSearchParams: () => ({}),
useSegments: () => [],
usePathname: () => '/',
Link: ({ children }: { children: React.ReactNode }) => children,
Stack: {
Screen: () => null,
},
}));
// expo-secure-store
jest.mock('expo-secure-store', () => ({
getItemAsync: jest.fn(),
setItemAsync: jest.fn(),
deleteItemAsync: jest.fn(),
}));
// expo-constants
jest.mock('expo-constants', () => ({
expoConfig: {
extra: {
apiUrl: 'http://test-api.local',
},
},
}));
// expo-haptics
jest.mock('expo-haptics', () => ({
impactAsync: jest.fn(),
notificationAsync: jest.fn(),
selectionAsync: jest.fn(),
}));
Check jest.setup.js - many mocks are already configured globally.
Pattern: React Query Testing
Problem: Components using React Query need QueryClientProvider.
// Use existing utility from codebase
import { createTestQueryClient, QueryWrapper } from '@/__tests__/utils/react-query-test-utils';
// Or create wrapper
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
}
function QueryWrapper({ children }: { children: React.ReactNode }) {
const queryClient = createTestQueryClient();
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
// Usage in tests
import { render, waitFor } from '@testing-library/react-native';
it('fetches and displays data', async () => {
const { getByText } = render(
<QueryWrapper>
<MyComponent />
</QueryWrapper>
);
await waitFor(() => {
expect(getByText('Loaded data')).toBeTruthy();
});
});
Pattern: Custom Hook Testing
import { renderHook, act, waitFor } from '@testing-library/react-native';
describe('useAuth', () => {
it('signs in user', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider, // If hook needs context
});
await act(async () => {
await result.current.signIn('token', mockUser);
});
expect(result.current.user).toEqual(mockUser);
expect(result.current.token).toBe('token');
});
});
// Hook with Zustand
describe('useAssessmentAnswers', () => {
beforeEach(() => {
useAssessmentStore.setState(initialState, true);
});
it('returns current answers', () => {
// Pre-populate store
useAssessmentStore.setState({ userAnswers: { q1: 4 } });
const { result } = renderHook(() => useAssessmentAnswers());
expect(result.current.answers).toEqual({ q1: 4 });
});
});
Pattern: Component Testing
import { render, fireEvent, waitFor } from '@testing-library/react-native';
describe('SessionCard', () => {
const mockSession = {
id: '1',
title: 'Serve Practice',
totalDuration: 45, // Backend-calculated
};
it('displays session data from backend', () => {
const { getByText } = render(<SessionCard session={mockSession} />);
expect(getByText('Serve Practice')).toBeTruthy();
expect(getByText('45 min')).toBeTruthy();
});
it('calls onPress when tapped', () => {
const onPress = jest.fn();
const { getByTestId } = render(
<SessionCard session={mockSession} onPress={onPress} />
);
fireEvent.press(getByTestId('session-card'));
expect(onPress).toHaveBeenCalledWith(mockSession.id);
});
});
Pattern: Navigation Testing
import { useRouter } from 'expo-router';
jest.mock('expo-router');
describe('SettingsScreen', () => {
const mockPush = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useRouter as jest.Mock).mockReturnValue({
push: mockPush,
back: jest.fn(),
});
});
it('navigates to profile on button press', () => {
const { getByText } = render(<SettingsScreen />);
fireEvent.press(getByText('Edit Profile'));
expect(mockPush).toHaveBeenCalledWith('/profile/edit');
});
});
// Testing with route params
jest.mock('expo-router', () => ({
useLocalSearchParams: () => ({ id: 'test-assessment-123' }),
useRouter: () => ({ push: jest.fn() }),
}));
Pattern: Avoiding act() Warnings
Problem: "Warning: An update inside a test was not wrapped in act(...)"
// WRONG - state update happens after test
it('loads data', () => {
render(<DataComponent />);
// Component fetches data async, updates state after test ends
});
// CORRECT - wait for async completion
it('loads data', async () => {
const { getByText } = render(<DataComponent />);
// Wait for loading to complete
await waitFor(() => {
expect(getByText('Data loaded')).toBeTruthy();
});
});
// CORRECT - use findBy* (has built-in waitFor)
it('loads data', async () => {
const { findByText } = render(<DataComponent />);
const element = await findByText('Data loaded');
expect(element).toBeTruthy();
});
Pattern: Snapshot Testing
When to use:
- UI components with stable structure
- Design system components
- Components where visual regression matters
When to avoid:
- Components with dynamic content
- Components that change frequently
- Large component trees (brittle)
// Good snapshot candidate - stable UI component
it('renders correctly', () => {
const tree = render(<Button title="Submit" />);
expect(tree.toJSON()).toMatchSnapshot();
});
// Bad snapshot candidate - dynamic content
it('renders user list', () => {
// Don't snapshot - list content varies
// Instead, test specific behaviors
});
Pattern: Mocking API Calls
// Mock Orval-generated hooks
jest.mock('@/api/generated/assessments', () => ({
useGetAssessment: jest.fn(() => ({
data: mockAssessment,
isLoading: false,
error: null,
})),
useSubmitAssessment: jest.fn(() => ({
mutate: jest.fn(),
isLoading: false,
})),
}));
// Mock with different states
import { useGetAssessment } from '@/api/generated/assessments';
it('shows loading state', () => {
(useGetAssessment as jest.Mock).mockReturnValue({
data: null,
isLoading: true,
error: null,
});
const { getByTestId } = render(<AssessmentScreen />);
expect(getByTestId('loading-spinner')).toBeTruthy();
});
it('shows error state', () => {
(useGetAssessment as jest.Mock).mockReturnValue({
data: null,
isLoading: false,
error: new Error('Failed to load'),
});
const { getByText } = render(<AssessmentScreen />);
expect(getByText('Failed to load')).toBeTruthy();
});
Recommended Test Utilities
__tests__/utils/react-query-test-utils.tsx # QueryClient wrapper
jest.setup.js # Global mocks
Test Commands
npm test # Run all tests
npm test -- --watch # Watch mode
npm test -- --coverage # Coverage report
npm test -- SessionCard # Run specific test file
npm test -- --updateSnapshot # Update snapshots
Common Issues
| Issue | Solution |
|---|---|
| "Cannot find module" | Check jest.setup.js module mappings |
| act() warning | Wrap state updates in act(), use waitFor/findBy |
| Store state bleeding | Add beforeEach with setState reset |
| Async test timeout | Increase timeout or check for hanging promises |
| Mock not working | Verify mock path matches import path exactly |
Relationship to Other Skills
- rn-async-patterns: Use post-condition validation in tests to verify async flows
- rn-zustand-patterns: Understand
getState()behavior for store tests - rn-state-flows: Integration tests should cover entire flows, not just units
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
perigon-backend
Perigon ASP.NET Core + EF Core + Aspire conventions
perigon-agent
Pointers for Copilot/agents to apply Perigon conventions
perigon-angular
Angular 21+ standalone/Material/signal conventions for Perigon WebApp
fastapi-mastery
Comprehensive FastAPI development skill covering REST API creation, routing, request/response handling, validation, authentication, database integration, middleware, and deployment. Use when working with FastAPI projects, building APIs, implementing CRUD operations, setting up authentication/authorization, integrating databases (SQL/NoSQL), adding middleware, handling WebSockets, or deploying FastAPI applications. Triggered by requests involving .py files with FastAPI code, API endpoint creation, Pydantic models, or FastAPI-specific features.
context7-efficient
Token-efficient library documentation fetcher using Context7 MCP with 86.8% token savings through intelligent shell pipeline filtering. Fetches code examples, API references, and best practices for JavaScript, Python, Go, Rust, and other libraries. Use when users ask about library documentation, need code examples, want API usage patterns, are learning a new framework, need syntax reference, or troubleshooting with library-specific information. Triggers include questions like "Show me React hooks", "How do I use Prisma", "What's the Next.js routing syntax", or any request for library/framework documentation.
browser-use
Browser automation using Playwright MCP. Navigate websites, fill forms, click elements, take screenshots, and extract data. Use when tasks require web browsing, form submission, web scraping, UI testing, or any browser interaction.
Didn't find tool you were looking for?