testing-best-practices

📁 webdev70/hosting-google 📅 11 days ago
3
总安装量
3
周安装量
#59569
全站排名
安装命令
npx skills add https://github.com/webdev70/hosting-google --skill testing-best-practices

Agent 安装分布

augment 3
gemini-cli 3
antigravity 3
claude-code 3
github-copilot 3
codex 3

Skill 文档

Testing Best Practices

This skill provides comprehensive expert knowledge of testing Node.js/Express applications with emphasis on Jest and Supertest, test organization, mocking strategies, and achieving comprehensive test coverage.

Testing Framework Setup

Jest Installation and Configuration

Install dependencies:

npm install --save-dev jest supertest @types/jest

package.json configuration:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:verbose": "jest --verbose"
  },
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": [
      "/node_modules/"
    ],
    "testMatch": [
      "**/__tests__/**/*.js",
      "**/?(*.)+(spec|test).js"
    ]
  }
}

jest.config.js (advanced):

module.exports = {
  // Use Node.js test environment
  testEnvironment: 'node',

  // Test file patterns
  testMatch: [
    '**/__tests__/**/*.js',
    '**/*.test.js',
    '**/*.spec.js'
  ],

  // Coverage settings
  collectCoverageFrom: [
    'src/**/*.js',
    'routes/**/*.js',
    '!src/index.js', // Exclude entry point
    '!**/node_modules/**'
  ],

  // Coverage thresholds
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },

  // Setup files
  setupFilesAfterEnv: ['<rootDir>/test/setup.js'],

  // Clear mocks between tests
  clearMocks: true,

  // Verbose output
  verbose: true,

  // Timeout for tests
  testTimeout: 10000
};

Test Directory Structure

Option 1: Separate test directory:

project/
├── src/
│   ├── server.js
│   ├── routes/
│   │   └── api.js
│   └── utils/
│       └── validators.js
├── test/
│   ├── setup.js
│   ├── server.test.js
│   ├── routes/
│   │   └── api.test.js
│   └── utils/
│       └── validators.test.js
└── package.json

Option 2: Co-located tests:

project/
├── src/
│   ├── server.js
│   ├── server.test.js
│   ├── routes/
│   │   ├── api.js
│   │   └── api.test.js
│   └── utils/
│       ├── validators.js
│       └── validators.test.js
└── package.json

Option 3: tests directories:

project/
├── src/
│   ├── __tests__/
│   │   └── server.test.js
│   ├── server.js
│   ├── routes/
│   │   ├── __tests__/
│   │   │   └── api.test.js
│   │   └── api.js
└── package.json

Testing Express Applications with Supertest

Basic API Testing

const request = require('supertest');
const app = require('../server');

describe('GET /', () => {
  it('should return 200 status', async () => {
    const response = await request(app).get('/');
    expect(response.status).toBe(200);
  });

  it('should return JSON content type', async () => {
    const response = await request(app).get('/api/users');
    expect(response.headers['content-type']).toMatch(/json/);
  });

  it('should return users array', async () => {
    const response = await request(app).get('/api/users');
    expect(response.body).toHaveProperty('users');
    expect(Array.isArray(response.body.users)).toBe(true);
  });
});

describe('POST /api/users', () => {
  it('should create a user with valid data', async () => {
    const userData = {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'SecurePass123!'
    };

    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .set('Content-Type', 'application/json')
      .expect(201);

    expect(response.body).toHaveProperty('id');
    expect(response.body.email).toBe(userData.email);
    expect(response.body).not.toHaveProperty('password'); // Don't return password
  });

  it('should reject invalid email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John',
        email: 'invalid-email',
        password: 'SecurePass123!'
      })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toMatch(/email/i);
  });

  it('should reject weak password', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John',
        email: 'john@example.com',
        password: '123' // Too short
      })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toMatch(/password/i);
  });
});

describe('Authentication', () => {
  let authToken;

  beforeAll(async () => {
    // Create a test user and get token
    const response = await request(app)
      .post('/api/login')
      .send({
        email: 'test@example.com',
        password: 'TestPass123!'
      });

    authToken = response.body.token;
  });

  it('should access protected route with valid token', async () => {
    const response = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body).toHaveProperty('user');
  });

  it('should reject access without token', async () => {
    await request(app)
      .get('/api/profile')
      .expect(401);
  });

  it('should reject invalid token', async () => {
    await request(app)
      .get('/api/profile')
      .set('Authorization', 'Bearer invalid-token')
      .expect(401);
  });
});

Testing Proxy Endpoints

const request = require('supertest');
const axios = require('axios');
const app = require('../server');

// Mock axios
jest.mock('axios');

describe('POST /api/proxy', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should proxy request successfully', async () => {
    const mockData = {
      results: [
        { id: 1, name: 'Result 1' },
        { id: 2, name: 'Result 2' }
      ]
    };

    axios.post.mockResolvedValue({
      data: mockData,
      status: 200
    });

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(200);

    expect(response.body).toEqual(mockData);
    expect(axios.post).toHaveBeenCalledWith(
      expect.any(String),
      { query: 'test' },
      expect.any(Object)
    );
  });

  it('should handle proxy errors', async () => {
    axios.post.mockRejectedValue({
      response: {
        status: 500,
        data: { error: 'Internal Server Error' }
      }
    });

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(500);

    expect(response.body).toHaveProperty('error');
  });

  it('should handle network errors', async () => {
    axios.post.mockRejectedValue(new Error('Network error'));

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(500);

    expect(response.body).toHaveProperty('error');
  });

  it('should validate request before proxying', async () => {
    const response = await request(app)
      .post('/api/proxy')
      .send({ invalid: 'data' })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(axios.post).not.toHaveBeenCalled();
  });
});

Mocking Strategies

Mocking External APIs

Mock entire module:

jest.mock('axios');

const axios = require('axios');

describe('External API calls', () => {
  it('should fetch data from external API', async () => {
    const mockData = { data: 'test' };
    axios.get.mockResolvedValue({ data: mockData });

    const result = await fetchExternalData();

    expect(result).toEqual(mockData);
    expect(axios.get).toHaveBeenCalledWith('https://api.example.com/data');
  });
});

Mock specific functions:

const userService = require('../services/user');

jest.spyOn(userService, 'findById').mockResolvedValue({
  id: 1,
  name: 'Test User'
});

describe('User routes', () => {
  it('should get user by id', async () => {
    const response = await request(app)
      .get('/api/users/1')
      .expect(200);

    expect(response.body.name).toBe('Test User');
    expect(userService.findById).toHaveBeenCalledWith('1');
  });
});

Manual mocks:

// __mocks__/axios.js
module.exports = {
  get: jest.fn(() => Promise.resolve({ data: {} })),
  post: jest.fn(() => Promise.resolve({ data: {} })),
  put: jest.fn(() => Promise.resolve({ data: {} })),
  delete: jest.fn(() => Promise.resolve({ data: {} }))
};

Mocking Database

// Mock database module
jest.mock('../db');

const db = require('../db');

describe('Database operations', () => {
  beforeEach(() => {
    db.query.mockClear();
  });

  it('should query users', async () => {
    const mockUsers = [
      { id: 1, name: 'User 1' },
      { id: 2, name: 'User 2' }
    ];

    db.query.mockResolvedValue({ rows: mockUsers });

    const users = await User.findAll();

    expect(users).toEqual(mockUsers);
    expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
  });

  it('should handle database errors', async () => {
    db.query.mockRejectedValue(new Error('Connection failed'));

    await expect(User.findAll()).rejects.toThrow('Connection failed');
  });
});

Mocking Environment Variables

describe('Environment configuration', () => {
  const originalEnv = process.env;

  beforeEach(() => {
    jest.resetModules();
    process.env = { ...originalEnv };
  });

  afterAll(() => {
    process.env = originalEnv;
  });

  it('should use default port when PORT not set', () => {
    delete process.env.PORT;
    const config = require('../config');
    expect(config.port).toBe(3000);
  });

  it('should use PORT from environment', () => {
    process.env.PORT = '8080';
    const config = require('../config');
    expect(config.port).toBe(8080);
  });
});

Unit vs Integration vs E2E Testing

Unit Tests

What: Test individual functions/modules in isolation

Example:

// validators.js
function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

function isStrongPassword(password) {
  return password.length >= 12 &&
    /[A-Z]/.test(password) &&
    /[a-z]/.test(password) &&
    /[0-9]/.test(password) &&
    /[^A-Za-z0-9]/.test(password);
}

module.exports = { isValidEmail, isStrongPassword };

// validators.test.js
const { isValidEmail, isStrongPassword } = require('./validators');

describe('Email validation', () => {
  it('should accept valid email', () => {
    expect(isValidEmail('test@example.com')).toBe(true);
  });

  it('should reject email without @', () => {
    expect(isValidEmail('testexample.com')).toBe(false);
  });

  it('should reject email without domain', () => {
    expect(isValidEmail('test@')).toBe(false);
  });

  it('should reject email with spaces', () => {
    expect(isValidEmail('test @example.com')).toBe(false);
  });
});

describe('Password validation', () => {
  it('should accept strong password', () => {
    expect(isStrongPassword('MyP@ssw0rd123!')).toBe(true);
  });

  it('should reject short password', () => {
    expect(isStrongPassword('Short1!')).toBe(false);
  });

  it('should reject password without uppercase', () => {
    expect(isStrongPassword('myp@ssw0rd123!')).toBe(false);
  });

  it('should reject password without special char', () => {
    expect(isStrongPassword('MyPassword123')).toBe(false);
  });
});

Integration Tests

What: Test multiple components working together

Example:

const request = require('supertest');
const app = require('../server');
const db = require('../db');

describe('User registration flow', () => {
  beforeEach(async () => {
    // Clean database before each test
    await db.query('DELETE FROM users');
  });

  it('should register user and allow login', async () => {
    // Register user
    const registerResponse = await request(app)
      .post('/api/register')
      .send({
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      })
      .expect(201);

    expect(registerResponse.body).toHaveProperty('id');

    // Login with registered credentials
    const loginResponse = await request(app)
      .post('/api/login')
      .send({
        email: 'test@example.com',
        password: 'SecurePass123!'
      })
      .expect(200);

    expect(loginResponse.body).toHaveProperty('token');

    // Access protected route with token
    const profileResponse = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${loginResponse.body.token}`)
      .expect(200);

    expect(profileResponse.body.email).toBe('test@example.com');
  });
});

End-to-End (E2E) Tests

What: Test complete user workflows from UI to database

Setup with Puppeteer:

npm install --save-dev puppeteer

Example:

const puppeteer = require('puppeteer');

describe('E2E: User registration', () => {
  let browser;
  let page;

  beforeAll(async () => {
    browser = await puppeteer.launch({
      headless: true,
      args: ['--no-sandbox']
    });
    page = await browser.newPage();
  });

  afterAll(async () => {
    await browser.close();
  });

  it('should complete registration flow', async () => {
    // Navigate to registration page
    await page.goto('http://localhost:3000/register');

    // Fill out form
    await page.type('#email', 'test@example.com');
    await page.type('#password', 'SecurePass123!');
    await page.type('#confirmPassword', 'SecurePass123!');

    // Submit form
    await page.click('button[type="submit"]');

    // Wait for redirect to dashboard
    await page.waitForNavigation();

    // Verify we're on dashboard
    const url = page.url();
    expect(url).toContain('/dashboard');

    // Verify welcome message
    const welcomeMessage = await page.$eval(
      '.welcome',
      el => el.textContent
    );
    expect(welcomeMessage).toContain('test@example.com');
  });
});

Test Organization

Describe Blocks

describe('User API', () => {
  describe('GET /api/users', () => {
    it('should return all users', async () => {
      // Test implementation
    });

    it('should support pagination', async () => {
      // Test implementation
    });

    it('should support filtering', async () => {
      // Test implementation
    });
  });

  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      // Test implementation
    });

    it('should reject duplicate email', async () => {
      // Test implementation
    });
  });

  describe('PUT /api/users/:id', () => {
    it('should update user', async () => {
      // Test implementation
    });

    it('should reject unauthorized update', async () => {
      // Test implementation
    });
  });
});

Setup and Teardown

describe('Database tests', () => {
  // Runs once before all tests in this describe block
  beforeAll(async () => {
    await db.connect();
  });

  // Runs once after all tests in this describe block
  afterAll(async () => {
    await db.disconnect();
  });

  // Runs before each test in this describe block
  beforeEach(async () => {
    await db.query('DELETE FROM users');
    await db.query('INSERT INTO users (email) VALUES ($1)', ['test@example.com']);
  });

  // Runs after each test in this describe block
  afterEach(async () => {
    jest.clearAllMocks();
  });

  it('should find user', async () => {
    const user = await User.findByEmail('test@example.com');
    expect(user).toBeTruthy();
  });

  it('should delete user', async () => {
    await User.deleteByEmail('test@example.com');
    const user = await User.findByEmail('test@example.com');
    expect(user).toBeNull();
  });
});

Test Fixtures

// test/fixtures/users.js
module.exports = {
  validUser: {
    email: 'test@example.com',
    password: 'SecurePass123!',
    name: 'Test User'
  },

  adminUser: {
    email: 'admin@example.com',
    password: 'AdminPass123!',
    name: 'Admin User',
    role: 'admin'
  },

  invalidUsers: {
    noEmail: {
      password: 'SecurePass123!',
      name: 'Test User'
    },
    weakPassword: {
      email: 'test@example.com',
      password: '123',
      name: 'Test User'
    }
  }
};

// Usage in tests
const fixtures = require('./fixtures/users');

describe('User creation', () => {
  it('should create valid user', async () => {
    const response = await request(app)
      .post('/api/users')
      .send(fixtures.validUser)
      .expect(201);
  });

  it('should reject user without email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send(fixtures.invalidUsers.noEmail)
      .expect(400);
  });
});

Async Testing

Testing Promises

describe('Async operations', () => {
  it('should resolve with data', async () => {
    const data = await fetchData();
    expect(data).toBeDefined();
  });

  it('should reject with error', async () => {
    await expect(fetchInvalidData()).rejects.toThrow('Not found');
  });

  // Alternative: using done callback
  it('should fetch data (callback style)', (done) => {
    fetchData()
      .then(data => {
        expect(data).toBeDefined();
        done();
      })
      .catch(done);
  });
});

Testing Callbacks

describe('Callback functions', () => {
  it('should call callback with data', (done) => {
    fetchDataWithCallback((err, data) => {
      expect(err).toBeNull();
      expect(data).toBeDefined();
      done();
    });
  });

  it('should call callback with error', (done) => {
    fetchInvalidDataWithCallback((err, data) => {
      expect(err).toBeTruthy();
      expect(data).toBeUndefined();
      done();
    });
  });
});

Code Coverage

Generating Coverage Reports

# Run tests with coverage
npm run test:coverage

# Coverage report output
----------|---------|----------|---------|---------|
File      | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files |   85.5  |   78.3   |   91.2  |   85.1  |
 server.js|   92.3  |   85.7   |   100   |   91.8  |
 routes/  |   78.9  |   71.4   |   83.3  |   79.2  |
----------|---------|----------|---------|---------|

Coverage Configuration

// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/index.js', // Exclude entry point
    '!src/**/*.test.js', // Exclude test files
    '!src/**/__tests__/**' // Exclude test directories
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    },
    // Per-file thresholds
    './src/critical-module.js': {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100
    }
  },

  coverageReporters: [
    'text',      // Terminal output
    'html',      // HTML report in coverage/
    'lcov',      // For CI tools
    'json'       // Machine-readable
  ]
};

Viewing HTML Coverage Report

npm run test:coverage
open coverage/index.html  # macOS
xdg-open coverage/index.html  # Linux
start coverage/index.html  # Windows

Testing Best Practices

1. Naming Conventions

// GOOD - Descriptive test names
describe('User registration', () => {
  it('should create user with valid email and password', () => {});
  it('should reject registration with duplicate email', () => {});
  it('should hash password before storing', () => {});
});

// BAD - Vague test names
describe('User', () => {
  it('works', () => {});
  it('test 1', () => {});
  it('should not fail', () => {});
});

2. AAA Pattern (Arrange, Act, Assert)

it('should calculate total price with tax', () => {
  // Arrange: Set up test data
  const items = [
    { price: 10, quantity: 2 },
    { price: 5, quantity: 3 }
  ];
  const taxRate = 0.1;

  // Act: Perform the action
  const total = calculateTotal(items, taxRate);

  // Assert: Verify the result
  expect(total).toBe(38.5); // (10*2 + 5*3) * 1.1
});

3. Test One Thing

// GOOD - Each test checks one behavior
it('should validate email format', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
});

it('should reject email without domain', () => {
  expect(isValidEmail('test@')).toBe(false);
});

// BAD - Testing multiple things
it('should validate inputs', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
  expect(isValidPassword('pass123')).toBe(false);
  expect(isValidPhone('1234567890')).toBe(true);
});

4. Avoid Test Interdependence

// BAD - Tests depend on each other
let userId;

it('should create user', async () => {
  const response = await createUser();
  userId = response.id; // Other tests depend on this
});

it('should update user', async () => {
  await updateUser(userId); // Fails if previous test fails
});

// GOOD - Each test is independent
describe('User operations', () => {
  let userId;

  beforeEach(async () => {
    const user = await createUser();
    userId = user.id;
  });

  it('should update user', async () => {
    await updateUser(userId);
  });

  it('should delete user', async () => {
    await deleteUser(userId);
  });
});

5. Use Meaningful Assertions

// GOOD - Specific assertions
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('users');
expect(response.body.users).toHaveLength(5);
expect(response.body.users[0]).toMatchObject({
  id: expect.any(Number),
  email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)
});

// BAD - Vague assertions
expect(response).toBeTruthy();
expect(response.body).toBeDefined();

6. Test Edge Cases

describe('Division function', () => {
  it('should divide positive numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  it('should handle negative numbers', () => {
    expect(divide(-10, 2)).toBe(-5);
  });

  it('should handle zero numerator', () => {
    expect(divide(0, 5)).toBe(0);
  });

  it('should throw error for division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });

  it('should handle decimal results', () => {
    expect(divide(5, 2)).toBe(2.5);
  });
});

CI/CD Integration

GitHub Actions

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm test

    - name: Generate coverage report
      run: npm run test:coverage

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/coverage-final.json
        fail_ci_if_error: true

npm Scripts for CI

{
  "scripts": {
    "test": "jest",
    "test:ci": "jest --ci --coverage --maxWorkers=2",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch"
  }
}

Common Jest Matchers

Equality

expect(value).toBe(4); // Strict equality (===)
expect(value).toEqual({ a: 1 }); // Deep equality
expect(value).not.toBe(5); // Negation

Truthiness

expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

Numbers

expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
expect(value).toBeCloseTo(0.3); // Floating point

Strings

expect(string).toMatch(/pattern/);
expect(string).toMatch('substring');
expect(string).toContain('substring');

Arrays and Iterables

expect(array).toContain('item');
expect(array).toHaveLength(3);
expect(array).toEqual(expect.arrayContaining([1, 2]));

Objects

expect(object).toHaveProperty('key');
expect(object).toHaveProperty('key', value);
expect(object).toMatchObject({ a: 1, b: 2 });
expect(object).toEqual(expect.objectContaining({ a: 1 }));

Functions

expect(fn).toThrow();
expect(fn).toThrow('error message');
expect(fn).toThrow(Error);
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(3);

Testing Checklist

Unit Tests

  • Test pure functions in isolation
  • Test all code paths (happy path and error cases)
  • Test edge cases and boundary conditions
  • Mock external dependencies
  • Achieve high code coverage (>80%)

Integration Tests

  • Test API endpoints
  • Test authentication/authorization
  • Test database operations
  • Test external API integration
  • Test error handling

E2E Tests

  • Test critical user flows
  • Test form submissions
  • Test navigation
  • Test authentication flow

General

  • Tests are fast (< 5 seconds for unit tests)
  • Tests are independent (can run in any order)
  • Tests are repeatable (same result every time)
  • Tests have clear, descriptive names
  • Setup and teardown properly implemented
  • No hardcoded values (use constants/fixtures)
  • CI/CD integration configured

Example Test Suite for Express API

const request = require('supertest');
const app = require('../server');
const db = require('../db');

describe('Express API Tests', () => {
  // Setup: Connect to test database
  beforeAll(async () => {
    await db.connect(process.env.TEST_DATABASE_URL);
  });

  // Cleanup: Disconnect from database
  afterAll(async () => {
    await db.disconnect();
  });

  // Reset database before each test
  beforeEach(async () => {
    await db.query('DELETE FROM users');
  });

  describe('GET /api/health', () => {
    it('should return health status', async () => {
      const response = await request(app)
        .get('/api/health')
        .expect(200);

      expect(response.body).toEqual({
        status: 'ok',
        timestamp: expect.any(Number)
      });
    });
  });

  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      const userData = {
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      };

      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);

      expect(response.body).toMatchObject({
        id: expect.any(Number),
        email: userData.email,
        name: userData.name
      });
      expect(response.body).not.toHaveProperty('password');
    });

    it('should reject duplicate email', async () => {
      const userData = {
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      };

      // Create first user
      await request(app).post('/api/users').send(userData);

      // Try to create duplicate
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(409);

      expect(response.body.error).toMatch(/already exists/i);
    });

    it('should validate email format', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({
          email: 'invalid-email',
          password: 'SecurePass123!',
          name: 'Test'
        })
        .expect(400);

      expect(response.body.error).toMatch(/email/i);
    });

    it('should enforce password requirements', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({
          email: 'test@example.com',
          password: 'weak',
          name: 'Test'
        })
        .expect(400);

      expect(response.body.error).toMatch(/password/i);
    });
  });

  describe('Authentication', () => {
    let authToken;
    const testUser = {
      email: 'auth@example.com',
      password: 'SecurePass123!',
      name: 'Auth User'
    };

    beforeEach(async () => {
      // Create user
      await request(app).post('/api/users').send(testUser);

      // Login and get token
      const response = await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: testUser.password
        });

      authToken = response.body.token;
    });

    it('should login with valid credentials', async () => {
      const response = await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: testUser.password
        })
        .expect(200);

      expect(response.body).toHaveProperty('token');
    });

    it('should reject invalid credentials', async () => {
      await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: 'WrongPassword'
        })
        .expect(401);
    });

    it('should access protected route with token', async () => {
      const response = await request(app)
        .get('/api/profile')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body.email).toBe(testUser.email);
    });

    it('should reject access without token', async () => {
      await request(app)
        .get('/api/profile')
        .expect(401);
    });
  });
});

Resources