As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
I want to talk about building a fortress around your JavaScript code. Not with walls, but with tests. Good tests are the safety net that lets you change things, add features, and fix bugs without that constant, nagging fear of breaking everything. Over the years, I've moved from writing simple console.log statements to using structured strategies that give me real confidence. Today, I'll share ten techniques that go beyond basic expect calls. These are the methods I use to handle complex applications, integrate with modern tools, and sleep well at night after a deployment.
Let's start at the foundation: how you organize your tests. Throwing hundreds of test files into a folder called __tests__ and hoping for the best is a recipe for maintenance headaches. I structure my test suites to reflect my application's architecture. I group tests by feature or module. This creates a clear map between what the code does and how it's verified. Think of it like a library. You wouldn't mix cookbooks with science textbooks. You organize them so anyone can find what they need.
Here's a conceptual model of how I think about test architecture. This isn't a specific library like Jest or Mocha, but it shows the principles they all use under the hood. A well-organized suite is predictable and scalable.
class TestArchitecture {
constructor() {
this.suites = new Map();
this.currentSuite = null;
}
describe(suiteName, suiteImplementation) {
const newSuite = {
name: suiteName,
tests: [],
beforeAll: [],
afterAll: [],
beforeEach: [],
afterEach: [],
parent: this.currentSuite
};
const previousSuite = this.currentSuite;
this.currentSuite = newSuite;
suiteImplementation();
this.currentSuite = previousSuite;
return newSuite;
}
it(testDescription, testFunction) {
if (!this.currentSuite) {
throw new Error('A test must live inside a describe block.');
}
this.currentSuite.tests.push({ description: testDescription, fn: testFunction });
}
beforeAll(hookFunction) {
this.currentSuite.beforeAll.push(hookFunction);
}
beforeEach(hookFunction) {
this.currentSuite.beforeEach.push(hookFunction);
}
}
// Using the structure
const framework = new TestArchitecture();
framework.describe('Shopping Cart Module', () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart(); // Fresh cart before each test
});
it('should start as empty', () => {
expect(cart.items.length).toBe(0);
});
it('should add an item correctly', () => {
cart.addItem({ id: 1, name: 'Book', price: 10 });
expect(cart.items.length).toBe(1);
});
describe('Total Calculation', () => {
beforeEach(() => {
cart.addItem({ id: 1, name: 'Book', price: 10 });
cart.addItem({ id: 2, name: 'Pen', price: 2 });
});
it('should sum item prices', () => {
expect(cart.getTotal()).toBe(12);
});
it('should apply a discount code', () => {
cart.applyDiscount('SAVE10');
expect(cart.getTotal()).toBe(10.8); // 12 - 10%
});
});
});
This nesting is powerful. The inner describe block for "Total Calculation" inherits the beforeEach from its parent. So, for its tests, the cart already has two items. This avoids repetition and makes the test intent very clear. The description reads almost like a specification: "Shopping Cart Module > Total Calculation > should apply a discount code."
The next technique is something I learned the hard way: mocking and stubbing effectively. In a real app, your function might call a database, fetch from an API, or write to a file. You can't run your tests on a real production database. This is where you simulate those external parts. The goal is to isolate the unit of code you're testing.
A common mistake is to mock everything. I focus on mocking external dependencies and leaving internal logic integrated. Here’s how I might mock a module that fetches user data.
// userService.js - The real module
import axios from 'axios';
export async function fetchUserData(userId) {
const response = await axios.get(`/api/users/${userId}`);
return response.data;
}
export function processUserData(rawData) {
return {
id: rawData.id,
fullName: `${rawData.firstName} ${rawData.lastName}`,
isActive: rawData.status === 'active'
};
}
// userService.test.js - The test with mocking
import { fetchUserData, processUserData } from './userService';
import axios from 'axios';
// Tell Jest to mock the entire axios module
jest.mock('axios');
describe('User Service', () => {
describe('fetchUserData', () => {
it('should return user data from the API', async () => {
// 1. Arrange: Set up the mock
const mockUser = { id: 123, firstName: 'John', lastName: 'Doe' };
axios.get.mockResolvedValue({ data: mockUser });
// 2. Act: Call the function under test
const result = await fetchUserData(123);
// 3. Assert
expect(result).toEqual(mockUser);
expect(axios.get).toHaveBeenCalledTimes(1);
expect(axios.get).toHaveBeenCalledWith('/api/users/123');
});
it('should throw an error on API failure', async () => {
// Arrange: Make the mock reject (simulate a failed request)
axios.get.mockRejectedValue(new Error('Network Error'));
// Act & Assert: Expect the promise to be rejected
await expect(fetchUserData(123)).rejects.toThrow('Network Error');
});
});
describe('processUserData', () => {
it('should format raw API data correctly', () => {
// Notice: No mocking here. This is a pure function test.
const rawData = { id: 123, firstName: 'Jane', lastName: 'Smith', status: 'active' };
const expected = { id: 123, fullName: 'Jane Smith', isActive: true };
const result = processUserData(rawData);
expect(result).toEqual(expected);
});
});
});
The key insight is what to mock. I mock axios.get because it's an external I/O operation. I don't mock processUserData when testing fetchUserData. They are in the same module, but one is a pure transformation. Testing them together, with the mock providing controlled input, is still a valuable unit test. Later, I test processUserData in isolation with direct inputs. This balance is crucial.
Snapshot testing is my third technique, and it's fantastic for UI components or any function that returns a complex, nested object structure. The first time you run a snapshot test, it takes the output of your function and saves it to a plain text file. On every subsequent test run, it compares the new output to the saved snapshot. If they differ, the test fails.
I use this for React components, configuration objects, or API response transformers. It catches unintended changes instantly. However, a word of caution: snapshot tests are not a replacement for thoughtful assertions. They are a complement. A failing snapshot just means something changed; you must decide if that change is correct.
// A function that generates a complex configuration object
function createWidgetConfig(theme, isPremium) {
const baseConfig = {
layout: 'grid',
animations: true,
theme: {
primary: theme?.primary || '#3498db',
background: theme?.background || '#f8f9fa'
}
};
if (isPremium) {
baseConfig.features = ['advanced-analytics', 'custom-widgets', 'priority-support'];
baseConfig.limits = { widgets: 50, users: 100 };
} else {
baseConfig.features = ['basic-analytics'];
baseConfig.limits = { widgets: 5, users: 1 };
}
// A non-deterministic part - we need to mock this for stable snapshots
baseConfig.buildId = process.env.BUILD_ID || `dev-${Date.now()}`;
return baseConfig;
}
// The snapshot test
describe('createWidgetConfig', () => {
// Mock the non-deterministic part before each test
beforeEach(() => {
// Mock the environment or Date for consistent snapshots
process.env.BUILD_ID = 'test-build-123';
jest.spyOn(Date, 'now').mockReturnValue(1640995200000); // Mock a fixed timestamp
});
afterEach(() => {
delete process.env.BUILD_ID;
jest.restoreAllMocks();
});
it('generates correct config for free tier', () => {
const config = createWidgetConfig({ primary: '#2ecc71' }, false);
// This creates/compares the snapshot
expect(config).toMatchSnapshot();
// On first run, creates __snapshots__/widgetConfig.test.js.snap with:
// exports[`createWidgetConfig generates correct config for free tier 1`] = `
// Object {
// "animations": true,
// "buildId": "test-build-123",
// "features": Array [
// "basic-analytics",
// ],
// "layout": "grid",
// "limits": Object {
// "users": 1,
// "widgets": 5,
// },
// "theme": Object {
// "background": "#f8f9fa",
// "primary": "#2ecc71",
// },
// }
// `;
});
it('generates correct config for premium tier', () => {
const config = createWidgetConfig({}, true);
expect(config).toMatchSnapshot(); // A different snapshot is saved for this test case
});
});
When the snapshot fails, you get a diff showing exactly what changed. You then run a command to update the snapshot if the change was intentional. This is incredibly fast for catching regressions in complex outputs. My rule is to use snapshots for structures that are stable and whose exact shape is important, like API contracts or component render trees.
My fourth technique is about testing not just the "happy path," but the edges and the failures. This is often called boundary or edge-case testing. What happens when you pass null? An empty string? A number larger than Number.MAX_SAFE_INTEGER? A network request that times out?
I make a list of these edges for every critical function. It feels tedious, but it’s where most bugs live. A function that works perfectly with correct data is only half-done.
// A simple validation function
function validatePassword(password) {
if (typeof password !== 'string') {
throw new TypeError('Password must be a string');
}
if (password.length < 8) {
return { isValid: false, error: 'Password must be at least 8 characters' };
}
if (!/[A-Z]/.test(password)) {
return { isValid: false, error: 'Password must contain an uppercase letter' };
}
if (!/[0-9]/.test(password)) {
return { isValid: false, error: 'Password must contain a number' };
}
return { isValid: true, error: null };
}
// The comprehensive edge-case test suite
describe('validatePassword', () => {
describe('valid passwords', () => {
// Happy paths
it('accepts a valid password', () => {
expect(validatePassword('SecurePass123')).toEqual({ isValid: true, error: null });
});
});
describe('invalid formats (type errors)', () => {
// Edge: Wrong type
it('throws on null', () => {
expect(() => validatePassword(null)).toThrow(TypeError);
});
it('throws on number', () => {
expect(() => validatePassword(12345678)).toThrow('Password must be a string');
});
it('throws on undefined', () => {
expect(() => validatePassword(undefined)).toThrow(TypeError);
});
it('throws on object', () => {
expect(() => validatePassword({})).toThrow(TypeError);
});
});
describe('invalid content', () => {
// Edge: Too short
it('rejects short passwords', () => {
expect(validatePassword('short')).toEqual({
isValid: false,
error: 'Password must be at least 8 characters'
});
});
// Edge: Boundary - exactly 7 chars
it('rejects 7-character passwords', () => {
expect(validatePassword('seven7')).toEqual({
isValid: false,
error: 'Password must be at least 8 characters'
});
});
// Edge: Boundary - exactly 8 chars but missing other criteria
it('rejects 8-char lowercase-only password', () => {
expect(validatePassword('password')).toEqual({
isValid: false,
error: 'Password must contain an uppercase letter'
});
});
// Edge: Missing number
it('rejects passwords without numbers', () => {
expect(validatePassword('NoNumberHere')).toEqual({
isValid: false,
error: 'Password must contain a number'
});
});
// Edge: Missing uppercase
it('rejects passwords without uppercase', () => {
expect(validatePassword('lowercase123')).toEqual({
isValid: false,
error: 'Password must contain an uppercase letter'
});
});
// Edge: Empty string (a string, but empty)
it('rejects empty string', () => {
expect(validatePassword('')).toEqual({
isValid: false,
error: 'Password must be at least 8 characters'
});
});
// Edge: Very long string (potential performance or truncation issue?)
it('handles very long passwords', () => {
const longPass = 'A' + '1'.repeat(10000);
const result = validatePassword(longPass);
expect(result.isValid).toBe(true);
});
});
});
This exhaustive approach gives me immense confidence. If someone later modifies the validatePassword function to, say, also require a special character, they will immediately see which of these tests break. It acts as living documentation for the function's contract.
Technique five is integration testing. Unit tests are in isolation, but your units need to work together. An integration test verifies that multiple modules interact correctly. For a web app, this might mean testing that when a user submits a form, the correct API is called, the state updates, and the UI changes—all without spinning up a full browser.
I often use a tool like Jest with jest.mock to mock only the very edges (like the actual fetch call or database driver) and let the rest of my application modules interact naturally.
// A simple integration flow: UserForm -> API Service -> State Store
// userFormIntegration.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import UserForm from './UserForm';
import { api } from './apiService';
import { userStore } from './userStore';
// Mock only the external API module
jest.mock('./apiService');
describe('User Form Integration', () => {
beforeEach(() => {
// Clear the store and mocks before each test
userStore.clear();
jest.clearAllMocks();
});
it('should submit form, call API, and update global store', async () => {
// 1. Render the component
render(<UserForm />);
// 2. Simulate user filling the form
await userEvent.type(screen.getByLabelText(/name/i), 'Alice Chen');
await userEvent.type(screen.getByLabelText(/email/i), 'alice@example.com');
fireEvent.click(screen.getByRole('button', { name: /save/i }));
// 3. Verify the API was called with correct data
expect(api.saveUser).toHaveBeenCalledTimes(1);
expect(api.saveUser).toHaveBeenCalledWith({
name: 'Alice Chen',
email: 'alice@example.com'
});
// 4. Simulate a successful API response
const mockResponse = { id: 'user-abc', name: 'Alice Chen', email: 'alice@example.com' };
api.saveUser.mockResolvedValue(mockResponse);
// 5. Wait for side-effects (like store update) to complete
await waitFor(() => {
expect(userStore.currentUser).toEqual(mockResponse);
});
// 6. Verify UI feedback (e.g., a success message appears)
expect(await screen.findByText('User saved successfully!')).toBeInTheDocument();
expect(screen.getByLabelText(/name/i).value).toBe(''); // Form should reset
});
it('should show an error message when API fails', async () => {
// Arrange: Mock a failing API
api.saveUser.mockRejectedValue(new Error('Network failure'));
render(<UserForm />);
await userEvent.type(screen.getByLabelText(/name/i), 'Bob');
await userEvent.type(screen.getByLabelText(/email/i), 'bob@test.com');
fireEvent.click(screen.getByRole('button', { name: /save/i }));
// Assert: Error UI is shown
expect(await screen.findByText(/network failure/i)).toBeInTheDocument();
expect(userStore.currentUser).toBeNull(); // Store should not be updated
});
});
This test doesn't require a browser or a real server. It tests the integration of the React component, the API service module, and the state store. The mock is only on the boundary (api.saveUser), letting the natural function calls between UserForm, apiService, and userStore happen. This catches bugs where modules are wired together incorrectly.
The sixth technique is property-based testing, a concept from functional programming. Instead of thinking of specific examples (like "password 'Abc123!' should be valid"), you describe the properties of your code. For example, "any string that meets criteria X, Y, Z should be valid." Then, a testing library generates hundreds of random inputs to check that property holds.
I use this for core business logic, like validation rules, mathematical functions, or data serialization. It often finds edge cases I never considered. In JavaScript, a library like fast-check or jsverify handles this.
import fc from 'fast-check';
// The function we want to test
function sortNumbersAscending(array) {
// Let's say we have a bug: it doesn't handle single-element arrays well
return [...array].sort((a, b) => a - b);
}
describe('sortNumbersAscending with fast-check', () => {
it('should have the same length as input', () => {
// Define the property: For any array of numbers...
fc.assert(
fc.property(fc.array(fc.integer()), (inputArray) => {
const sorted = sortNumbersAscending(inputArray);
return sorted.length === inputArray.length;
}),
{ seed: 42, path: '' } // Optional: seed for reproducible runs
);
});
it('should have elements in non-decreasing order', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (inputArray) => {
const sorted = sortNumbersAscending(inputArray);
for (let i = 1; i < sorted.length; i++) {
if (sorted[i - 1] > sorted[i]) {
return false; // Property violated
}
}
return true; // Property holds for this input
})
);
});
it('should be idempotent (sorting twice is the same as sorting once)', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (inputArray) => {
const onceSorted = sortNumbersAscending(inputArray);
const twiceSorted = sortNumbersAscending(onceSorted);
// Use a deep equality check
return JSON.stringify(onceSorted) === JSON.stringify(twiceSorted);
})
);
});
it('should contain the same elements as input (is a permutation)', () => {
// This is a stricter property: sorted array is a permutation of input
fc.assert(
fc.property(fc.array(fc.integer()), (inputArray) => {
const sorted = sortNumbersAscending(inputArray);
const inputSorted = [...inputArray].sort((a, b) => a - b);
return JSON.stringify(sorted) === JSON.stringify(inputSorted);
})
);
});
});
When you run this, fast-check might generate arrays like [], [0], [1, -999999999], [0, 0, 0], and thousands more. If a property fails, it will "shrink" the failing case to the smallest example that still fails, making debugging easy. For instance, it might find that our buggy sortNumbersAscending fails for the input [0]. This is powerful because a human tester might not think to test a single-element array.
Technique seven involves testing asynchronous code and timing. Modern JavaScript is full of promises, async/await, timeouts, and intervals. Testing these requires careful handling to avoid flaky tests that pass sometimes and fail other times.
The key is to make time deterministic. Use fake timers to control setTimeout, setInterval, and Date. Also, always ensure your tests wait for promises to resolve or reject.
// A function with async operations and a timer
function startPolling(apiEndpoint, callback, intervalMs = 1000) {
let isPolling = true;
async function poll() {
if (!isPolling) return;
try {
const response = await fetch(apiEndpoint);
const data = await response.json();
callback(null, data);
} catch (error) {
callback(error, null);
} finally {
if (isPolling) {
setTimeout(poll, intervalMs);
}
}
}
poll();
return {
stop: () => { isPolling = false; }
};
}
// The test
describe('startPolling', () => {
let originalFetch;
let mockCallback;
let fakeTimers;
beforeEach(() => {
// Mock fetch globally
originalFetch = global.fetch;
global.fetch = jest.fn();
mockCallback = jest.fn();
// Use Jest's fake timers
jest.useFakeTimers();
});
afterEach(() => {
global.fetch = originalFetch;
jest.useRealTimers();
});
it('should poll the endpoint at the specified interval', async () => {
// Arrange: Mock fetch to resolve successfully
global.fetch.mockResolvedValue({
json: async () => ({ status: 'ok', data: 'test' })
});
// Act: Start polling
const poller = startPolling('/api/status', mockCallback, 2000);
// Assert: Initial call should happen immediately
await Promise.resolve(); // Let the microtask queue clear for the first poll
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(global.fetch).toHaveBeenCalledWith('/api/status');
expect(mockCallback).toHaveBeenCalledWith(null, { status: 'ok', data: 'test' });
// Advance timer by interval
jest.advanceTimersByTime(2000);
await Promise.resolve(); // Clear microtasks again
expect(global.fetch).toHaveBeenCalledTimes(2);
expect(mockCallback).toHaveBeenCalledTimes(2);
// Advance timer again
jest.advanceTimersByTime(2000);
await Promise.resolve();
expect(global.fetch).toHaveBeenCalledTimes(3);
// Cleanup
poller.stop();
// Advance timer once more to ensure it stopped
jest.advanceTimersByTime(2000);
await Promise.resolve();
expect(global.fetch).toHaveBeenCalledTimes(3); // Should NOT be 4
});
it('should handle API errors', async () => {
const networkError = new Error('Timeout');
global.fetch.mockRejectedValue(networkError);
const poller = startPolling('/api/status', mockCallback, 1000);
await Promise.resolve(); // Let the first poll attempt settle
expect(mockCallback).toHaveBeenCalledWith(networkError, null);
poller.stop();
});
});
By taking control of time with jest.useFakeTimers() and advanceTimersByTime, I can simulate minutes or hours of polling in milliseconds. The await Promise.resolve() lines are crucial because even with mocked timers, promise resolution happens in the microtask queue, which needs a moment to process. This pattern makes async timing tests fast and reliable.
The eighth technique is contract testing, especially useful in a microservices architecture. When Service A depends on an API from Service B, how do you know changes to Service B won't break Service A? A unit test with mocks won't catch this because the mock stays the same. Contract testing ensures both sides agree on the "shape" of their interaction.
A simple form of this is to write tests that verify the structure of API responses or the parameters your code sends. You can use schema validators like ajv for JSON Schema or joi for runtime validation.
// apiContract.test.js - Tests that our code adheres to the expected API contract
import { validateUserApiResponse } from './apiSchemas'; // A Joi or Ajv validator
// This is a test that runs against a REAL, live development API
// It's often run in a CI stage against a deployed staging environment
describe('User API Contract', () => {
const BASE_URL = process.env.TEST_API_URL || 'http://localhost:3001';
it('GET /api/users/:id returns a valid user object', async () => {
const userId = 'test-user-1';
const response = await fetch(`${BASE_URL}/api/users/${userId}`);
const data = await response.json();
// This is the contract test: does the response match our expected schema?
const validationResult = validateUserApiResponse(data);
expect(validationResult.error).toBeUndefined();
expect(data).toHaveProperty('id', userId);
expect(data).toHaveProperty('name');
expect(data).toHaveProperty('email');
expect(typeof data.email).toBe('string');
// Schema does deeper validation (email format, UUID for id, etc.)
});
it('POST /api/users accepts and returns valid user data', async () => {
const newUser = {
name: 'Contract Test User',
email: 'contract-test@example.org',
password: 'ValidPass123'
};
const response = await fetch(`${BASE_URL}/api/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser)
});
expect(response.status).toBe(201);
const createdUser = await response.json();
// Validate the response schema
const validationResult = validateUserApiResponse(createdUser);
expect(validationResult.error).toBeUndefined();
// Verify the data matches what we sent (excluding server-generated fields)
expect(createdUser.name).toBe(newUser.name);
expect(createdUser.email).toBe(newUser.email);
expect(createdUser).toHaveProperty('id');
expect(createdUser).not.toHaveProperty('password'); // Should not leak password
});
});
// Example schema file (using Joi)
// apiSchemas.js
import Joi from 'joi';
export const userSchema = Joi.object({
id: Joi.string().uuid().required(),
name: Joi.string().min(1).max(100).required(),
email: Joi.string().email().required(),
createdAt: Joi.date().iso().required(),
updatedAt: Joi.date().iso().required()
}).strict(); // 'strict' means no extra properties allowed
export function validateUserApiResponse(data) {
return userSchema.validate(data, { abortEarly: false });
}
These contract tests are often run in a Continuous Integration pipeline against a staging environment before production deployment. They are not unit tests; they are integration tests that verify the real interface between services. If the backend team changes the email field to be an object with address and verified sub-fields, this test will fail immediately, signaling a breaking change that needs communication.
The ninth technique is visual regression testing for front-end applications. While snapshot testing checks the structure of your rendered component, visual regression testing checks the actual pixels. It takes screenshots of your UI in different states and compares them over time.
This is invaluable for catching CSS changes, layout shifts, or visual bugs that pure logic tests miss. Tools like Percy, Chromatic, or even Jest with puppeteer can be used.
// Example using Jest and puppeteer for a simple visual test
import puppeteer from 'puppeteer';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });
describe('Button Component Visual Regression', () => {
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch();
});
afterAll(async () => {
await browser.close();
});
beforeEach(async () => {
page = await browser.newPage();
await page.setViewport({ width: 800, height: 600 });
// Serve your static build or a test harness page
await page.goto('http://localhost:3000/test-button.html');
});
afterEach(async () => {
await page.close();
});
it('renders the primary button correctly', async () => {
const button = await page.waitForSelector('.btn-primary');
const screenshot = await button.screenshot();
// This compares against a stored baseline image.
// On first run, it saves the screenshot as the baseline.
// On subsequent runs, it fails if the pixels differ.
expect(screenshot).toMatchImageSnapshot({
failureThreshold: 0.01, // Allow 1% pixel difference (antialiasing, etc.)
failureThresholdType: 'percent'
});
});
it('shows correct hover state', async () => {
const button = await page.waitForSelector('.btn-primary');
await page.hover('.btn-primary');
// Wait for CSS transition
await page.waitForTimeout(100);
const screenshot = await button.screenshot();
expect(screenshot).toMatchImageSnapshot({
customSnapshotIdentifier: 'button-primary-hover' // Unique name for this state
});
});
});
Visual tests are more expensive to run than unit tests, so I use them selectively for key UI components and critical user flows. They give me confidence that a dependency update or a refactor didn't accidentally change a button's padding or a modal's shadow.
Finally, technique ten is about tooling integration and creating a smooth workflow. Tests should be easy to run, fast, and provide clear feedback. I integrate testing into every step: my editor, my pre-commit hooks, and my CI/CD pipeline.
In my package.json, I define multiple scripts for different contexts.
{
"scripts": {
"test": "jest --coverage --passWithNoTests",
"test:watch": "jest --watch",
"test:ui": "jest --watchAll --coverage",
"test:integration": "jest --testPathPattern=integration --runInBand",
"test:visual": "percy exec -- jest --testPathPattern=visual",
"test:contract": "jest --testPathPattern=contract --runInBand",
"test:ci": "jest --ci --coverage --maxWorkers=2",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"type-check": "tsc --noEmit",
"precommit": "npm run lint && npm run type-check && npm run test",
"prepare": "husky install"
}
}
I use husky to set up Git hooks. The pre-commit hook runs linters and fast unit tests to catch obvious issues before code is even committed. The full test suite, including integration and visual tests, runs in CI on every pull request.
I also configure my test runner (Jest) for optimal performance and reporting.
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/**/__tests__/**',
'!src/**/__mocks__/**'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testEnvironment: 'jsdom', // For React testing
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy', // Mock CSS imports
'^@components/(.*)$': '<rootDir>/src/components/$1' // Alias for clean imports
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
},
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
],
reporters: [
'default',
['jest-junit', { outputDirectory: 'test-results', outputName: 'junit.xml' }] // For CI reporting
]
};
This setup ensures that when I run npm test, I get a coverage report showing which parts of my code aren't exercised. The thresholds (branches: 80) enforce a minimum standard. The jest-junit reporter creates an XML file that CI systems like Jenkins or GitLab CI can parse to show test results and trends.
Testing is not a separate phase. It's part of how I write code. These ten techniques—from thoughtful organization and mocking to property-based testing and visual regression—form a multi-layered safety net. Each layer catches different types of bugs. Together, they allow me to move quickly, refactor with courage, and deliver software that works as expected. Start with one technique that addresses your biggest pain point, and gradually build your testing strategy. The time you invest will pay back many times over in reduced bugs and increased developer confidence.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)