- Overview
- Prerequisites
- NX Workspace Testing Setup
- Jest Configuration
- Frontend Testing Types
- Step-by-Step Implementation
- Testing Best Practices
- Benefits of Unit Testing
- Limitations and Considerations
- Troubleshooting
- Conclusion
This document provides a comprehensive guide for implementing unit testing in NX workspace using Jest, specifically focused on frontend applications. NX provides excellent built-in support for Jest testing with optimized configurations and parallel execution capabilities.
- NX workspace (version 20.2.2 or higher)
- Node.js (version 18 or higher)
- TypeScript support
- React/Next.js applications
- Basic understanding of unit testing concepts
NX automatically configures Jest through the @nx/jest/plugin in your nx.json:
{
"plugins": [
{
"plugin": "@nx/jest/plugin",
"options": {
"targetName": "test"
}
}
]
}The workspace uses a centralized Jest preset (jest.preset.js):
const nxPreset = require('@nx/jest/preset').default;
module.exports = { ...nxPreset };Each project has its own jest.config.ts that extends the workspace preset:
export default {
displayName: 'project-name',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/project-name',
};const config: Config = {
// Clear mocks between tests
clearMocks: true,
// Enable coverage collection
collectCoverage: true,
coverageDirectory: 'coverage',
coverageProvider: 'v8',
// Test environment for React components
testEnvironment: 'jsdom',
// Setup files for test environment
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
// Module name mapping for path aliases
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
// Transform configuration
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
};{
"devDependencies": {
"@nx/jest": "20.2.2",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "15.0.6",
"@testing-library/user-event": "^14.5.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.1.0",
"@types/jest": "^29.5.12"
}
}- Purpose: Test React components in isolation
- Tools: React Testing Library, Jest
- What to test: Rendering, props, user interactions, state changes
- Purpose: Test custom React hooks
- Tools: @testing-library/react-hooks
- What to test: Hook behavior, state updates, side effects
- Purpose: Test pure functions and utilities
- Tools: Jest
- What to test: Input/output, edge cases, error handling
- Purpose: Test API calls and data fetching
- Tools: Jest with mocking
- What to test: API calls, error handling, data transformation
- Purpose: Test component interactions
- Tools: React Testing Library
- What to test: Component communication, user workflows
Create jest.setup.ts in your project root:
import '@testing-library/jest-dom';
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
};Create src/test-utils.tsx:
import React, { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Mock providers for testing
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) => render(ui, { wrapper: AllTheProviders, ...options });
export * from '@testing-library/react';
export { customRender as render };Example component test (Button.test.tsx):
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button Component', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('applies disabled state correctly', () => {
render(<Button disabled>Disabled button</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});Example hook test (useCounter.test.ts):
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('should increment count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});Example API test (api.test.ts):
import { fetchUserData } from './api';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ id: '1', name: 'John Doe' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('API Service', () => {
it('fetches user data successfully', async () => {
const userData = await fetchUserData('1');
expect(userData).toEqual({ id: '1', name: 'John Doe' });
});
});# Run all tests
nx test
# Run tests for specific project
nx test project-name
# Run tests in watch mode
nx test project-name --watch
# Run tests with coverage
nx test project-name --coverage
# Run tests in CI mode
nx test project-name --cidescribe('ComponentName', () => {
it('should do something specific', () => {
// Arrange - Set up test data and mocks
const mockProps = { title: 'Test Title' };
// Act - Execute the function or render component
render(<Component {...mockProps} />);
// Assert - Verify the expected outcome
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
});// Good
it('should display error message when API call fails');
// Bad
it('should work');// Good - Test what user sees
expect(screen.getByRole('button')).toBeInTheDocument();
// Bad - Test implementation details
expect(component.state.isVisible).toBe(true);// Only when semantic queries don't work
<button data-testid="submit-button">Submit</button>// Mock API calls
jest.mock('./api', () => ({
fetchData: jest.fn(),
}));
// Mock modules
jest.mock('next/router', () => ({
useRouter: () => ({
push: jest.fn(),
pathname: '/test',
}),
}));- Catch bugs during development phase
- Reduce production issues
- Faster debugging process
- Forces better code structure
- Encourages modular design
- Improves maintainability
- Tests serve as living documentation
- Show expected behavior
- Help new developers understand code
- Safe to refactor with test coverage
- Regression prevention
- Maintain functionality during changes
- Faster debugging
- Automated testing
- Continuous integration support
- Shared understanding of requirements
- Reduced code review time
- Better code quality standards
- Cannot test visual appearance: Jest can't verify CSS styling or visual design
- Limited browser testing: No real browser environment
- Mock limitations: Mocks may not perfectly replicate real behavior
- Integration gaps: Unit tests don't catch integration issues
- Test execution time: Large test suites can be slow
- Memory usage: Each test runs in isolation
- CI/CD impact: Tests must complete before deployment
- Initial setup time: 15-25% increase in initial development time for test setup and configuration
- Ongoing development: 20-40% increase in feature development time due to writing tests
- Learning curve: 2-4 weeks additional time for team members to learn testing best practices
- Test maintenance: 10-15% additional time for maintaining and updating existing tests
- Debugging tests: 5-10% additional time spent debugging test failures and flaky tests
- Test maintenance: Tests need updates when code changes
- False positives: Tests may fail due to implementation changes
- Test complexity: Complex tests can be hard to maintain
- 100% coverage doesn't mean bug-free: Edge cases may be missed
- Integration testing needed: Unit tests don't cover system interactions
- User experience testing: Cannot test actual user workflows
- DOM manipulation: Complex DOM interactions are hard to test
- Async operations: Testing async code requires careful handling
- State management: Complex state interactions can be difficult to test
- Third-party libraries: Some libraries are hard to mock effectively
// Add to jest.config.ts
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@shared/(.*)$': '<rootDir>/../../libs/shared-lib/src/$1',
}// Add to jest.config.ts
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
}// Add to jest.setup.ts
process.env.NODE_ENV = 'test';
process.env.API_URL = 'http://localhost:3000';// Use async/await
it('should handle async operations', async () => {
const { result } = renderHook(() => useAsyncData());
await waitFor(() => {
expect(result.current.data).toBeDefined();
});
});// Clean up after tests
afterEach(() => {
cleanup();
jest.clearAllMocks();
});Unit testing in NX workspace with Jest provides a robust foundation for frontend application testing. While it has limitations, the benefits significantly outweigh the challenges when implemented correctly. Focus on testing user behavior, maintain good test coverage, and complement unit tests with integration and end-to-end tests for comprehensive quality assurance.
- Use NX's built-in Jest configuration for optimal performance
- Focus on testing user behavior rather than implementation details
- Maintain good test coverage but don't obsess over 100%
- Complement unit tests with other testing strategies
- Keep tests simple, readable, and maintainable
- Use proper mocking and test utilities for complex scenarios
This testing strategy will help ensure your frontend applications are reliable, maintainable, and bug-free while supporting your team's development workflow.