## What is Software 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.
## Why Testing Matters
**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.
## The Testing Pyramid
Tests organized by speed and scope:
**Unit Tests** (70% of tests):
- Test individual functions/components in isolation
- Extremely fast (milliseconds)
- Catch logic errors early
**Integration Tests** (20% of tests):
- Test how multiple parts work together
- Moderate speed (seconds)
- Catch interaction bugs
**End-to-End Tests** (10% of tests):
- Test entire user flows in real browser
- Slow (minutes)
- Catch real-world issues
Most tests should be fast unit tests. Few should be slow E2E tests.
## Unit Testing
Test individual functions independently:
```javascript
// 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**:
```javascript
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();
});
```
## Integration Testing
Test how components/modules work together:
```javascript
// 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.
## End-to-End Testing
Test complete user journeys in a real browser:
```javascript
// 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.
## Testing Tools
**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.
## Test-Driven Development (TDD)
Write tests before code:
1. **Write failing test**: Define what code should do
2. **Write minimal code**: Make test pass
3. **Refactor**: Improve code while tests ensure it still works
**Example**:
```javascript
// 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.
## What to Test
**Do Test**:
- Business logic and calculations
- Edge cases (empty arrays, null values, boundary conditions)
- User interactions (button clicks, form submissions)
- API endpoints and database operations
- Error handling
**Do Not Test**:
- Third-party library code (they test their own code)
- Trivial getters/setters
- External services (mock them instead)
## Mocking
Replace external dependencies with fake versions:
```javascript
// 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.
## Test Coverage
Percentage of code executed by tests:
**80% coverage is good target**. 100% is often overkill and diminishing returns.
```bash
npm test -- --coverage
```
Shows which lines are tested and which are not. Focus on testing critical paths, not achieving arbitrary coverage numbers.
## Common Testing Mistakes
**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.
## Real-World Testing
**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.
## Testing in CI/CD
Tests run automatically on every commit:
```yaml
# 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.
## The Bottom Line
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.