# Performance Optimization Standards for Jest
This document outlines the coding standards and best practices focused on **performance optimization** when using Jest for testing JavaScript and TypeScript code. Adhering to these standards will result in faster test suites, reduced resource consumption, and improved developer experience. This guide complements other Jest coding standards and concentrates solely on performance considerations.
## 1. Strategic Test Design
The foundation of performant testing lies in how tests are designed and structured.
### 1.1. Minimize Test Scope
**Standard:** Keep tests focused on a single unit of functionality (unit tests) or a specific user interaction (integration tests). Avoid large, end-to-end tests unless absolutely necessary.
**Why:** Narrowly scoped tests execute faster and provide more accurate feedback. A failure pinpoints the exact source of the problem without requiring extensive debugging.
**Do This:**
* Write focused unit tests that exercise individual functions or components.
* Use integration tests to verify interactions between modules, but keep them targeted.
**Don't Do This:**
* Create large, monolithic tests that cover multiple aspects of the application simultaneously.
* Rely solely on end-to-end tests for verifying all functionality.
**Example:**
"""javascript
// Good: Focused unit test
describe('formatDate', () => {
it('should format a date object correctly', () => {
const date = new Date(2024, 0, 1, 12, 0, 0);
expect(formatDate(date)).toBe('2024-01-01 12:00:00');
});
});
// Bad: Overly broad test
describe('User Registration', () => {
it('should register a user and display a success message', () => {
// Simulate user input
// Click multiple buttons/links
// Assert visual elements on multiple pages
});
});
"""
### 1.2. Test Pyramid Principle
**Standard:** Adhere to the Test Pyramid, prioritizing unit tests, then integration tests, and finally end-to-end tests.
**Why:** Unit tests are much faster and more isolated than integration or end-to-end tests. Aim for a large base of unit tests to cover core logic efficiently.
**Do This:**
* Design your test suite to have the largest percentage of tests as unit tests.
* Use integration tests sparingly for critical interactions between modules.
* Reserve end-to-end tests for essential user workflows.
**Don't Do This:**
* Rely primarily on end-to-end tests, which are slow and brittle.
* Neglect unit testing in favor of integration or end-to-end testing.
### 1.3. Prioritize Important Tests
**Standard:** Identify the most critical tests and ensure they run quickly.
**Why:** Fast feedback on critical functionality is essential for rapid development. Slow critical tests bottleneck the development workflow.
**Do This:**
* Tag important tests using ".only" during development and focus on optimizing them first. Remove ".only" before committing.
* Use "--findRelatedTests" or "--onlyChanged" to focus only on necessary tests during local development.
**Don't Do This:**
* Ignore the performance of critical tests.
* Leave ".only" on critical tests in the build pipeline.
## 2. Optimized Mocking Strategies
Mocking is a crucial part of unit testing, but inefficient mocking can significantly slow down test execution.
### 2.1. Minimize Mocking
**Standard:** Mock only what is necessary to isolate the unit under test. Avoid over-mocking dependencies.
**Why:** Creating and managing mocks incurs overhead. Excessive mocking increases test complexity and slows down execution.
**Do This:**
* Mock external dependencies (e.g., APIs, databases, third-party libraries).
* Avoid mocking internal components with stable interfaces.
* Consider using real implementations where appropriate (e.g., in-memory databases for integration tests).
**Don't Do This:**
* Mock every dependency, regardless of its impact on the test.
* Mock implementation details that are not part of the unit's public API.
**Example:**
"""javascript
// Good: Mock the external API
jest.mock('./api'); // Mock the entire module
import { fetchUserData } from './user';
import { getUser } from './api';
describe('fetchUserData', () => {
it('should fetch user data from the API', async () => {
(getUser as jest.Mock).mockResolvedValue({ id: 123, name: 'John Doe' }); //mockResolvedValue once required for TS
const userData = await fetchUserData(123);
expect(userData).toEqual({ id: 123, name: 'John Doe' });
expect(getUser).toHaveBeenCalledWith(123);
});
});
// Bad: Over-mocking (unnecessary mock)
jest.mock('./utils'); // Mock the utils module even when not relevant
"""
### 2.2. Use "jest.mock" Wisely
**Standard:** Prefer "jest.mock" at the top of the test file for global mocking, but use "jest.spyOn" or manual mocks for more granular control.
**Why:** "jest.mock" hoists the mock to the top of the file, ensuring it's applied before any code is executed, avoiding timing issues and improving performance by allowing Jest to optimize mocking behavior. However, it mocks the entire module. For more granular control, use "jest.spyOn" or manual mocks.
**Do This:**
* Use "jest.mock" for global mocks that apply to the entire test suite.
* Use "jest.spyOn" to mock specific methods of an object.
* Create manual mocks using "__mocks__" directories for complex mocking scenarios.
* Consider "jest.unmock()" to selectively disable mocks in specific tests.
**Don't Do This:**
* Use "jest.mock" inside "it" blocks, as it can lead to unpredictable behavior.
* Overuse "jest.spyOn" when a global mock would suffice.
**Example:**
"""javascript
// Good: Global mock using jest.mock (Typescript)
jest.mock('./api');
import { fetchUserData } from './user';
import { getUser } from './api';
describe('fetchUserData', () => {
it('should fetch user data from the API', async () => {
(getUser as jest.Mock).mockResolvedValue({ id: 123, name: 'John Doe' });
const userData = await fetchUserData(123);
expect(userData).toEqual({ id: 123, name: 'John Doe' });
expect(getUser).toHaveBeenCalledWith(123);
});
});
// Good: Spying on a method
import * as myModule from './myModule';
describe('myFunction', () => {
it('should call the helper function', () => {
const helperSpy = jest.spyOn(myModule, 'helperFunction');
myFunction();
expect(helperSpy).toHaveBeenCalled();
});
});
// Good: Manual mock using __mocks__
// File structure:
// - myModule.js
// - __mocks__/myModule.js
// - myModule.test.js
// myModule.js
export const realFunction = () => {
// ... some implementation
};
// __mocks__/myModule.js
export const realFunction = jest.fn(() => 'mocked value');
// myModule.test.js
import { realFunction } from './myModule';
describe('realFunction', () => {
it('should use the mocked version', () => {
expect(realFunction()).toBe('mocked value');
});
});
"""
### 2.3. Mock Implementations for Performance
**Standard:** Use mock implementations with minimal overhead for computationally expensive or I/O-bound operations.
**Why:** Default mocks can have significant performance overhead, especially when used with complex functions or large data sets. Providing a simple mock implementation can drastically improve test speed.
**Do This:**
* Use "mockImplementation" or "mockImplementationOnce" to provide custom mock implementations.
* Ensure that mock implementations return the expected data type to avoid runtime errors.
**Don't Do This:**
* Use default mocks for functions that perform complex calculations or I/O operations.
* Create mock implementations that inadvertently introduce performance bottlenecks.
**Example:**
"""javascript
// Good: Mock implementation
import { expensiveFunction } from './utils';
jest.mock('./utils', () => ({
expensiveFunction: jest.fn().mockImplementation(() => 'mocked result'),
}));
describe('myComponent', () => {
it('should use the mocked result', () => {
expect(expensiveFunction()).toBe('mocked result');
});
});
"""
## 3. Optimized Test Setup and Teardown
Proper setup and teardown of test environments can significantly affect test performance.
### 3.1. Use "beforeAll" and "afterAll" for Global Setup/Teardown
**Standard:** Use "beforeAll" and "afterAll" for setup and teardown operations that apply to the entire test suite.
**Why:** "beforeAll" and "afterAll" execute only once per test suite, minimizing overhead compared to "beforeEach" and "afterEach", which run before/after each test.
**Do This:**
* Use "beforeAll" to initialize shared resources, such as database connections or mock servers.
* Use "afterAll" to clean up shared resources and prevent resource leaks.
* Use "beforeEach" and "afterEach" only for per-test setup and teardown.
**Don't Do This:**
* Perform global setup/teardown operations inside "beforeEach" or "afterEach".
* Neglect to clean up shared resources after each test suite.
**Example:**
"""javascript
let dbConnection;
beforeAll(async () => {
dbConnection = await connectToDatabase(); //returns a Promise
});
afterAll(async () => {
await dbConnection.close(); //returns a Promise
});
describe('User Model', () => {
it('should create a new user', async () => { //async test
const user = await createUser({ name: 'John Doe' }); //returns a Promise
expect(user.name).toBe('John Doe');
});
});
"""
### 3.2. Optimize Database Interactions
**Standard:** Minimize database interactions within tests. Use in-memory databases for integration tests or mock database calls for unit tests.
**Why:** Database operations are typically slow and can significantly impact test performance. Reducing the number of database interactions and using faster alternatives improves overall test execution time.
**Do This:**
* Use in-memory databases (e.g., SQLite in-memory mode) for integration tests.
* Mock database calls for unit tests to avoid real database interactions.
* Batch database operations where possible to minimize round trips.
**Don't Do This:**
* Use real databases for unit tests.
* Perform excessive database queries within each test.
**Example:**
"""javascript
// Setup in-memory SQLite database
const sequelize = new Sequelize('sqlite::memory:');
beforeAll(async () => {
await sequelize.sync({ force: true }); // Create tables
});
afterAll(async () => {
await sequelize.close();
});
"""
### 3.3. Avoid External API Calls
**Standard:** Avoid making real API calls during tests. Mock API responses to ensure test isolation and improve performance.
**Why:** API calls are slow and depend on external factors, making tests unreliable and slow. Mocking API responses provides consistent and fast test results.
**Do This:**
* Use "jest.mock" to mock the API client module.
* Use "nock" or similar libraries to intercept and mock HTTP requests.
**Don't Do This:**
* Make real API calls during unit or integration tests.
* Rely on external services being available and responsive during tests.
## 4. Optimizing Asynchronous Tests
Asynchronous tests, if not handled correctly, can contribute significantly to test suite slow downs.
### 4.1. Use "async/await" for Clarity and Performance
**Standard:** Prefer "async/await" syntax for asynchronous tests instead of callbacks or Promises that are not easily handled.
**Why:** "async/await" makes asynchronous code more readable and easier to debug, while also providing better performance compared to traditional callback-based approaches.
**Do This:**
* Declare test functions as "async".
* Use "await" to wait for Promises to resolve.
* Use "try/catch" blocks for error handling within asynchronous tests.
**Don't Do This:**
* Use callbacks for asynchronous tests.
* Forget to handle errors in asynchronous tests.
**Example:**
"""javascript
// Good: Async/await
describe('fetchData', () => {
it('should fetch data successfully', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
it('should handle errors gracefully', async () => {
try {
await fetchData();
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
});
});
"""
### 4.2. Properly Handle Promises
**Standard:** Ensure that asynchronous tests resolve or reject Promises correctly. Use "resolves" and "rejects" matchers for cleaner assertions.
**Why:** Unresolved Promises can cause tests to hang indefinitely, leading to wasted time and resources. Properly handling Promises ensures that tests complete reliably and provide accurate results.
**Do This:**
* Use "resolves" and "rejects" matchers to assert Promise outcomes.
* Return Promises from test functions to signal asynchronous completion.
* Use "jest.runAllTimers()" to resolve pending timers (when using "setTimeout" or "setInterval").
**Don't Do This:**
* Forget to return Promises from test functions.
* Ignore unhandled Promise rejections.
**Example:**
"""javascript
// Good : Using resolves/rejects
it('should resolve with correct data', () => {
return expect(fetchData()).resolves.toEqual({ data: 'test' });
});
it('should reject with an error', () => {
return expect(fetchData()).rejects.toThrow('error');
});
// Good: Handling timers
it('should call the callback after 1 second', () => {
jest.useFakeTimers(); // Important when testing timers
const callback = jest.fn();
setTimeout(callback, 1000);
// Fast-forward until all timers have been executed
jest.runAllTimers();
expect(callback).toHaveBeenCalled();
});
"""
## 5. Code Splitting and Dynamic Imports
Code splitting and dynamic imports are not directly test-related but influence the size of code under test which impacts performance.
### 5.1 Test Code Splitted Modules Effectively
**Standard:** Ensure that code splitting is properly verified in integration tests, without resorting to complex mocking.
**Why:** Verify that different chunks of code actually resolve and work together as expected when testing UI code that uses dynamic "import()" statements. Proper test design can avoid the need to create overly complex mocks that attempt to replicate module loading. Instead treat modules as black boxes with defined inputs and expected outputs.
**Do This:**
* Design integration tests that load code-split modules dynamically use user interaction.
* Verify end-to-end functionality that depends on code-split components.
**Don't Do This:**
* Attempt to directly manipulate modules with testing tools to test module resolution.
**Example:**
A UI button that loads a module on demand,
"""jsx
// ComponentWithLazyLoading.jsx
import React, { useState } from 'react';
function ComponentWithLazyLoading() {
const [ComponentToLoad, setComponentToLoad] = useState(null);
const loadComponent = async () => {
const { default: LoadedComponent } = await import('./MyLazyLoadedComponent');
setComponentToLoad(() => LoadedComponent);
};
return (
Load Component
{ComponentToLoad && }
);
}
export default ComponentWithLazyLoading;
"""
"""jsx
// ComponentWithLazyLoading.test.jsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ComponentWithLazyLoading from './ComponentWithLazyLoading';
// This test verifies that clicking the button loads the lazy component
it('loads the lazy component when the button is clicked', async () => {
render();
const button = screen.getByText('Load Component');
fireEvent.click(button);
// Wait for the lazy component to be loaded and rendered
await waitFor(() => {
expect(screen.getByText('I am a lazy loaded component!')).toBeInTheDocument();
});
});
"""
## 6. Jest Configuration Optimization
Jest's configuration options have a significant impact on test performance. Tuning these settings can yield substantial improvements.
### 6.1. Configure "testEnvironment"
**Standard:** Set the "testEnvironment" option appropriately for your project. Use "node" for backend tests and "jsdom" for frontend tests.
**Why:** Choosing the correct test environment optimizes the runtime environment for your tests. "jsdom" emulates a browser environment, while "node" provides a Node.js environment.
**Do This:**
* Set "testEnvironment: 'node'" in "jest.config.js" for backend tests.
* Set "testEnvironment: 'jsdom'" for frontend tests that require a browser environment.
**Don't Do This:**
* Use the default test environment without considering your project's needs.
* Use "jsdom" for backend tests or "node" for frontend tests that rely on browser APIs
"""javascript
// jest.config.js
module.exports = {
testEnvironment: 'node', // Or 'jsdom'
};
"""
### 6.2. Use "transform" for File Processing
**Standard:** Configure the "transform" option to specify how Jest should process different file types.
**Why:** The "transform" option allows you to use Babel, TypeScript, or other tools to transpile your code before running tests. Ensure that the transformations are optimized for performance.
**Do This:**
* Use "babel-jest" for transpiling JavaScript and TypeScript code.
* Configure Babel to cache transformations for faster subsequent test runs:
"""javascript
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
'@babel/preset-react',
],
plugins: ['@babel/plugin-transform-runtime'],
};
"""
**Don't Do This:**
* Neglect to configure the "transform" option, leading to slow test execution.
* Use inefficient transformations that negatively impact test performance.
"""javascript
// jest.config.js
module.exports = {
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
};
"""
### 6.3. Consider "cacheDirectory"
**Standard:** Set the "cacheDirectory" option to enable caching of transformed modules.
**Why:** Enabling caching significantly speeds up test runs by reusing transformed modules from previous runs.
**Do This:**
* Set "cacheDirectory" to a persistent directory, such as ".jest-cache".
**Don't Do This:**
* Disable caching unless necessary for debugging purposes.
* Use a temporary directory for "cacheDirectory", as it defeats the purpose of caching.
"""javascript
// jest.config.js
module.exports = {
cacheDirectory: '.jest-cache',
};
"""
### 6.4. Choose the Right Watch Mode
**Standard:** Understand and utilize Jest's watch modes effectively.
**Why:** Watch mode can significantly speed up development by only running tests related to changed files. However, inefficient use can lead to overhead.
**Do This:**
* Use "--watch" (the default) for general development. Jest intelligently re-runs tests based on file changes.
* Use "--watchAll" to run all tests on every change (useful in CI environments or for ensuring complete coverage).
* Consider "--onlyChanged" or "--findRelatedTests" for targeted test runs during local development.
**Don't Do This:**
* Use "--watchAll" unnecessarily during local development.
* Ignore using "--onlyChanged" when working on specific features.
## 7. Performance Monitoring and Profiling
Regular monitoring and profiling of test performance are crucial for identifying and addressing bottlenecks.
### 7.1. Use Jest's "--profile" Option
**Standard:** Use the "--profile" option to generate performance profiles of your test runs.
**Why:** The "--profile" option provides detailed insights into test execution time, allowing you to identify slow tests and optimize their performance.
**Do This:**
* Run "jest --profile" to generate a performance profile.
* Analyze the profile to identify slow tests and bottlenecks.
* Use profiling tools to drill down into the performance characteristics of individual tests.
**Don't Do This:**
* Ignore performance monitoring and profiling.
* Fail to address identified performance bottlenecks.
### 7.2. Track Test Execution Time
**Standard:** Track test execution time over time to identify performance regressions.
**Why:** Monitoring test execution time helps you detect and address performance problems before they impact the development workflow.
**Do This:**
* Use CI/CD systems to track test execution time.
* Set up alerts to notify you of significant performance regressions.
**Don't Do This:**
* Ignore test execution time trends.
* Fail to investigate and address performance regressions.
By adhering to these performance optimization standards, development teams can create faster, more reliable, and more maintainable test suites, ultimately leading to improved software quality. The latest versions of Jest have focused on performance improvements, so leverage the most up-to-date features and configurations. Remember to continuously monitor and profile test performance to identify and address bottlenecks as they arise.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Jest This document outlines the coding standards for designing components in Jest. These standards aim to promote reusable, maintainable, and testable components, taking into account modern Jest practices. ## 1. Component Structure and Organization ### 1.1. Standard: Single Responsibility Principle (SRP) **Do This:** Design components that have a single, well-defined responsibility. A component should do one thing and do it well. **Don't Do This:** Create "god components" that handle multiple unrelated tasks. **Why:** Components adhering to SRP are easier to understand, test, and reuse. Changes to one component are less likely to impact other parts of the application. **Example:** """jsx // Good: Button component only handles button-related logic function Button({ onClick, children, disabled }) { return ( <button onClick={onClick} disabled={disabled}> {children} </button> ); } // Bad: Component handling button logic and authentication function MultiPurposeComponent({ onClick, children, disabled, authenticate }) { const handleClick = () => { if (authenticate()) { onClick(); } } return ( <button onClick={handleClick} disabled={disabled}> {children} </button> ); } """ ### 1.2. Standard: Component Composition **Do This:** Favor composition over inheritance. Build complex components by composing smaller, reusable components. **Don't Do This:** Create deep inheritance hierarchies that are difficult to understand and maintain. **Why:** Composition allows for greater flexibility and reusability. It avoids the rigid structure imposed by inheritance. **Example:** """jsx // Good: Composing a Layout component with other components function Layout({ children }) { return ( <Header /> {children} <Footer /> ); } // Bad: Inheritance leading to complex component hierarchy class BaseComponent extends React.Component { // ... common logic } class ExtendedComponent extends BaseComponent { // ... more logic } """ ### 1.3. Standard: Component Naming Conventions **Do This:** Use descriptive and consistent naming conventions for components and their props. Aim for clarity and readability. Use PascalCase for components. **Don't Do This:** Use vague or ambiguous names. Avoid abbreviations or acronyms unless they are widely understood within the team. **Why:** Clear naming conventions improve the readability and maintainability of the codebase. **Example:** """jsx // Good: function UserProfileCard({ user, onEdit }) { return ( {user.name} <button onClick={onEdit}>Edit</button> ); } // Bad: function UPC({ u, oe }) { return ( {u.name} <button onClick={oe}>Edit</button> ); } """ ### 1.4. Standard: Directory Structure **Do This:** Organize components into a logical directory structure. Consider grouping related components into modules or folders. Utilize an "index.js" file to expose components from a module. **Don't Do This:** Create a flat directory structure with all components in a single folder. **Why:** A well-organized directory structure improves navigation and maintainability, especially in larger projects. **Example:** """ src/ components/ Button/ Button.jsx Button.test.jsx index.js Card/ Card.jsx Card.test.jsx index.js """ "src/components/Button/index.js": """javascript export { default as Button } from './Button'; """ ## 2. Component Props and Data Flow ### 2.1. Standard: Explicit Prop Types **Do This:** Use prop types to define the expected type, shape, and requirements for each prop. Use TypeScript or PropTypes for explicit type checking. **Don't Do This:** Rely on implicit type inference or skip prop type validation altogether. **Why:** Prop types catch errors early, improve code readability, and provide a clear contract for how components should be used. TypeScript is generally preferred for larger projects. **Example (TypeScript):** """tsx interface Props { name: string; age?: number; // Optional onClick: () => void; } function UserCard({ name, age, onClick }: Props) { return ( {name} {age && <p>Age: {age}</p>} <button onClick={onClick}>Click Me</button> ); } """ **Example (PropTypes - Less preferred):** """javascript import PropTypes from 'prop-types'; function UserCard({ name, age, onClick }) { return ( {name} {age && <p>Age: {age}</p>} <button onClick={onClick}>Click Me</button> ); } UserCard.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number, onClick: PropTypes.func.isRequired, }; """ ### 2.2. Standard: Immutable Data **Do This:** Treat component props as immutable data. Avoid modifying props directly within the component. If data needs to be modified, create a local copy or use state. **Don't Do This:** Mutate props directly, as this can lead to unpredictable behavior and rendering issues. **Why:** Immutable data improves performance, simplifies debugging, and prevents unintended side effects. **Example:** """jsx // Good: Creating a local copy of the prop function NameDisplay({ name }) { const [localName, setLocalName] = React.useState(name); const handleChange = (e) => { setLocalName(e.target.value); }; return ( <input type="text" value={localName} onChange={handleChange} /> <p>Original Name: {name}</p> ); } // Bad: Mutating the prop directly function NameDisplay({ name }) { const handleChange = (e) => { name = e.target.value; // Avoid this! }; return <input type="text" onChange={handleChange} />; } """ ### 2.3. Standard: Controlled vs. Uncontrolled Components **Do This:** Decide whether a component should be controlled or uncontrolled based on its use case. Use controlled components when you need tight control over user input and data flow. Use Uncontrolled components for simpler form elements where you don't need to manage every change. **Don't Do This:** Mix and match controlled and uncontrolled component patterns within the same form or UI element without a clear understanding of the consequences. **Why:** Understanding the controlled vs uncontrolled pattern is critical for managing data flow in React. **Example (Controlled):** """jsx function ControlledInput() { const [value, setValue] = React.useState(''); const handleChange = (e) => { setValue(e.target.value); }; return <input type="text" value={value} onChange={handleChange} />; } """ **Example (Uncontrolled):** """jsx function UncontrolledInput() { const inputRef = React.useRef(null); const handleSubmit = () => { alert("Value: ${inputRef.current.value}"); }; return ( <input type="text" ref={inputRef} /> <button onClick={handleSubmit}>Submit</button> ); } """ ### 2.4. Standard: Avoiding Prop Drilling **Do This:** Avoid passing props through multiple layers of components that don't need them ("prop drilling"). Use context, state management libraries (Redux, Zustand, Jotai), or component composition to provide data where it's needed. **Don't Do This:** Create deeply nested prop chains that make it difficult to track the flow of data. **Why:** Prop drilling increases complexity and reduces the maintainability of the codebase. **Example:** """jsx // Good: Using Context import React, { createContext, useContext, useState } from 'react'; const ThemeContext = createContext(); function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(theme === 'light' ? 'dark' : 'light'); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } function ThemedComponent() { const { theme, toggleTheme } = useContext(ThemeContext); return ( Current theme: {theme} <button onClick={toggleTheme}>Toggle Theme</button> ); } function App() { return ( <ThemeProvider> <ThemedComponent /> </ThemeProvider> ); } // Bad: Prop Drilling function App() { const [theme, setTheme] = useState('light'); return <Layout theme={theme} setTheme={setTheme} />; } function Layout({ theme, setTheme }) { return <Content theme={theme} setTheme={setTheme} />; } function Content({ theme, setTheme }) { return <ThemedComponent theme={theme} setTheme={setTheme} />; } function ThemedComponent({ theme, setTheme }) { return ( Current theme: {theme} <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> ); } """ ## 3. Component State Management ### 3.1. Standard: Local State vs. Global State **Do This:** Use local state for component-specific data that doesn't need to be shared across the application. Use global state management (Context, Redux, etc.) for data that needs to be accessed and modified by multiple components. **Don't Do This:** Overuse global state for data that could be managed locally. Avoid managing local state with global tools. **Why:** Proper state management improves performance and reduces complexity. Understanding when to use each approach is essential. **Example (Local State):** """jsx function Counter() { const [count, setCount] = React.useState(0); return ( Count: {count} <button onClick={() => setCount(count + 1)}>Increment</button> ); } """ **Example (Global State with Context):** """jsx import React, { createContext, useContext, useState } from 'react'; const CountContext = createContext(); function CountProvider({ children }) { const [count, setCount] = useState(0); return ( <CountContext.Provider value={{ count, setCount }}> {children} </CountContext.Provider> ); } function Counter() { const { count, setCount } = useContext(CountContext); return ( Count: {count} <button onClick={() => setCount(count + 1)}>Increment</button> ); } function DisplayCount() { const { count } = useContext(CountContext); return <p>Current Count: {count}</p> } function App() { return ( <CountProvider> <Counter /> <DisplayCount /> </CountProvider> ); } """ ### 3.2. Standard: State Updates **Do This:** When updating state based on the previous state, use the functional form of "setState" to avoid race conditions and ensure that you're working with the correct previous state. Prefer "useState" hook whenever possible. **Don't Do This:** Directly modify the state object. Rely on the implicit behavior of "setState". **Why:** Using the functional form of "useState" or older "setState" guarantees that you are always working with the most up-to-date state. **Example:** """jsx // Good: Using the functional form of useState function Counter() { const [count, setCount] = React.useState(0); const increment = () => { setCount((prevCount) => prevCount + 1); }; return ( Count: {count} <button onClick={increment}>Increment</button> ); } """ ### 3.3 Standard: Minimizing State Variables **Do This:** Aim to derive state where possible instead of storing redundant information in multiple state variables. Calculate values using functions or memoization rather than duplicating data within the component’s state. **Don't Do This:** Create multiple state variables if a single variable and computed properties can achieve the same result. Avoid redundancy by keeping the component state lean and focused on the essential data. **Why:** Reducing the number of state variables simplifies the component logic and makes it easier to manage updates. **Example:** """jsx // Good: Deriving full name from first and last names function UserProfile({ firstName, lastName }) { const fullName = "${firstName} ${lastName}"; return ( Full Name: {fullName} ); } // Bad: Storing full name as a separate state function UserProfile({ firstName, lastName }) { const [fullName, setFullName] = React.useState("${firstName} ${lastName}"); React.useEffect(() => { setFullName("${firstName} ${lastName}"); }, [firstName, lastName]); return ( Full Name: {fullName} ); } """ ## 4. Component Rendering and Performance ### 4.1. Standard: Memoization **Do This:** Use "React.memo" to prevent unnecessary re-renders of pure functional components. Use "useMemo" and "useCallback" hooks to memoize expensive calculations and function references. **Don't Do This:** Overuse memoization, as it adds overhead. Only memoize components or values that are likely to cause performance bottlenecks. **Why:** Memoization can significantly improve performance by reducing the number of re-renders. **Example:** """jsx // Memoizing a pure functional component const MyComponent = React.memo(function MyComponent({ data }) { console.log('Rendering MyComponent'); return <div>{data.value}</div>; }); // Memoizing a value function MyComponentContainer({ data }) { const expensiveValue = React.useMemo(() => { // Perform an expensive calculation return data.value * 2; }, [data.value]); return <div>{expensiveValue}</div>; } //Memoizing a callback function MyComponentContainer({ onClick }) { const memoizedCallback = React.useCallback(() => { onClick(); }, [onClick]); return <button onClick={memoizedCallback}>Click me</button> } """ ### 4.2. Standard: Virtualization **Do This:** Use virtualization techniques (e.g., "react-window", "react-virtualized") to efficiently render large lists or tables. **Don't Do This:** Render all items in a large list at once, as this can lead to poor performance. **Why:** Virtualization only renders the visible items, improving performance for large datasets. **Example (react-window):** """jsx import { FixedSizeList } from 'react-window'; function Row({ index, style }) { return ( Row {index + 1} ); } function MyList() { return ( <FixedSizeList height={500} width={300} itemSize={50} itemCount={1000} > {Row} </FixedSizeList> ); } """ ### 4.3. Standard: Code Splitting **Do This:** Use code splitting to break down large bundles into smaller chunks that can be loaded on demand. Employ "React.lazy" and "Suspense" for component-level code splitting. **Don't Do This:** Load all code upfront, as this can increase initial load time. **Why:** Code splitting improves initial load time by only loading the code that is needed for the current page or component. **Example:** """jsx import React, { Suspense } from 'react'; const MyComponent = React.lazy(() => import('./MyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense> ); } """ ### 4.4. Standard: Avoiding Inline Styles and Functions **Do This:** Define styles in CSS files or use CSS-in-JS libraries. Define event handlers outside of the render method or use "useCallback" to memoize them. **Don't Do This:** Use inline styles and functions, as they can cause unnecessary re-renders and make it difficult to maintain styles. **Why:** Using external styles and memoizing event handlers improves performance and makes it easier to maintain styles. Defining event handlers within the render method creates a new function on every render, preventing "React.memo" from working correctly. **Example:** """jsx // Good: External styles and memoized event handler import styles from './MyComponent.module.css'; function MyComponent({ onClick }) { const handleClick = React.useCallback(() => { onClick(); }, [onClick]); return <button className={styles.button} onClick={handleClick}>Click Me</button>; } // Bad: Inline style and function function MyComponent({ onClick }) { return ( <button style={{ backgroundColor: 'blue', color: 'white' }} onClick={() => onClick()} > Click Me </button> ); } """ ## 5. Testing and Maintainability ### 5.1. Standard: Unit Testing **Do This:** Write unit tests for all components, focusing on testing the behavior of individual components in isolation. Use Jest and React Testing Library for testing. **Don't Do This:** Skip unit tests, as this can lead to regressions and make it difficult to maintain the codebase. **Why:** Unit tests verify that components are working correctly and provide a safety net for future changes. **Example:** """jsx // MyComponent.jsx function MyComponent({ message }) { return <div>{message}</div>; } export default MyComponent; // MyComponent.test.jsx import React from 'react'; import { render, screen } from '@testing-library/react'; import MyComponent from './MyComponent'; test('renders message', () => { render(<MyComponent message="Hello, world!" />); const messageElement = screen.getByText(/Hello, world!/i); expect(messageElement).toBeInTheDocument(); }); """ ### 5.2. Standard: Component Documentation **Do This:** Add detailed documentation comments (JSDoc style, or similar) to your React components, ensuring that each prop is thoroughly described. Additionally, keep the README.md file in the component's directory updated with usage examples and any specific considerations. **Don't Do This:** Neglect documenting component props and usage, assuming that other developers will understand the component's purpose and functionality without guidance. **Why:** Clear documentation is helpful internally with team members and when opensourcing. **Example:** """jsx /** * A simple button component. * * @param {Object} props - The component props. * @param {string} props.text - The text to display on the button. * @param {Function} props.onClick - The function to call when the button is clicked. * @param {boolean} [props.disabled=false] - Whether the button is disabled. * @returns {JSX.Element} A button element. */ function MyButton({ text, onClick, disabled = false }) { return ( <button onClick={onClick} disabled={disabled}> {text} </button> ); } export default MyButton; """ ### 5.3. Standard: Accessibility **Do This:** Make sure components are accessible to users with disabilities. Use semantic HTML, ARIA attributes, and keyboard navigation. Test components with accessibility testing tools. **Don't Do This:** Ignore accessibility considerations, as this can exclude users with disabilities. **Why:** Accessibility ensures that everyone can use the application, regardless of their abilities. **Example:** """jsx // Good: Using semantic HTML and ALT text function ImageWithAlt({ src, alt }) { return <img src={src} alt={alt} />; } // Bad: Missing ALT text function ImageWithoutAlt({ src }) { return <img src={src} alt="" />; // Avoid this } """ ### 5.4. Standard: Error Handling **Do This:** Implement robust error handling in components. Use try-catch blocks to catch errors, display error messages to the user, and log errors for debugging. **Don't Do This:** Ignore errors or allow them to crash the application. **Why:** Error handling prevents crashes and provides a better user experience. **Example:** """jsx function ApiComponent({ url }) { const [data, setData] = React.useState(null); const [error, setError] = React.useState(null); React.useEffect(() => { async function fetchData() { try { const response = await fetch(url); const json = await response.json(); setData(json); } catch (e) { setError(e); } } fetchData(); }, [url]); if (error) { return <div>Error: {error.message}</div>; } if (!data) { return <div>Loading...</div>; } return <div>Data: {JSON.stringify(data)}</div>; } """ These standards provide a solid foundation for designing components in Jest. By following these guidelines, developers can create reusable, maintainable, and testable components that contribute to the overall quality and success of the project.
# Security Best Practices Standards for Jest This document outlines security best practices for writing Jest tests. Following these guidelines reduces the risk of introducing vulnerabilities in your codebase, especially within test environments that might be overlooked in traditional security audits. It outlines common pitfalls and secure testing patterns, explaining WHY certain practices matter for overall security and how to implement them effectively within Jest. ## 1. General Security Principles for Jest Tests Security in testing isn't just about finding bugs; it's about preventing them by writing tests that don't inadvertently introduce vulnerabilities. These are the foundational principles: * **Principle of Least Privilege:** Tests should only have the permissions they absolutely need. Avoid running tests with elevated privileges unless necessary. * **Input Validation:** Test inputs, _especially_ those coming from external sources (mocks, fixtures), should be rigorously validated to prevent test poisoning or unexpected behavior. * **Secure Secrets Management:** Never hardcode sensitive information (API keys, passwords) in tests. Use environment variables or secure configuration management techniques. * **Dependency Hygiene:** Keep Jest and its dependencies up to date to patch known vulnerabilities. Regularly scan dependencies for security issues. * **Test Isolation:** Ensure tests are isolated from each other. A compromised test should not affect other tests or the system under test. * **Defense in Depth:** Security should be layered. Don't rely on a single security measure; implement multiple safeguards. ## 2. Avoiding Common Vulnerabilities in Jest Jest tests, like any other code, can be susceptible to vulnerabilities. Here's how to address some common ones: ### 2.1. Test Poisoning Test poisoning occurs when malicious or unexpected input to tests causes them to fail or behave erratically, potentially masking real bugs or even destabilizing the test environment. **Do This:** * **Explicitly Define Valid Input Ranges:** Always define the expected range and format of test inputs. * **Use Mocking Frameworks Safely:** When mocking external dependencies, define the exact expected input and output to prevent unexpected behavior with malformed data. Validate the incoming parameters in the mock implementation where possible. * **Sanitize and Validate Data:** Before using data in assertions, sanitize it as you would in production code. **Don't Do This:** * **Use Unvalidated Input:** Don't use external or generated data directly in tests without validation. * **Assume Input Type:** Don't assume the type or format of input data without explicit checks. * **Allow Side Effects in Mocking:** Avoid mocks with side effects that could destabilize the test suite. A test should only impact the system under test, nothing else. **Example:** """javascript // Vulnerable code: Assuming 'id' is always a number test('Deletes a user with the given id', async () => { const id = request.params.id; // Assuming request object is available in the test, possibly via mocking. await deleteUser(id); expect(deleteUser).toHaveBeenCalledWith(id); }); // Secure code: Validating the 'id' test('Deletes a user with the given id (validated)', async () => { const id = request.params.id; if (!/^\d+$/.test(id)) { throw new Error('Invalid ID format'); // Or fail the test gracefully. This prevents attempting to delete a user with an invalid id. } await deleteUser(id); expect(deleteUser).toHaveBeenCalledWith(id); }); // Secure code with mock validation: Validating parameter sent to a mocked function test('Creates a new user with valid input', async () => { const mockCreateUser = jest.fn(); const userData = { name: 'John Doe', email: 'john.doe@example.com' }; // Validate data before sending it to the function if (typeof userData.name !== 'string' || userData.name.length === 0) { throw new Error('Name must be a non-empty string') } if (typeof userData.email !== 'string' || !userData.email.includes('@')) { throw new Error('Invalid email address') } mockCreateUser.mockImplementation((user) => { // Parameter to the mock is also validated to prevent vulnerabilities. if (typeof user.name !== 'string' || user.name.length === 0) { throw new Error('Invalid user name passed to mock'); } return Promise.resolve({ id: 123, ...user }); }); const newUser = await mockCreateUser(userData); expect(mockCreateUser).toHaveBeenCalledWith(userData); // Validating that the mocked function was called. expect(newUser.name).toBe(userData.name); }); """ **Why:** Validating inputs prevents unexpected data from compromising the test environment or leading to false positives/negatives. Mock validation ensures that your mock behaves as expected when fed invalid data. ### 2.2. Information Disclosure Tests can inadvertently leak sensitive information, such as API keys, database passwords, or internal system details. **Do This:** * **Use Environment Variables:** Store sensitive information in environment variables and access them in tests using "process.env". * **Avoid Logging Secrets:** Do not log sensitive data to the console or store it in test reports. * **Use Secure Fixtures:** If using fixtures, ensure they don't contain real sensitive data. Use synthetic data instead. * **Redact Sensitive Data in Snapshots:** When using snapshot testing, redact sensitive data from snapshots using custom serializers. **Don't Do This:** * **Hardcode Secrets:** Never hardcode API keys, passwords, or other sensitive information directly in test files. * **Commit Secrets to Repositories:** Make sure environment variable files and any files containing secrets are added to ".gitignore". * **Expose Sensitive Data in Logs:** Avoid logging responses or requests containing sensitive information. **Example:** """javascript // Vulnerable code: Hardcoding API key test('Fetches data from API', async () => { const apiKey = 'YOUR_API_KEY'; // DON'T DO THIS! const data = await fetchData(apiKey); expect(data).toBeDefined(); }); // Secure code: Using environment variables test('Fetches data from API using environment variables', async () => { const apiKey = process.env.API_KEY; if (!apiKey) { throw new Error('API_KEY environment variable not set'); } const data = await fetchData(apiKey); expect(data).toBeDefined(); }); // jest.config.js (or package.json jest configuration) // setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'], // Optional – for global environment setup // Secure secrets management using "dotenv": // setupTests.js -- this file must be configured in the jest.config.js require('dotenv').config({ path: '.env.test' }); // Load env variables in a test environment //.env.test (Example) //API_KEY=secure_test_api_key //Redacting sensitive data in Jest Snapshot Serializers //In jest.config.js (or package.json jest configuration), add (or update) the snapshotSerializers array: // snapshotSerializers: ["<rootDir>/src/redactSecrets.js"], //redactSecrets.js exports.test = (val) => val && val.hasOwnProperty('authorization'); // Check if the property to redact exists exports.print = (val, serialize) => { const redactedValue = '[REDACTED]'; const newObj = { ...val, authorization: redactedValue }; return serialize(newObj); }; """ **Why:** Preventing information disclosure protects sensitive data from being exposed and potentially exploited. Using environment variables and secure configuration management ensures that secrets are not stored in code repositories. Snapshot redaction ensures credentials and other secrets aren't persisted in snapshots. ### 2.3. Denial of Service (DoS) Malicious tests could consume excessive resources (CPU, memory, disk space), leading to a denial of service. This is especially important in CI/CD environments. **Do This:** * **Limit Test Data Size:** Avoid using excessively large datasets in tests to prevent memory exhaustion. * **Set Test Timeouts:** Configure Jest's "testTimeout" option to prevent tests from running indefinitely. * **Control Parallelism:** Limit the number of tests running in parallel to avoid overloading the system. Adjust the "maxWorkers" configuration. * **Monitor Resource Usage:** Monitor resource usage during test execution to identify tests that consume excessive resources. **Don't Do This:** * **Unbounded Loops:** Don't write tests with infinite loops or unbounded recursion. * **Excessive Memory Allocation:** Avoid allocating large amounts of memory unnecessarily. * **Uncontrolled External Requests:** Avoid making a large number of unchecked external requests in tests. **Example:** """javascript // Vulnerable code: Unbounded loop (simulated) test('Processes a large dataset', () => { const largeDataset = generateLargeDataset(); // Huge dataset largeDataset.forEach(item => { processItem(item); // Could be slow or inefficient }); }, 60000); // Even with a timeout, this is problematic for large datasets // Secure code: Limiting data size and using timeouts test('Processes a sample dataset', () => { const sampleDataset = generateSampleDataset(100); // Limited dataset sampleDataset.forEach(item => { processItem(item); }); }, 5000); // Timeout for excessive processing time // jest.config.js module.exports = { testTimeout: 5000, // Global timeout for all tests (milliseconds) maxWorkers: 4, // Limit the number of parallel test workers }; """ **Why:** Protecting against DoS attacks ensures that tests don't consume excessive resources or destabilize the test environment. Timeouts, data size limits, and controlled parallelism help mitigate these risks. ### 2.4. Code Injection If a test manipulates strings that are later interpreted as code (e.g., using "eval" or "Function"), it can be vulnerable to code injection attacks. While uncommon in Jest, improper mocking can create this scenario. **Do This:** * **Avoid "eval" and "Function":** Never use "eval" or "Function" to dynamically execute code in tests unless absolutely necessary. * **Sanitize Untrusted Data:** If you must use "eval" or "Function", sanitize any untrusted data before passing it to these functions. * **Careful Mocking:** Ensure that mocked return values do not include any executable code. **Don't Do This:** * **Dynamically Construct Code:** Avoid dynamically constructing code strings from user-controlled input. * **Execute Untrusted Data:** Never execute untrusted data as code. **Example:** """javascript // Vulnerable code: Using eval with potentially malicious input test('Processes data with a dynamic function', () => { const maliciousInput = '}; console.log("Hacked!"); //'; // Example of malicious input const funcString = 'function process(data) { return ' + maliciousInput + '}'; eval(funcString); // POTENTIAL CODE INJECTION const result = process({ value: 10 }); expect(result).toBe(10); }); // Secure code: Avoiding eval and using safe alternatives (if possible) test('Processes data with a safe calculation', () => { const inputData = { value: 10 }; const result = calculateResult(inputData.value); // Assuming calculateResult performs validation and is safe expect(result).toBeGreaterThan(0); }); function calculateResult(value) { if (typeof value !== 'number') { throw new Error('Invalid input: Value must be a number'); } return value * 2; // Simple and safe operation without dynamic code execution } """ **Why:** Preventing code injection attacks ensures that malicious code cannot be executed within the test environment. Avoid using "eval" and always sanitize user input if dynamic code execution is unavoidable. Mocking safety avoids unexpected code changes. ### 2.5. Dependency Vulnerabilities Jest projects often rely on numerous dependencies, including testing libraries, mocking tools, and assertion frameworks. These dependencies may contain security vulnerabilities that can be exploited. One common attack vector is through transitive dependencies - dependencies of your direct dependencies. **Do This:** * **Regularly Audit Dependencies:** Use tools like "npm audit" or "yarn audit" to identify known vulnerabilities in your project's dependencies. Set up automated dependency scanning in your CI/CD pipeline using tools like Snyk or GitHub Dependabot. * **Keep Dependencies Up-to-Date:** Update your project's dependencies regularly to patch known vulnerabilities. Use semantic versioning (semver) to manage dependency updates and minimize the risk of introducing breaking changes. * **Use Specific Dependency Versions:** Avoid using wildcard or range-based version specifications in your "package.json" file. Pin specific dependency versions to ensure that your project uses a consistent and known-vulnerable set of dependencies. * **Review Dependency Licenses:** Ensure that the licenses of your project's dependencies are compatible with your project's licensing terms and policies. Be aware of any restrictions or obligations associated with the use of open-source dependencies. * **Minimize Transitive Dependencies:** Reduce the number of transitive dependencies in your project by using direct dependencies that have fewer dependencies themselves. Consider using lighter-weight alternatives to reduce your project's dependency footprint. * **Use a Software Bill of Materials (SBOM):** Generate and maintain an SBOM for your project to provide a comprehensive inventory of all your dependencies, including direct and transitive dependencies. Use the SBOM to track and manage dependency vulnerabilities and license compliance. **Don't Do This:** * **Ignore Dependency Audits:** Don't ignore warnings or errors reported by dependency auditing tools. Address identified vulnerabilities promptly by updating dependencies or applying patches. * **Use Outdated Dependencies:** Don't continue using outdated versions of dependencies that have known security vulnerabilities. * **Blindly Update Dependencies:** Don't blindly update all dependencies to the latest versions without testing and verifying compatibility. Thoroughly test your project after updating dependencies to ensure that no regressions or breaking changes are introduced. * **Use Unverified Dependencies:** Don't use dependencies from untrusted sources or repositories. Only use dependencies that have been scanned for vulnerabilities and verified to be safe. **Example:** """bash # Run npm audit to identify vulnerabilities in your project's dependencies npm audit # Run yarn audit to identify vulnerabilities in your project's dependencies yarn audit # Update a specific dependency to the latest version npm update <dependency-name> # Install a security patch for a specific dependency npm install <dependency-name>@<patched-version> #Generate an SBOM npm install -g @cyclonedx/bom cyclonedx-bom //Generates a CycloneDX format SBOM. """ **Why:** Regularly auditing, updating, and managing dependencies is crucial for maintaining a secure Jest testing environment. Addressing dependency vulnerabilities proactively prevents potential exploits that could compromise your tests and expose sensitive data. ## 3. Secure Coding Patterns in Jest Using secure coding patterns in Jest tests helps to create more robust and reliable tests, and reduces the risk of introducing vulnerabilities. ### 3.1. Mocking Strategies Mocking external dependencies is a common practice in Jest to isolate the system under test and ensure repeatable test results. However, improper mocking can introduce security vulnerabilities. **Do This:** * **Validate Mocked Methods:** Ensure mocks return expected data types and structures. * **Scope Mocks:** Only mock what's necessary for the current test. Avoid global mocks that could affect other tests unexpectedly. * **Use Mock Implementations:** Use Jest's "mockImplementation" or "mockResolvedValue" to define the exact behavior of mocked functions, including error handling. Consider using "spyOn" to patch a method and assert the calls if you want to run real code. **Don't Do This:** * **Overshadow Modules:** Don't mock entire modules unless necessary, as this can mask underlying issues with the system under test. * **Return Non-Deterministic Output:** Avoid mocks that return random or non-deterministic data, as this can lead to flaky tests and hide vulnerabilities. * **Ignore Error Conditions:** Don't create mocks that always succeed, ignoring potential error conditions in the real system. This can lead to false positives if the code isn't correctly handling errors. **Example:** """javascript // Vulnerable code: Mocking without validation jest.mock('./externalService', () => ({ fetchData: jest.fn(() => Promise.resolve({ data: 'Some data' })), // No validation of return type. })); // Secure code: Mocking with validation jest.mock('./externalService', () => ({ fetchData: jest.fn(() => { const data = { data: 'Some data' }; if (typeof data.data !== 'string') { throw new Error('Invalid data type'); } return Promise.resolve(data); }), })); //Scoping mocks: describe('User creation', () => { it('Creates a user successfully', async () => { const createUser = jest.fn(() => Promise.resolve({ id: 1, name: 'Test User' })); //Here we call "createUser" that is scoped only in this test case, and does not affect the other test cases const user = await createUser({ name: 'Test User' }); expect(user).toEqual({ id: 1, name: 'Test User' }); }); it('Handles errors when user creation fails', async () => { const createUser = jest.fn(() => Promise.reject(new Error('User creation failed'))); //Here we call "createUser" that is scoped only in this test case, and does not affect the other test cases await expect(createUser({ name: 'Test User' })).rejects.toThrow('User creation failed'); }); }); """ **Why:** Secure mocking prevents tests from being misled by unexpected behavior from mocked dependencies. Validating mock outputs and scoping mocks ensure test reliability and reduce the risk of masking vulnerabilities. ### 3.2. Assertion Strategies The way assertions are written can impact test effectiveness and security. Overly permissive or poorly written assertions can mask errors. **Do This:** * **Precise Assertions:** Use precise assertions that check for specific values or states, rather than generic assertions that could pass even with errors. * **Error Handling Assertions:** Test both success and failure scenarios and assert that errors are handled correctly. * **Boundary Condition Assertions:** Test boundary conditions and edge cases to ensure that the system handles unusual inputs correctly. **Don't Do This:** * **Tautological Assertions:** Avoid assertions that always pass regardless of the system's behavior (e.g., "expect(true).toBe(true)"). * **Ignoring Errors:** Don't ignore errors or exceptions during test execution. Always assert that expected errors are thrown. * **Asserting Unimportant Details:** Don't focus assertions on trivial implementation details. Check functionality rather than exact data format. **Example:** """javascript // Vulnerable code: Generic assertion test('Processes data', async () => { const result = await processData(); expect(result).toBeDefined(); // Too generic; doesn't check the actual data }); // Secure code: Precise assertion test('Processes data and returns the correct value', async () => { const result = await processData(); expect(result).toBe(123); // Checks for a specific, expected value }); test('Handles invalid input gracefully', async () => { await expect(processData(null)).rejects.toThrow('Invalid input'); // Assertion of a thrown error in a promise rejection }); test('Handles invalid input gracefully (sync)', () => { expect(() => processDataSync(null)).toThrow('Invalid input'); // Assertion of a thrown error in synchronous code }); """ **Why:** Precise assertions ensure that tests accurately verify the system's behavior. Error handling assertions help identify cases where errors are not handled correctly, potentially leading to vulnerabilities. ### 3.3. Test Data Management How test data is created and managed is critical to the reliability and security of tests. **Do This:** * **Data Generation:** If you need sample user data, use a library like "faker.js" to generate this. * **Test-Specific Data:** Create data that is specific to the test being run. * **Sanitize Data:** Make sure the dataset is sanitized to prevent introducing vulnerabilities to the data. **Don't Do This:** * **Using Real User PII:** Don't use PII. * **Leaving behind any "backdoors."** Avoid creating test-specific endpoints - these can potentially be left in your production code. **Example:** """javascript const { faker } = require('@faker-js/faker'); test('can POST a valid user', async () => { const newUser = { firstName: faker.person.firstName(), lastName: faker.person.lastName(), email: faker.internet.email(), } const result = await postNewUser(newUser); expect(newUSer.email).toContain('@'); }); """ **Why:** When a test doesn't depend on external data sources, it is less likely that it will break. When you're not using PII (personally identifiable information), you are less likely to have a security breach via the testing suite. ## 4. Test Environment Security Securing the test environment is as crucial as securing the production environment. A compromised test environment can lead to vulnerabilities being introduced into the codebase. ### 4.1. Isolation and Sandboxing Ensure that tests are isolated from each other and the external environment to prevent interference and security breaches. **Do This:** * **Use Containerization:** Run tests in isolated containers (e.g., Docker) to prevent them from affecting the host system. * **Virtualization:** Use virtualization technologies (e.g., VMs) to create separate test environments for different projects or test suites. * **Mock External Dependencies:** Mock external dependencies (e.g., databases, APIs) to prevent tests from interacting with real systems. **Don't Do This:** * **Run Tests Directly on Production Systems:** Don't run tests directly on production systems, as this can lead to data corruption or service interruptions. * **Share Test Environments:** Don't share test environments between different projects or teams, as this can lead to conflicts and security vulnerabilities. ### 4.2. Access Control Restrict access to the test environment and test data to authorized personnel only. **Do This:** * **Use Role-Based Access Control (RBAC):** Implement RBAC to control access to test resources based on user roles and responsibilities. * **Multi-Factor Authentication (MFA):** Enforce MFA for access to the test environment to prevent unauthorized access. * **Regularly Review Access Permissions:** Review access permissions regularly to ensure that they are still appropriate and that no unauthorized users have access to the test environment. **Don't Do This:** * **Use Default Credentials:** Don't use default credentials for the test environment, as these are easily compromised. * **Grant Excessive Permissions:** Don't grant excessive permissions to users, as this can increase the risk of unauthorized access and data breaches. ### 4.3. Monitoring and Logging Implement monitoring and logging to detect and respond to security incidents in the test environment. **Do This:** * **Implement Security Monitoring:** Implement security monitoring to detect suspicious activity in the test environment, such as unauthorized access attempts or data exfiltration. * **Centralized Logging:** Configure centralized logging to collect and analyze logs from all components of the test environment. * **Security Incident Response Plan:** Develop a security incident response plan to guide the response to security incidents in the test environment. **Don't Do This:** * **Disable Logging:** Don't disable logging in the test environment, as this can make it difficult to detect and respond to security incidents. * **Ignore Security Alerts:** Don't ignore security alerts from monitoring systems, as these may indicate a security incident. **Example:** """bash #Example of restricting access to the test databases: # Create a dedicated user for tests with limited privileges CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'test_password'; # Grant only the necessary permissions GRANT SELECT, INSERT, UPDATE, DELETE ON test_database.* TO 'test_user'@'localhost'; # Revoke unnecessary privileges to further limit access REVOKE ALL PRIVILEGES ON test_database.* FROM 'test_user'@'localhost'; FLUSH PRIVILEGES; """ **Why:** Test Environment Security ensures that those with ill-intent can't penetrate the codebase. When you use access restriction, monitor environments, and isolate them, it provides a safe system. By following these guidelines, you can create more secure and reliable Jest tests that help to protect your codebase from vulnerabilities. Remember that security is an ongoing process, and it requires continuous vigilance and improvement. These standards should be regularly reviewed and updated to reflect the latest security threats and best practices.
# Core Architecture Standards for Jest This document outlines the core architectural standards for writing maintainable, performant, and reliable tests using Jest. It focuses on the fundamental architectural patterns, project structure, and organizational principles specific to Jest projects. These standards aim to guide developers in building robust testing suites that effectively validate their code. ## 1. Project Structure and Organization A well-organized test suite is crucial for maintainability and scalability. The following standards guide project structure within your Jest testing environment. ### 1.1 Test File Location **Standard:** Locate test files close to the source code they test. Use a consistent naming convention. **Do This:** * Place test files in a "__tests__" directory within the same directory as the source code. * Name test files with a ".test.js" or ".spec.js" extension (depending on project preference, but remain consistent). **Don't Do This:** * Create a single, monolithic "test" directory at the root of the project. * Mix test files with source code. * Use inconsistent naming conventions for test files. **Why:** * **Proximity:** Makes it easy to find and update tests when modifying source code. * **Organization:** Clearly separates tests from source code, improving project structure. * **Discoverability:** Jest automatically discovers test files following these conventions. **Example:** """ my-project/ ├── src/ │ ├── utils/ │ │ ├── calculator.js │ │ ├── __tests__/ │ │ │ └── calculator.test.js <-- Test located here │ │ └── string-formatter.js │ ├── components/ │ │ ├── button.js │ │ └── __tests__/ │ │ └── button.test.js <-- Test located here │ └── index.js ├── package.json └── jest.config.js """ ### 1.2 Test File Organization within "__tests__" **Standard:** Organize tests within the "__tests__" directory based on the structure of the source code being tested. **Do This:** * Mirror the source code directory structure within the "__tests__" directory. * Create separate test files for each module or component. * Group related tests within a "describe" block. **Don't Do This:** * Place all tests in a single test file. * Mix tests for different modules or components in the same test file. * Avoid using "describe" blocks to group related tests. **Why:** * **Clarity:** Makes it easy to understand which tests belong to which modules. * **Maintainability:** Reduces the impact of changes on the test suite. * **Readability:** Improves the overall readability and organization of the test suite. **Example:** """javascript // src/utils/calculator.js export const add = (a, b) => a + b; export const subtract = (a, b) => a - b; // src/utils/__tests__/calculator.test.js import { add, subtract } from '../calculator'; describe('calculator', () => { describe('add', () => { it('should add two numbers correctly', () => { expect(add(2, 3)).toBe(5); }); }); describe('subtract', () => { it('should subtract two numbers correctly', () => { expect(subtract(5, 2)).toBe(3); }); }); }); """ ### 1.3 Mocking Strategies and Location **Standard:** Mock dependencies effectively and locate mocks appropriately. **Do This:** * Use Jest's "jest.mock()" to mock modules. * Create a "__mocks__" directory next to the module being mocked for manual mocks. * Use inline mocks for simple cases (e.g., "jest.fn()"). * Centralize reusable mocks into a separate "mocks" directory at the project root. **Don't Do This:** * Over-mock (mocking everything leads to brittle tests). * Leave mocks scattered throughout the test files. * Hardcode mock implementation directly within tests if the mock is used elsewhere. **Why:** * **Isolation:** Effective mocking isolates the unit under test, preventing external dependencies from affecting test results. * **Reusability:** Centralizing mocks promotes code reuse and reduces duplication. * **Maintainability:** Well-organized mocks are easier to update and maintain. **Example:** """javascript // src/api/user-service.js export const fetchUser = async (id) => { const response = await fetch("/users/${id}"); return response.json(); }; // src/api/__mocks__/user-service.js (Manual Mock) export const fetchUser = jest.fn(async (id) => ({ id, name: 'Mock User' })); // src/components/user/__tests__/user.test.js import { fetchUser } from '../../api/user-service'; import UserComponent from '../user'; jest.mock('../../api/user-service'); // Apply the mock it('fetches and displays user data', async () => { fetchUser.mockResolvedValue({ id: 1, name: 'Test User' }); // Set the mock return value // ... render the component and assert the output ... }); """ ### 1.4 "jest.config.js" Configuration **Standard:** Maintain a clear and well-documented "jest.config.js" file. **Do This:** * Use comments to explain the purpose of each configuration option. * Organize configuration options logically (e.g., module name mappers, coverage thresholds). * Externalize complex configuration logic into separate files if necessary. * Use environment variables for sensitive configuration values (e.g., API keys for integration tests). * Define a clear "testEnvironment" (e.g., 'node', 'jsdom'). **Don't Do This:** * Leave the "jest.config.js" file undocumented and cluttered. * Commit sensitive configuration values directly into the file. * Use deprecated configuration options. * Neglect to specify a "testEnvironment". **Why:** * **Clarity:** Makes it easy to understand and modify the Jest configuration. * **Maintainability:** Reduces the complexity of the configuration file. * **Security:** Protects sensitive configuration values. * **Consistency:** Ensures consistent test execution across different environments. **Example:** """javascript // jest.config.js module.exports = { testEnvironment: 'jsdom', // Specifies the testing environment moduleNameMapper: { '^@components(.*)$': '<rootDir>/src/components$1', // Maps module names }, // Add more configurations }; """ ## 2. Test Suite Architecture and Patterns This section focuses on architectural patterns within the Jest test suite itself. ### 2.1 AAA (Arrange-Act-Assert) Pattern **Standard:** Structure each test case using the Arrange-Act-Assert (AAA) pattern for clarity and maintainability. **Do This:** * **Arrange:** Set up the necessary preconditions and inputs for the test. * **Act:** Execute the function or component under test. * **Assert:** Verify that the output or side effects are as expected. **Don't Do This:** * Mix the Arrange, Act, and Assert steps together. * Omit any of the three steps. **Why:** * **Readability:** Makes it easy to understand what the test is trying to achieve. * **Maintainability:** Simplifies the process of modifying and debugging tests. * **Clarity:** Reduces the complexity of test cases. **Example:** """javascript it('should add two numbers correctly', () => { // Arrange const a = 2; const b = 3; // Act const result = add(a, b); // Assert expect(result).toBe(5); }); """ ### 2.2 Component vs. Integration Tests **Standard:** Distinguish between component (unit) and integration tests and structure accordingly. **Do This:** * **Component Tests:** Focus on testing individual components or modules in isolation by mocking dependencies. * **Integration Tests:** Test the interaction between multiple components or modules. * Organize component and integration tests into separate directories or files. * Use appropriate mocking strategies for each type of test. **Don't Do This:** * Confuse component and integration tests. * Over-mock in integration tests. * Under-mock in component tests. **Why:** * **Specificity:** Component tests provide fast feedback on individual component behavior. Integration tests verify that components work together correctly. * **Efficiency:** Component tests are typically faster and easier to write than integration tests. * **Diagnosability:** Easier to isolate the source of a bug when tests are appropriately categorized. **Example:** """javascript // src/components/button/__tests__/button.test.js (Component Test) import Button from '../button'; import { render, fireEvent } from '@testing-library/react'; it('calls onClick handler when clicked', () => { const handleClick = jest.fn(); const { getByText } = render(<Button onClick={handleClick}>Click Me</Button>); fireEvent.click(getByText('Click Me')); expect(handleClick).toHaveBeenCalledTimes(1); }); // src/integration/__tests__/user-flow.test.js (Integration Test) import App from '../../App'; // Assuming App integrates multiple components import { render, screen, fireEvent } from '@testing-library/react'; it('allows a user to login and see their profile', async () => { render(<App />); // ... simulate user login flow using fireEvent and screen methods ... expect(screen.getByText('Welcome, Test User')).toBeInTheDocument(); }); """ ### 2.3 Test Data Management **Standard:** Implement a strategy for managing test data to ensure consistency and avoid duplication. **Do This:** * Use factory functions to create test data. * Store test data in a separate file or module. * Use consistent naming conventions for test data variables. * Use snapshot testing for complex data structures. **Don't Do This:** * Hardcode test data directly in test cases. * Duplicate test data across multiple test files. * Use inconsistent naming conventions for test data variables. **Why:** * **Consistency:** Ensures that test data is consistent across all test cases. * **Reusability:** Allows test data to be reused across multiple test files. * **Maintainability:** Simplifies the process of updating and maintaining test data. **Example:** """javascript // test-data/user-factory.js export const createUser = (overrides = {}) => ({ id: 1, name: 'John Doe', email: 'john.doe@example.com', ...overrides, }); // src/components/user/__tests__/user.test.js import { createUser } from '../../../test-data/user-factory'; it('displays user name correctly', () => { const user = createUser({ name: 'Test User' }); // ... render the component and assert that the user name is displayed correctly ... }); """ ### 2.4 Asynchronous Testing **Standard:** Use appropriate techniques for testing asynchronous code. **Do This:** * Use "async/await" for cleaner and more readable asynchronous tests. * Use "jest.mock" to mock asynchronous dependencies. * Use "Promise.resolve" and "Promise.reject" to simulate successful and failed asynchronous operations. * Use "waitFor" (from "@testing-library/react") for waiting on state updates from async operations in React components. **Don't Do This:** * Rely on implicit timeouts in asynchronous tests. * Ignore unhandled promise rejections. * Overcomplicate asynchronous tests. **Why:** * **Reliability:** Ensures that asynchronous tests are reliable and accurate. * **Readability:** Makes asynchronous tests easier to understand and maintain. * **Efficiency:** Allows asynchronous tests to be executed efficiently. **Example:** """javascript // Using async/await it('fetches data asynchronously', async () => { const result = await fetchData(); expect(result).toEqual({ data: 'Test Data' }); }); // Mocking asynchronous dependencies jest.mock('../data-service', () => ({ fetchData: jest.fn(async () => ({ data: 'Mock Data' })), })); """ ### 2.5 Snapshot Testing **Standard:** Utilize snapshot testing judiciously for UI components and complex data structures. **Do This:** * Use snapshot testing to verify the structure and content of UI components. * Use snapshot testing to verify the output of complex data transformations. * Review snapshot changes carefully to ensure that they are intentional. * Update snapshots when necessary using "jest -u". * Keep snapshots up-to-date with code changes. **Don't Do This:** * Rely solely on snapshot testing for all UI testing. * Commit outdated or incorrect snapshots. * Ignore snapshot differences. **Why:** * **Efficiency:** Simplifies the process of verifying UI components and complex data structures. * **Accuracy:** Ensures that UI components and data structures are rendered correctly. * **Maintainability:** Reduces the effort required to update and maintain UI tests. **Example:** """javascript // src/components/profile/__tests__/profile.test.js import Profile from '../profile'; import { render } from '@testing-library/react'; it('renders the profile component correctly', () => { const { asFragment } = render(<Profile name="Test User" />); expect(asFragment()).toMatchSnapshot(); }); """ ## 3. Advanced Architectural Considerations ### 3.1 Code Coverage Thresholds **Standard:** Define and enforce code coverage thresholds to ensure adequate test coverage. **Do This:** * Set code coverage thresholds in "jest.config.js". * Use meaningful thresholds aligned with project requirements. * Track code coverage trends over time. * Investigate code coverage gaps and address them as needed. **Don't Do This:** * Ignore code coverage reports. * Set unrealistically high or low code coverage thresholds. * Treat code coverage as the sole measure of test quality. **Why:** * **Quality:** Helps to ensure that code is adequately tested. * **Risk Reduction:** Reduces the risk of introducing bugs and regressions. * **Maintainability:** Improves the maintainability of the codebase. **Example:** """javascript // jest.config.js module.exports = { coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, }, }; """ ### 3.2 Parallelization and Performance **Standard:** Optimize test execution for performance. **Do This:** * Leverage Jest's parallelization capabilities. * Avoid unnecessary mocking. * Use caching to reduce the cost of expensive operations. * Profile test execution to identify performance bottlenecks. * Specify "testMatch" and "testPathIgnorePatterns" to reduce the number of tests. **Don't Do This:** * Run tests serially when parallelization is possible. * Create unnecessary mocks. * Ignore performance bottlenecks. **Why:** * **Speed:** Reduces the time required to execute the test suite. * **Efficiency:** Improves the efficiency of the development process. * **Productivity:** Allows developers to iterate more quickly. ### 3.3 CI/CD Integration **Standard:** Integrate Jest tests into the CI/CD pipeline. **Do This:** * Run Jest tests as part of the build process. * Fail the build if any tests fail. * Collect code coverage data and report it to a code coverage tool. * Use a consistent testing environment across all stages of the CI/CD pipeline. **Don't Do This:** * Skip running tests in the CI/CD pipeline. * Ignore test failures in the CI/CD pipeline. * Use inconsistent testing environments across different stages of the CI/CD pipeline. **Why:** * **Reliability:** Ensures that code is thoroughly tested before being deployed to production. * **Early Detection:** Identifies and addresses bugs early in the development process. * **Confidence:** Increases confidence in the quality of the codebase. This document provides a comprehensive guide to establishing core architecture standards for Jest projects. By following these standards, development teams can create robust, maintainable, and performant test suites that contribute to the overall quality of their software.
# State Management Standards for Jest This document provides coding standards for managing state within Jest tests. These standards ensure tests are predictable, maintainable, and avoid common pitfalls associated with stateful testing. We'll cover approaches suitable for various state management patterns, including those used in React (Redux, Zustand, Context), Vue (Vuex, Pinia), and other JavaScript frameworks, focusing on the latest Jest features and best practices. ## 1. General Principles ### 1.1. Stateless Tests **Rule:** Tests should ideally be stateless to prevent interference and ensure consistent results. Each test should be independent and not rely on the state left by previous tests. **Do This:** * Reset the state before each test or suite. * Avoid modifying global variables directly within tests. * Use mock implementations or spies to control the behavior of dependencies. **Don't Do This:** * Rely on the order of test execution. * Persist state between tests without proper cleanup. **Why:** Stateful tests are prone to creating flaky tests, where the results can vary depending on environment and order of execution. This makes debugging difficult and reduces confidence in the test suite. ### 1.2. Clear State Initialization **Rule:** Explicitly define the initial state within your tests or mock implementations if the system under test relies on it. **Do This:** * Provide default values when mocking state. * Use "beforeEach" or "beforeAll" hooks to initialize state before running tests. * Document the state initialization process. **Don't Do This:** * Rely on implicit or undefined state. * Leave state initialization ambiguous or undocumented. **Why:** Clearity in state setup avoids assumptions that can lead to errors and inconsistencies. Documented initialization helps new developers understand the dependencies within the tested system. ### 1.3. Controlled Mutations **Rule:** When state needs to be mutated during a test, do so in a controlled and predictable manner. **Do This:** * Use controlled mutation functions on state objects. * Make assertions on the expected state after interaction. * Where applicable, test the reducer/state-altering function in isolation. **Don't Do This:** * Mutate state directly without proper tracking. * Make assumptions about the state after actions without explicitly asserting it. **Why:** Controlled mutations are crucial for verifying the system's behavior following state transition. Assertions help ensure the transition is correct. ## 2. Managing Application State ### 2.1. Mocking State Management Libraries (Redux, Zustand, Pinia etc.) **Rule**: When testing components or modules that interact with state management libraries, mock the library to control the state in a test environment and decouple your tests from the complexities of the actual state management setup. **Do This:** * Use "jest.mock()" to replace the actual state management instances with mock implementations. * Create getter and setter mock functions for the state within the mock implementation. * Implement "dispatch" or "update" function mocks that simulate state changes. **Don't Do This:** * Test the state management library itself. Focus on testing the component's behavior in response to the state changes. * Import the real store/state directly into your tests, which tightly couples the test to the real implementation. **Why:** Mocking decouples the test from the actual state management implementation, making tests faster, more resilient to changes, and easier to understand. **Example (Redux):** """javascript // myComponent.test.js import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; // Install redux-mock-store! import MyComponent from './MyComponent'; // Mock the redux store const mockStore = configureStore([]); describe('MyComponent', () => { it('should update state on button click', () => { const initialState = { myReducer: { count: 0, } }; const store = mockStore(initialState); render( <Provider store={store}> <MyComponent /> </Provider> ); const button = screen.getByText('Increment'); fireEvent.click(button); const actions = store.getActions(); expect(actions[0].type).toEqual('INCREMENT_COUNT'); }); }); //MyComponent import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; const MyComponent = () => { const dispatch = useDispatch(); const count = useSelector(state => state.myReducer.count); const handleIncrement = () => { dispatch({ type: 'INCREMENT_COUNT' }); }; return ( <div> <p>Count: {count}</p> <button onClick={handleIncrement}>Increment</button> </div> ); }; export default MyComponent; """ **Example (Zustand):** """javascript // myComponent.test.js import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import useMyStore from './myStore'; // Assuming you have a Zustand store import MyComponent from './MyComponent'; // Jest mock the store. Always do before imports! jest.mock('./myStore', () => { const mockSet = jest.fn(); // This will track calls to set return { __esModule: true, // This is important for ES module mocks! default: () => ({ count: 0, increment: mockSet, // Mock the increment method set: mockSet }), mockSet: mockSet }; }); describe('MyComponent', () => { it('should call increment when button is clicked', () => { render(<MyComponent />); const button = screen.getByText('Increment'); fireEvent.click(button); // Assert that set was called correctly expect(useMyStore().increment).toHaveBeenCalled(); }); it('should display the initial count', () => { render(<MyComponent />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); }); // myStore.js import { create } from "zustand"; const useMyStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })); export default useMyStore; // MyComponent.js import React from 'react'; import useMyStore from './myStore'; const MyComponent = () => { const { count, increment } = useMyStore(); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }; export default MyComponent; """ **Example (Pinia):** """javascript // myComponent.test.js import { render, screen, fireEvent } from '@testing-library/react'; import { useMyStore } from './myStore'; import MyComponent from './MyComponent'; import { setActivePinia, createPinia } from 'pinia'; // Import Pinia functions // Jest mock the store. Always do before imports! jest.mock('./myStore', () => { const mockIncrement = jest.fn(); return { __esModule: true, // Important for ES module mocks useMyStore: () => ({ count: 0, increment: mockIncrement, $patch: jest.fn() //mock patch }), mockIncrement: mockIncrement }; }); describe('MyComponent', () => { beforeEach(() => { // setup Pinia for each test setActivePinia(createPinia()) }) it('should call increment when button is clicked', () => { render(<MyComponent />); const button = screen.getByText('Increment'); fireEvent.click(button); // Assert that increment was called on the mock store expect(useMyStore().increment).toHaveBeenCalled(); }); it('should display the initial count', () => { render(<MyComponent />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); }); // myStore.js import { defineStore } from 'pinia' export const useMyStore = defineStore('myStore', { state: () => { return { count: 0 } }, actions: { increment() { this.count++ }, }, }) // MyComponent.js import React from 'react'; import { useMyStore } from './myStore'; const MyComponent = () => { const myStore = useMyStore(); const { count, increment } = myStore; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }; export default MyComponent; """ **Common Anti-Patterns:** * **Testing Implementation Details:** Testing the internal workings of the state management library rather than the component's behavior is brittle. * **Skipping Mocks:** Not mocking leads to integration tests instead of isolated unit tests, making it hard to pinpoint issues. ### 2.2. Context API **Rule**: When testing components using React's Context API, mock the context provider to control the context value within the test environment. **Do This:** * Create a mock context provider. * Provide the mock provider with a controlled value. * Wrap the component under test with the mock provider. **Don't Do This:** * Rely on the actual context implementation during testing. * Modify the global context directly in tests. **Why:** This ensures that only the necessary value is tested without dependencies on the broader implementation details. **Example:** """javascript // theme-context.js (Simplified context example) import React, { createContext, useState, useContext } from 'react'; const ThemeContext = createContext(); export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); }; export const useTheme = () => useContext(ThemeContext); //ThemedComponent.js import React from 'react'; import { useTheme } from './theme-context'; const ThemedComponent = () => { const { theme, toggleTheme } = useTheme(); return ( <div style={{ backgroundColor: theme === 'light' ? 'white' : 'black', color: theme === 'light' ? 'black' : 'white' }}> <p>Current Theme: {theme}</p> <button onClick={toggleTheme}>Toggle Theme</button> </div> ); }; export default ThemedComponent; // ThemedComponent.test.js import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import ThemedComponent from './ThemedComponent'; import { ThemeContext } from './theme-context'; // Mock context provider const MockThemeProvider = ({ theme, toggleTheme, children }) => ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); describe('ThemedComponent', () => { it('should display the correct theme from context', () => { render( <MockThemeProvider theme="dark" toggleTheme={() => {}}> <ThemedComponent /> </MockThemeProvider> ); expect(screen.getByText('Current Theme: dark')).toBeInTheDocument(); }); it('should call toggleTheme when the button is clicked', () => { const toggleThemeMock = jest.fn(); render( <MockThemeProvider theme="light" toggleTheme={toggleThemeMock}> <ThemedComponent /> </MockThemeProvider> ); fireEvent.click(screen.getByText('Toggle Theme')); expect(toggleThemeMock).toHaveBeenCalledTimes(1); }); }); """ **Common Anti-Patterns:** * **Directly Importing Context:** Avoid importing the actual "ThemeProvider" in test files, which would defeat the purpose of isolation. * **Ignoring Context:** Neglecting to mock the context can lead to tests unknowingly relying on default or global context value. ### 2.3. State Machines (XState, etc.) **Rule**: When your application uses state machines, test the state transitions and side effects of each transition. **Do This:** * Instantiate the state machine with a mock context. * Send events to the state machine. * Assert that the state machine transitions to the expected state. * Assert that any side effects are triggered (through mocked functions). **Don't Do This:** * Test the state machine library itself. Test *your* machine definition. * Ignore testing side effects that depend on the state machine. **Why:** State machines are deterministic; ensuring correctness requires explicitly verifying transitions and side effects. **Example (XState):** """javascript // bookingMachine.js (Simplified XState example) import { createMachine, assign } from 'xstate'; const bookingMachine = createMachine({ id: 'booking', initial: 'idle', context: { passengers: 0, }, states: { idle: { on: { ADD_PASSENGER: { actions: assign({ passengers: (context) => context.passengers + 1 }) } } }, // ... more states } }); export default bookingMachine; // bookingMachine.test.js import bookingMachine from './bookingMachine'; import { Interpreter } from 'xstate'; describe('bookingMachine', () => { it('should increment passengers when ADD_PASSENGER event is sent', () => { const service = new Interpreter(bookingMachine.withContext({ passengers: 0 })); //Provide a context for our machine to use to set our initial passengers value to 0 service.start(); service.send({ type: 'ADD_PASSENGER' }); expect(service.state.context.passengers).toBe(1); service.send({ type: 'ADD_PASSENGER' }); expect(service.state.context.passengers).toBe(2); }); }); """ **Explanation:** 1. **(bookingMachine.js)**: Define the state machine using "createMachine" from XState. This machine manages passengers. 2. **(bookingMachine.test.js)**: * Import the state machine definition. * Create an "Interpreter" instance and start it. * Send events to the state machine using "service.send()". * Assert the state changes using "expect(service.state.value).toBe(...)" and context changes with "expect(service.state.context.passengers)" ## 3. Dealing with Global State ### 3.1. Isolating Global State Modifications **Rule**: Modifications to global state should be isolated and reverted after each test to prevent cross-test contamination. **Do This:** * Use "jest.spyOn" and "mockRestore" to restore original implementations. * Store previous values of global variables and restore them in "afterEach" or "afterAll" blocks. **Don't Do This:** * Leave global state modifications without reverting them. * Rely on default browser or Node.js environments to handle global state. **Why:** Modifying globals can create unpredictable behavior that is difficult to trace. **Example:** """javascript // moduleThatModifiesGlobal.js export function modifyGlobal(newValue) { global.myGlobalVariable = newValue; } // moduleThatReadsGlobal.js export function readGlobal() { return global.myGlobalVariable; } // moduleThatModifiesGlobal.test.js import { modifyGlobal } from './moduleThatModifiesGlobal'; import { readGlobal } from './moduleThatReadsGlobal'; describe('Global State Modification', () => { const originalValue = global.myGlobalVariable; // Store original value afterEach(() => { global.myGlobalVariable = originalValue; // Restore value after each test }); it('should modify global state', () => { modifyGlobal('new value'); expect(readGlobal()).toBe('new value'); }); it('should not affect other tests', () => { expect(readGlobal()).toBe(originalValue); //Make sure it is the original value }); }); """ ### 3.2. Mocking Browser APIs **Rule:** When testing code that relies on browser APIs (e.g., "window", "document", "localStorage"), mock these objects to ensure tests are not affected by the actual browser environment. **Do This:** * Use "jest.spyOn" to mock methods and properties of browser APIs. * Create mock implementations for complex objects like "localStorage". * Restore mocks after each test using "mockRestore". **Don't Do This:** * Run tests directly in a browser environment without mocking. * Hardcode browser-specific values in tests without mocking. **Why:** Using mocks will make the tests more predictable regardless of the environment they are running in. **Example:** """javascript // moduleUsingLocalStorage.js export function saveToLocalStorage(key, value) { localStorage.setItem(key, value); } export function getFromLocalStorage(key) { return localStorage.getItem(key); } // moduleUsingLocalStorage.test.js import { saveToLocalStorage, getFromLocalStorage } from './moduleUsingLocalStorage'; describe('LocalStorage', () => { let localStorageMock; beforeEach(() => { localStorageMock = { getItem: jest.fn(), setItem: jest.fn(), clear: jest.fn(), }; global.localStorage = localStorageMock; }); afterEach(() => { global.localStorage = undefined; }); it('should save to localStorage', () => { saveToLocalStorage('myKey', 'myValue'); expect(localStorageMock.setItem).toHaveBeenCalledWith('myKey', 'myValue'); }); it('should get from localStorage', () => { localStorageMock.getItem.mockReturnValue('myValue'); const value = getFromLocalStorage('myKey'); expect(localStorageMock.getItem).toHaveBeenCalledWith('myKey'); expect(value).toBe('myValue'); }); }); """ ## 4. Asynchronous State Updates ### 4.1. Waiting for Updates **Rule:** Use Jest's asynchronous testing tools to properly handle and wait for state updates triggered by asynchronous operations to ensure updates are complete before assertions are made. **Do This:** * Use "async/await" with "Promise" based updates * Use "waitFor" or "findBy*" methods from "@testing-library/react" which handle re-renders. * Utilize "jest.advanceTimersByTime" if timers are involved in state transition. **Don't Do This:** * Avoid using "setTimeout" with arbitrary wait times; instead, use methods that wait for specific conditions to be met. * Forget to await asynchronous updates before making assertions, which can lead to false positives or negatives. **Why:** Async operations, such as fetching data from an API, can cause components and their state to update after an initial render. Waiting for these updates ensures your assertions are accurate and reliable. **Example with "async/await":** """javascript // Component triggering asynchronous state update const AsyncComponent = () => { const [data, setData] = React.useState(null); React.useEffect(() => { const fetchData = async () => { const result = await Promise.resolve({ message: "Hello Async" }); setData(result); }; fetchData(); }, []); return <div>{data ? data.message : 'Loading...'}</div>; }; // Test with async/await import { render, screen, waitFor } from '@testing-library/react'; it('should update state asynchronously', async () => { render(<AsyncComponent />); // Wait for the 'Loading...' text to disappear await waitFor(() => screen.getByText('Hello Async')); // Now assert that the updated state is rendered expect(screen.getByText('Hello Async')).toBeInTheDocument(); }); """ ### 4.2. Mocking Asynchronous Functions **Rule:** When testing asynchronous state updates, mock the asynchronous functions (e.g., API calls) to control the outcome and timing of the updates. **Do This:** * Use "jest.spyOn" to mock functions that cause asynchronous updates. * Use "mockResolvedValue" or "mockRejectedValue" to control the promise's resolution or rejection. * Use "async/await" in the test to handle the asynchronous nature of the mock. **Don't Do This:** * Call actual APIs during tests, leading to slow and unreliable tests. * Forget to handle promise rejections, which can cause unhandled promise rejection errors. **Why:** It is important to control the behavior so when assertions are made, you know the system under test matches your expectations. **Example:** """javascript //Component fetching data which affects state const DataFetchingComponent = () => { const [data, setData] = React.useState(null); React.useEffect(() => { const fetchData = async () => { const response = await fetch('/api/data'); const result = await response.json(); setData(result); }; fetchData(); }, []); return <div>{data ? data.message : 'Loading...'}</div>; }; // Mocking an API call it('should fetch and display data', async () => { const mockData = { message: 'Mocked Data' }; //Before render, mock the value being returned. global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve(mockData), }) ); render(<DataFetchingComponent />); // Wait for the data to load and the component to update await waitFor(() => expect(screen.getByText('Mocked Data')).toBeInTheDocument()); }); """ ## 5. Performance Considerations ### 5.1. Avoiding Excessive Renders **Rule:** Optimize test performance by minimizing the number of renders triggered during tests, especially for complex components. **Do This:** * Use "React.memo" or "useMemo" to prevent unnecessary re-renders. * Write tests to only trigger updates in the specific parts of the component being tested. * Use "act" from React Testing Library to batch multiple state updates when necessary **Don't Do This:** * Trigger broad or unnecessary state updates. * Ignore performance warnings or logs related to excessive renders. **Why:** Reducing the number of renders will speed up the running the tests. ### 5.2. Minimize Test Setup **Rule:** Keep test setup minimal to reduce the overall execution time of test suites. **Do This:** * Only mock the state and dependencies required for each test. * Use the same setup and teardown for similar tests * Refactor redundant setups. **Don't Do This:** * Create overly complex or unnecessary setups. * Repeat setup code across multiple tests without refactoring. **Why:** Minimizing the setup means less time consumed before the actual testing. ## 6. Security Considerations ### 6.1. Sensitive Information **Rule:** Avoid storing any sensitive or production-specific data in the jest environment, and specifically avoid committing it to your source control (e.g., using ".gitignore"). **Do This:** * Use placeholders or mock values for sensitive data (API keys, passwords, IDs). * Ensure test data are not related to any actual production data. * Use environment variables or protected config files external to source control where necessary. **Don't Do This:** * Put secrets in plain text in test files. * Commit test files to your source repository that may expose such data. **Why:** Prevents accidental leaking of credentials or production-specific information. This also makes test data consistent and prevents tests failing due to changes in production information. ### 6.2. Input Validation **Rule:** Test for appropriate input sanitization and validation for any component/function that takes user-provided, or external data to prevent injection or cross-site scripting (XSS) issues. **Do This:** * Check behavior with invalid, malicious, and edge-case inputs. * Test for proper encoding or escaping of output to prevent XSS. **Example:** """javascript // Component accepting user input const UserInputComponent = ({ onSubmit }) => { const [userInput, setUserInput] = React.useState(''); const handleSubmit = () => { onSubmit(userInput); // Submit user input }; return ( <div> <input value={userInput} onChange={e => setUserInput(e.target.value)} /> <button onClick={handleSubmit}>Submit</button> </div> ); }; // Test for input sanitization it('should sanitize user input', () => { const onSubmit = jest.fn(); render(<UserInputComponent onSubmit={onSubmit} />); const maliciousInput = '<script>alert("XSS");</script>'; fireEvent.change(screen.getByRole('textbox'), { target: { value: maliciousInput } }); fireEvent.click(screen.getByRole('button')); expect(onSubmit).toHaveBeenCalledWith(expect.not.stringContaining('<script>')); // Ensure input is sanitized }); """ ## Conclusion Following these state management standards for Jest tests ensures robust, reliable, and maintainable test suites. These standards contribute to high-quality software and enable developers to work with confidence. Adhering to these standards and continuously refining them based on project-specific requirements will lead to better testing practices and contribute to the overall success of your team's projects.
# Testing Methodologies Standards for Jest This document outlines the testing methodologies standards for Jest, a delightful JavaScript Testing Framework with a focus on simplicity. These standards aim to guide developers in writing effective, maintainable, and performant tests, ensuring high-quality code and reliable applications. ## 1. Unit Testing Unit testing focuses on testing individual components or functions in isolation. In Jest, this is the most common type of testing. ### 1.1. Standards * **Do This:** Test individual functions or components in isolation. Mock dependencies to avoid external influences and focus on the unit's behavior. * **Don't Do This:** Test multiple units or external dependencies within a single unit test. This makes tests brittle and obscures the cause of failures. **Why This Matters:** Isolating units ensures that failures are directly attributable to the code in question, improving maintainability and debugging. ### 1.2. Code Examples **Example: Testing a simple function** """javascript // math.js const add = (a, b) => a + b; const subtract = (a, b) => a - b; module.exports = { add, subtract }; """ """javascript // math.test.js const { add, subtract } = require('./math'); describe('Math functions', () => { it('should add two numbers correctly', () => { expect(add(2, 3)).toBe(5); }); it('should subtract two numbers correctly', () => { expect(subtract(5, 2)).toBe(3); }); }); """ **Example: Mocking dependencies** """javascript // api.js const fetchData = async (url) => { const response = await fetch(url); const data = await response.json(); return data; }; module.exports = { fetchData }; """ """javascript // api.test.js const { fetchData } = require('./api'); describe('fetchData', () => { it('should return data from the API', async () => { global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ id: 1, name: 'Test' }), }) ); const data = await fetchData('https://example.com/api'); expect(data).toEqual({ id: 1, name: 'Test' }); expect(fetch).toHaveBeenCalledWith('https://example.com/api'); global.fetch.mockClear(); // Clear the mock after the test }); }); """ ### 1.3. Anti-Patterns to Avoid * **Testing Implementation Details:** Avoid testing private methods or internal states directly. Focus on the public API and observable behavior. * **Over-mocking:** Mock only the necessary dependencies. Over-mocking can lead to tests that don't reflect the actual behavior of the system. * **Writing Fragile Tests:** Tests should not break due to minor refactoring changes that do not alter the functionality. ## 2. Integration Testing Integration testing verifies the interaction between two or more units or components. It ensures that units work together correctly. ### 2.1. Standards * **Do This:** Test the interaction between distinct units or modules within the application. Use real dependencies where appropriate, but be mindful of external services. * **Don't Do This:** Overlap integration tests with unit tests (testing units in isolation) or end-to-end tests (testing the entire system). **Why This Matters:** Integration tests catch errors that unit tests might miss, ensuring components work together seamlessly. ### 2.2. Code Examples **Example: Testing the interaction between two modules** """javascript // user.js const api = require('./api'); const getUser = async (id) => { const user = await api.fetchUser(id); return user; }; module.exports = { getUser }; """ """javascript // api.js const fetchUser = async (id) => { const response = await fetch("https://example.com/users/${id}"); const user = await response.json(); return user; }; module.exports = { fetchUser }; """ """javascript // user.test.js const { getUser } = require('./user'); const api = require('./api'); jest.mock('./api'); // Mock the entire api module describe('getUser', () => { it('should fetch and return a user', async () => { api.fetchUser.mockResolvedValue({ id: 1, name: 'John Doe' }); // Mock the fetchUser function const user = await getUser(1); expect(user).toEqual({ id: 1, name: 'John Doe' }); expect(api.fetchUser).toHaveBeenCalledWith(1); }); }); """ **Example: Testing a React component with mocked API calls (using "jest.spyOn")** """jsx // components/UserList.jsx import React, { useState, useEffect } from 'react'; import { fetchUsers } from '../api'; function UserList() { const [users, setUsers] = useState([]); useEffect(() => { const loadUsers = async () => { const usersData = await fetchUsers(); setUsers(usersData); }; loadUsers(); }, []); return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } export default UserList; """ """javascript // api.js export const fetchUsers = async () => { const response = await fetch('https://example.com/users'); const data = await response.json(); return data; }; """ """javascript // components/UserList.test.jsx import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import UserList from './UserList'; import * as api from '../api'; describe('UserList Component', () => { it('should render a list of users fetched from the API', async () => { const mockUsers = [{ id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' }]; const spy = jest.spyOn(api, 'fetchUsers').mockResolvedValue(mockUsers); // Mock fetchUsers render(<UserList />); // Wait for the component to fetch and render the users await waitFor(() => { expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('Jane Smith')).toBeInTheDocument(); }); expect(spy).toHaveBeenCalledTimes(1); // Verify the API was called }); }); """ ### 2.3. Anti-Patterns to Avoid * **Testing Too Many Components Together:** Keep integration tests focused on specific interactions to avoid complex and brittle tests. * **Ignoring Error Handling:** Test how components handle errors during integration, such as API request failures. * **Using Real External Services Unnecessarily:** If possible, mock external services to ensure tests are fast and reliable. Use real services only when absolutely necessary and in a controlled environment. ## 3. End-to-End (E2E) Testing End-to-end testing validates the entire application flow, often simulating real user interactions. While Jest isn't typically used for full E2E testing (tools like Cypress or Playwright are preferred), it can be used for simpler E2E scenarios, especially when combined with tools like Puppeteer. ### 3.1. Standards * **Do This:** Simulate user workflows as closely as possible. Focus on critical paths and important user journeys. * **Don't Do This:** Overuse E2E tests for simple component rendering or logic. Those are better suited for unit or integration tests. Rely solely on E2E tests; balance with unit and integration tests. **Why This Matters:** E2E tests provide confidence that the entire application works correctly from the user's perspective. ### 3.2. Code Examples **Example: Using Jest with Puppeteer for a simple E2E test** First, install Puppeteer: "npm install puppeteer" """javascript // e2e.test.js const puppeteer = require('puppeteer'); describe('E2E Tests', () => { let browser; let page; beforeAll(async () => { browser = await puppeteer.launch(); // You can pass options here, like { headless: false } to see the browser page = await browser.newPage(); await page.goto('http://localhost:3000'); // Replace with your app's URL }); afterAll(async () => { await browser.close(); }); it('should display the correct title', async () => { const title = await page.title(); expect(title).toBe('My App'); }); it('should allow a user to fill out a form and submit it', async () => { await page.type('#name', 'John Doe'); await page.type('#email', 'john.doe@example.com'); await page.click('#submit'); // Wait for a success message (adjust selector as needed) await page.waitForSelector('.success-message'); const successMessage = await page.$eval('.success-message', el => el.textContent); expect(successMessage).toContain('Form submitted successfully!'); }); }); """ Ensure your application is running on "http://localhost:3000" (or adjust the URL in the test). ### 3.3. Anti-Patterns to Avoid * **Writing Brittle E2E Tests:** Avoid relying on specific CSS classes or IDs that are likely to change during development. Use more robust selectors (e.g., data-testid attributes). * **Ignoring Asynchronous Operations:** Make sure to "await" asynchronous operations in your E2E tests to avoid timing issues. * **Running E2E Tests Too Frequently:** Due to their long execution time, run E2E tests less frequently than unit or integration tests, typically as part of a CI/CD pipeline or before major releases. ## 4. Test-Driven Development (TDD) TDD is a development practice where you write tests *before* you write the code itself. This ensures that your code is testable and that you have a clear understanding of the requirements before you start coding. ### 4.1. Standards * **Do This:** Write a failing test before writing any code. Write the minimal code necessary to make the test pass. Refactor the code to improve its quality while keeping the tests passing (Red-Green-Refactor). * **Don't Do This:** Write code without tests, or write tests after the code is complete. **Why This Matters:** TDD leads to better code design, fewer bugs, and higher test coverage. ### 4.2. Workflow 1. **Red:** Write a test that defines a desired behavior or functionality. This test should initially fail because the code implementing that behavior doesn't exist yet. 2. **Green:** Write the minimal amount of code required to make the test pass. This code should focus solely on satisfying the test and might not be the most elegant or efficient solution. 3. **Refactor:** Refactor the code to improve its design, readability, and performance. During refactoring, ensure that all tests, including the one written in the "Red" phase, continue to pass. ### 4.3. Code Example **Example: Implementing TDD for a simple function** 1. **Red:** Write a failing test for a function that multiplies two numbers. """javascript // multiply.test.js const multiply = require('./multiply'); describe('multiply', () => { it('should multiply two numbers correctly', () => { expect(multiply(2, 3)).toBe(6); }); }); """ This test will fail because the "multiply" function doesn't exist. 2. **Green:** Write the minimal code to make the test pass. """javascript // multiply.js const multiply = (a, b) => { return a * b; }; module.exports = multiply; """ Now the test will pass. 3. **Refactor:** If needed, refactor the code. In this simple case, there isn't much to refactor. However, if the function had more complex logic, this would be the time to improve the code while ensuring the test still passes. ### 4.4. Benefits of TDD with Jest * **Clear Requirements:** TDD forces developers to clearly define the requirements before writing any code. * **Testable Code:** Since tests are written first, the code is naturally designed to be testable. * **High Test Coverage:** TDD results in high test coverage, as every piece of code is written with a corresponding test. * **Reduced Bugs:** Early detection of bugs through failing tests reduces the likelihood of introducing bugs into production. * **Confident Refactoring:** Refactoring becomes easier and safer because the tests provide a safety net, ensuring that changes don't break existing functionality. ## 5. Behaviour-Driven Development (BDD) BDD is an evolution of TDD that focuses on writing tests from the perspective of the user. ### 5.1. Standards * **Do This:** Write tests that describe the expected behavior of the system in a way that is understandable by both developers and non-technical stakeholders. Use descriptive language and focus on the "what" rather than the "how." * **Don't Do This:** Write tests that are too technical or implementation-specific. **Why This Matters:** BDD improves communication and collaboration between developers, testers, and stakeholders, ensuring that the system meets the needs of the users. ### 5.2. BDD with Jest While Jest doesn't enforce a specific BDD syntax, it is possible to write BDD-style tests using Jest's "describe" and "it" blocks. ### 5.3. Code Example **Example: Implementing BDD for a user login feature** """javascript describe('User Login', () => { it('Given a user is on the login page', () => { // Arrange: Set up the initial state (e.g., navigate to the login page) // ... }); it('When the user enters valid credentials and submits the form', () => { // Act: Perform the action (e.g., enter email and password, click submit) // ... }); it('Then the user should be redirected to their dashboard', () => { // Assert: Verify the outcome (e.g., check if the user is redirected to the dashboard URL) // ... expect(window.location.href).toBe('/dashboard'); }); it('And a success message should be displayed', () => { // Assert: Verify the outcome (e.g., check if a success message is displayed) // ... expect(screen.getByText('Login successful!')).toBeInTheDocument(); }); it('But if the user enters invalid credentials', () => { }); it('Then an error message should be displayed', () => { // Assert: Verify the outcome (e.g., check if an error message is displayed) // ... expect(screen.getByText('Invalid credentials')).toBeInTheDocument(); }); }); """ **Explanation:** * The "describe" block describes the feature being tested (User Login). * Each "it" block describes a specific scenario or behavior from the user's perspective. * The "Given", "When", "Then" structure (Arrange, Act, Assert) helps to organize the tests and make them more readable. While not enforced by Jest, using comments to indicate these helps with organization, as does use of distinct it() calls for each step. ### 5.4. Benefits of BDD with Jest * **Improved Communication:** BDD promotes collaboration by using a common language that is understandable by everyone on the team. * **Focus on User Needs:** BDD ensures that the system is built with the needs of the users in mind. * **Comprehensive Testing:** BDD encourages developers to think about all possible scenarios and edge cases. * **Executable Specifications:** BDD tests serve as executable specifications that can be used to verify that the system behaves as expected. ## 6. Property-Based Testing (PBT) Property-based testing involves defining properties that the code should satisfy for all valid inputs, rather than just a few specific examples. Jest does not have built-in PBT support but can be combined with libraries like "jsverify" or "fast-check" to enable PBT. ### 6.1. Standards * **Do This:** Identify and define properties that should hold true for all valid inputs. Write tests that generate random inputs and verify that the properties are satisfied. * **Don't Do This:** Use PBT for simple unit tests where example-based testing is sufficient. Overcomplicate the test suite by applying PBT where it is not needed. **Why This Matters:** PBT helps uncover edge cases and unexpected behavior that example-based testing might miss, leading to more robust and reliable code. ### 6.2. Code Example **Example: Using Jest with "fast-check" for property-based testing** First, install "fast-check": "npm install fast-check" """javascript // add.js const add = (a, b) => a + b; // Deliberate bug: should handle negative numbers correctly module.exports = add; """ """javascript // add.test.js const fc = require('fast-check'); const add = require('./add'); describe('add', () => { it('should satisfy the commutative property', () => { fc.assert( fc.property(fc.integer(), fc.integer(), (a, b) => { return add(a, b) === add(b, a); }) ); }); it('should satisfy the identity property (adding zero)', () => { fc.assert( fc.property(fc.integer(), (a) => { return add(a, 0) === a; }) ); }); it('should always return a number', () => { fc.assert( fc.property(fc.integer(), fc.integer(), (a, b) => { return typeof add(a, b) === 'number'; }) ); }); }); """ ### 6.3. Benefits of PBT with Jest * **Increased Test Coverage:** PBT generates a wide range of inputs, providing much greater coverage than example-based testing. * **Discovery of Edge Cases:** PBT can uncover edge cases and unexpected behavior that might be missed by manual testing or example-based testing. * **Improved Code Reliability:** By ensuring that the code satisfies properties for all valid inputs, PBT leads to more robust and reliable code. ## 7. Accessibility Testing Accessibility Testing ensures your application is usable by people with disabilities ### 7.1. Standards * **Do This:** Implement accessibility checks using tools like "jest-axe" or "axe-core". Write tests that check for common accessibility issues. * **Don't Do This:** Ignore accessibility concerns in testing. Rely solely on manual accessibility testing - automate as much as possible. **Why This Matters:** Accessibility testing ensures your application is inclusive and compliant with accessibility standards. ### 7.2. Code Example: """javascript // Install jest-axe // npm install jest-axe axe-core --save-dev import React from 'react'; import { render } from '@testing-library/react'; import { axe, toHaveNoViolations } from 'jest-axe'; expect.extend(toHaveNoViolations); function MyComponent() { return ( <div> <h1>Hello World</h1> <img src="example.jpg" alt="An example image" /> </div> ); } describe('Accessibility tests', () => { it('should not have any accessibility violations', async () => { const { container } = render(<MyComponent />); const results = await axe(container); expect(results).toHaveNoViolations(); }); }); """ ## 8. Performance Testing Performance testing checks how your code performs under different conditions, such as high load or stress. Jest is primarily focused on functional testing, but it can be used for basic performance testing. More comprehensive performance testing might require dedicated tools. ### 8.1. Standards * **Do This:** Measure the execution time of critical functions or components. Set performance budgets and write tests that ensure these budgets are met. * **Don't Do This:** Neglect performance considerations in testing. Assume that performance issues will be caught during runtime. Treat Jest as a replacement for dedicated performance testing tools when complex profiling is needed. ### 8.2. Code Example: measuring execution time """javascript describe('Performance tests', () => { it('should execute within the performance budget', () => { const startTime = performance.now(); // Code to be tested for (let i = 0; i < 100000; i++) { Math.sqrt(i); } const endTime = performance.now(); const executionTime = endTime - startTime; const performanceBudget = 50; // milliseconds expect(executionTime).toBeLessThan(performanceBudget); }); }); """ **Example: Using "jest.useFakeTimers" to control time-related behavior** """javascript describe('setTimeout', () => { beforeEach(() => { jest.useFakeTimers(); // Enable fake timers }); afterEach(() => { jest.clearAllTimers(); // Clear all timers after each test }); it('should call the callback after the specified delay', () => { const callback = jest.fn(); setTimeout(callback, 1000); // Fast-forward the timers jest.advanceTimersByTime(1000); expect(callback).toHaveBeenCalledTimes(1); }); }); """ ## 9. Security Testing Security testing identifies vulnerabilities in your code that could be exploited by attackers. Unit tests aren't appropriate for full security review, but security-minded unit tests are valuable. ### 9.1 Standards * **Do This**: Test code against common vulnerabilities like XSS and SQL injection by providing malicious inputs * **Don't Do This**: Assume frameworks prevent all security vulnerabilities. Ignore validation or sanitization of untrusted data. ### 9.2 Code Example: testing for XSS vulnerabilities in a component """javascript import React from 'react'; import { render } from '@testing-library/react'; function DisplayText({ text }) { return <div>{text}</div>; } describe('DisplayText Component', () => { it('should not render malicious HTML (XSS protection)', () => { const maliciousText = '<img src="x" onerror="alert(\'XSS\')" />'; const { container } = render(<DisplayText text={maliciousText} />); // Check that the XSS payload is not executed expect(container.innerHTML).not.toContain('<img src="x" onerror="alert(\'XSS\')"'); //May see <img, > //Check that it shows up as literal text expect(container.textContent).toContain(maliciousText); }); }); """ ## 10. Mocking Strategies Effective mocking is crucial for unit and integration testing. Jest provides powerful mocking capabilities. ### 10.1. Standards * **Do This:** Use "jest.mock()" to mock entire modules or "jest.spyOn()" to mock specific functions within a module. Use mock implementations to define the behavior of mocked functions. * **Don't Do This:** Over-mock dependencies, as this can lead to tests that don't accurately reflect the behavior of the system. Mock external dependencies that are unlikely to change or are already well-tested. ### 10.2. Code Example: **Example: Mocking a module with "jest.mock()"** """javascript // api.js export const fetchData = async (url) => { const response = await fetch(url); const data = await response.json(); return data; }; """ """javascript // api.test.js import { fetchData } from './api'; jest.mock('./api', () => ({ fetchData: jest.fn(() => Promise.resolve({ id: 1, name: 'Mocked Data' })), })); describe('fetchData', () => { it('should return mocked data from the API', async () => { const data = await fetchData('https://example.com/api'); expect(data).toEqual({ id: 1, name: 'Mocked Data' }); }); }); """ **Example: Spying on a function with "jest.spyOn()"** """javascript // utils.js export const greet = (name) => "Hello, ${name}!"; export const logGreeting = (name) => { const greeting = greet(name); console.log(greeting); return greeting; }; """ """javascript // utils.test.js import { greet, logGreeting } from './utils'; describe('logGreeting', () => { it('should call greet with the correct name', () => { const greetSpy = jest.spyOn(utils, 'greet'); //Spy on the utils.greet logGreeting('John'); expect(greetSpy).toHaveBeenCalledWith('John'); greetSpy.mockRestore(); // Restore the original method to avoid interference with other tests }); }); """ This comprehensive document provides a foundation for establishing Jest coding standards, promoting consistency, maintainability, and reliability in test suites. Regularly revisit and update these standards to align with new Jest features, evolving best practices, and the specific requirements of your projects.