Ensuring code reliability with unit, integration, and E2E testing.
Software testing is checking if your code works correctly before users encounter bugs. Instead of hoping everything works, you write automated tests that verify behavior - like having a robot assistant that constantly checks your code for mistakes.
Good tests catch bugs during development instead of production. Bad tests (or no tests) mean users discover your bugs for you.
Confidence to Change Code: Without tests, every code change is scary. You might break something without knowing. Tests let you refactor fearlessly.
Catch Bugs Early: Finding bugs during development costs minutes. Finding them in production costs hours of debugging, customer trust, and potentially money.
Living Documentation: Tests show how code should behave. New team members read tests to understand what the code does.
Faster Development: Counterintuitive, but tests speed up development. Manual testing after every change wastes time. Automated tests run in seconds.
Tests organized by speed and scope:
Unit Tests (70% of tests):
Integration Tests (20% of tests):
No related topics found.
End-to-End Tests (10% of tests):
Most tests should be fast unit tests. Few should be slow E2E tests.
Test individual functions independently:
// Function to test
function calculateDiscount(price, percentage) {
return price * (percentage / 100);
}
// Unit test
test('calculates 10% discount correctly', () => {
expect(calculateDiscount(100, 10)).toBe(10);
});
test('handles zero discount', () => {
expect(calculateDiscount(100, 0)).toBe(0);
});
You test the function with different inputs, verify outputs match expectations.
React Component Example:
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
test('renders button with correct label', () => {
render(<Button label="Click Me" onClick={() => {}} />);
expect(screen.getByText('Click Me')).toBeInTheDocument();
});
test('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button label="Click" onClick={handleClick} />);
fireEvent.click(screen.getByText('Click'));
expect(handleClick).toHaveBeenCalled();
});
Test how components/modules work together:
// Test API + Database interaction
test('creates user and returns correct data', async () => {
const userData = { name: 'Priya', email: 'priya@example.com' };
const response = await request(app)
.post('/api/users')
.send(userData);
expect(response.status).toBe(201);
expect(response.body.name).toBe(userData.name);
// Check database
const user = await db.users.findOne({ email: userData.email });
expect(user).toBeTruthy();
});
Tests the entire flow: API receives request → validates data → saves to database → returns response.
Test complete user journeys in a real browser:
// Playwright E2E test
test('user can complete checkout', async ({ page }) => {
// Go to product page
await page.goto('https://shop.example.com/product/123');
// Add to cart
await page.click('button:has-text("Add to Cart")');
// Go to checkout
await page.click('a:has-text("Checkout")');
// Fill form
await page.fill('#email', 'test@example.com');
await page.fill('#card', '4242424242424242');
// Submit
await page.click('button:has-text("Pay")');
// Verify success
await expect(page.locator('text=Order Confirmed')).toBeVisible();
});
This tests everything together: frontend, backend, database, payment integration - the entire system.
Jest: Most popular JavaScript testing framework. Fast, easy to use, great for React.
Vitest: Modern, faster alternative to Jest. Better Vite integration.
React Testing Library: Test React components how users interact with them, not implementation details.
Playwright/Cypress: E2E testing in real browsers. Playwright is faster and more reliable.
Supertest: Test Node.js APIs without starting a server.
Write tests before code:
Example:
// 1. Write test first (fails because function does not exist)
test('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
// 2. Write minimal code to pass
function add(a, b) {
return a + b;
}
// 3. Refactor if needed (tests ensure it still works)
TDD forces you to think about requirements before implementation. Tests guide design.
Do Test:
Do Not Test:
Replace external dependencies with fake versions:
// Mock API call
jest.mock('./api');
api.fetchUser.mockResolvedValue({ id: 1, name: 'Priya' });
test('displays user name', async () => {
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('Priya')).toBeInTheDocument();
});
});
Mocking makes tests faster and predictable. You control what the API returns instead of depending on real servers.
Percentage of code executed by tests:
80% coverage is good target. 100% is often overkill and diminishing returns.
npm test -- --coverage
Shows which lines are tested and which are not. Focus on testing critical paths, not achieving arbitrary coverage numbers.
Testing Implementation Details: Tests break when you refactor internal code, even though functionality is unchanged.
Too Many E2E Tests: They are slow and flaky. Most tests should be fast unit tests.
Not Testing Edge Cases: Tests pass on happy path but fail with empty arrays, null values, or unexpected input.
Ignoring Flaky Tests: Tests that randomly fail destroy confidence. Fix or delete them immediately.
Airbnb: Comprehensive test suite catches bugs before deployment. They rarely have critical production bugs.
Stripe: Payment systems cannot have bugs. Extensive testing ensures reliability. Their test suite runs thousands of tests on every commit.
GitHub: Tests run on every pull request. Code does not merge without passing tests. This maintains code quality at scale.
Tests run automatically on every commit:
# GitHub Actions example
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install
- run: npm test
If tests fail, deployment stops. Bugs never reach production.
Testing is not optional for professional software. It is the difference between "works on my machine" and "works reliably in production." Good tests save time, catch bugs early, and give confidence to ship code fast.
Start with unit tests for critical logic. Add integration tests for complex interactions. Use E2E tests sparingly for critical user flows. The investment in writing tests pays off exponentially in reduced debugging time and increased code quality.