javascript

examples

examples.js
/**
 * 20.2 Integration & E2E Testing - Examples
 *
 * Demonstrates integration testing patterns
 * and end-to-end testing concepts
 */

// ============================================
// PART 1: Simple Integration Tests
// ============================================

/**
 * Example application layers to test together
 */

// Data Access Layer
class UserRepository {
  constructor(database) {
    this.db = database;
  }

  async findById(id) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
  }

  async findByEmail(email) {
    return this.db.query('SELECT * FROM users WHERE email = ?', [email]);
  }

  async create(userData) {
    const result = await this.db.query(
      'INSERT INTO users (name, email, password) VALUES (?, ?, ?)',
      [userData.name, userData.email, userData.password]
    );
    return { id: result.insertId, ...userData };
  }

  async update(id, userData) {
    await this.db.query('UPDATE users SET name = ?, email = ? WHERE id = ?', [
      userData.name,
      userData.email,
      id,
    ]);
    return this.findById(id);
  }

  async delete(id) {
    await this.db.query('DELETE FROM users WHERE id = ?', [id]);
    return true;
  }
}

// Service Layer
class UserService {
  constructor(userRepository, emailService, validator) {
    this.userRepo = userRepository;
    this.emailService = emailService;
    this.validator = validator;
  }

  async registerUser(userData) {
    // Validate
    const validation = this.validator.validate(userData);
    if (!validation.valid) {
      throw new Error(validation.errors.join(', '));
    }

    // Check if email exists
    const existing = await this.userRepo.findByEmail(userData.email);
    if (existing) {
      throw new Error('Email already registered');
    }

    // Hash password (simplified)
    const hashedPassword = this.hashPassword(userData.password);

    // Create user
    const user = await this.userRepo.create({
      ...userData,
      password: hashedPassword,
    });

    // Send welcome email
    await this.emailService.send(
      user.email,
      'Welcome!',
      `Hello ${user.name}, welcome to our platform!`
    );

    return { id: user.id, name: user.name, email: user.email };
  }

  hashPassword(password) {
    // Simplified - use bcrypt in real apps
    return Buffer.from(password).toString('base64');
  }

  async getUserProfile(userId) {
    const user = await this.userRepo.findById(userId);
    if (!user) {
      throw new Error('User not found');
    }
    return {
      id: user.id,
      name: user.name,
      email: user.email,
    };
  }
}

// ============================================
// PART 2: In-Memory Database for Integration Tests
// ============================================

class InMemoryDatabase {
  constructor() {
    this.tables = new Map();
    this.autoIncrement = new Map();
  }

  createTable(name, columns) {
    this.tables.set(name, []);
    this.autoIncrement.set(name, 1);
  }

  async query(sql, params = []) {
    // Simple SQL parser for testing
    const operation = sql.trim().split(' ')[0].toUpperCase();

    switch (operation) {
      case 'SELECT':
        return this.select(sql, params);
      case 'INSERT':
        return this.insert(sql, params);
      case 'UPDATE':
        return this.update(sql, params);
      case 'DELETE':
        return this.delete(sql, params);
      default:
        throw new Error(`Unsupported operation: ${operation}`);
    }
  }

  select(sql, params) {
    const tableMatch = sql.match(/FROM\s+(\w+)/i);
    const table = tableMatch ? tableMatch[1] : null;
    const data = this.tables.get(table) || [];

    const whereMatch = sql.match(/WHERE\s+(\w+)\s*=\s*\?/i);
    if (whereMatch) {
      const field = whereMatch[1];
      const value = params[0];
      return data.find((row) => row[field] == value) || null;
    }

    return data;
  }

  insert(sql, params) {
    const tableMatch = sql.match(/INTO\s+(\w+)/i);
    const table = tableMatch ? tableMatch[1] : null;
    const data = this.tables.get(table);

    const columnsMatch = sql.match(/\(([^)]+)\)\s+VALUES/i);
    const columns = columnsMatch
      ? columnsMatch[1].split(',').map((c) => c.trim())
      : [];

    const id = this.autoIncrement.get(table);
    this.autoIncrement.set(table, id + 1);

    const row = { id };
    columns.forEach((col, i) => {
      row[col] = params[i];
    });

    data.push(row);
    return { insertId: id };
  }

  update(sql, params) {
    const tableMatch = sql.match(/UPDATE\s+(\w+)/i);
    const table = tableMatch ? tableMatch[1] : null;
    const data = this.tables.get(table);

    const id = params[params.length - 1];
    const row = data.find((r) => r.id == id);

    if (row) {
      const setMatch = sql.match(/SET\s+(.+?)\s+WHERE/i);
      if (setMatch) {
        const assignments = setMatch[1].split(',');
        let paramIndex = 0;
        assignments.forEach((a) => {
          const col = a.split('=')[0].trim();
          row[col] = params[paramIndex++];
        });
      }
    }

    return { affectedRows: row ? 1 : 0 };
  }

  delete(sql, params) {
    const tableMatch = sql.match(/FROM\s+(\w+)/i);
    const table = tableMatch ? tableMatch[1] : null;
    const data = this.tables.get(table);

    const id = params[0];
    const index = data.findIndex((r) => r.id == id);

    if (index !== -1) {
      data.splice(index, 1);
    }

    return { affectedRows: index !== -1 ? 1 : 0 };
  }

  reset() {
    this.tables.clear();
    this.autoIncrement.clear();
  }
}

// ============================================
// PART 3: Integration Test Examples
// ============================================

/**
 * Integration Test Suite for UserService + UserRepository
 */
async function runUserIntegrationTests() {
  console.log('=== User Integration Tests ===\n');

  // Setup
  const db = new InMemoryDatabase();
  db.createTable('users', ['id', 'name', 'email', 'password']);

  const userRepo = new UserRepository(db);

  // Track sent emails
  const sentEmails = [];
  const emailService = {
    send: async (to, subject, body) => {
      sentEmails.push({ to, subject, body });
      return true;
    },
  };

  const validator = {
    validate: (data) => {
      const errors = [];
      if (!data.name || data.name.length < 2) {
        errors.push('Name must be at least 2 characters');
      }
      if (!data.email || !data.email.includes('@')) {
        errors.push('Invalid email');
      }
      if (!data.password || data.password.length < 6) {
        errors.push('Password must be at least 6 characters');
      }
      return { valid: errors.length === 0, errors };
    },
  };

  const userService = new UserService(userRepo, emailService, validator);

  // Test 1: Full registration flow
  console.log('Test 1: Full registration flow');
  try {
    const user = await userService.registerUser({
      name: 'John Doe',
      email: 'john@example.com',
      password: 'secret123',
    });

    console.assert(user.id === 1, 'Should create user with ID');
    console.assert(user.name === 'John Doe', 'Should have correct name');
    console.assert(!user.password, 'Should not return password');

    // Verify email was sent
    console.assert(sentEmails.length === 1, 'Should send welcome email');
    console.assert(
      sentEmails[0].to === 'john@example.com',
      'Should send to correct email'
    );

    // Verify user in database
    const dbUser = await userRepo.findById(1);
    console.assert(dbUser !== null, 'User should be in database');
    console.assert(
      dbUser.password !== 'secret123',
      'Password should be hashed'
    );

    console.log('  ✓ User registered successfully');
    console.log('  ✓ Welcome email sent');
    console.log('  ✓ Password hashed in database');
  } catch (e) {
    console.log('  ✗ Error:', e.message);
  }

  // Test 2: Prevent duplicate email registration
  console.log('\nTest 2: Prevent duplicate email registration');
  try {
    await userService.registerUser({
      name: 'Jane Doe',
      email: 'john@example.com', // Same email
      password: 'secret456',
    });
    console.log('  ✗ Should have thrown error');
  } catch (e) {
    console.assert(
      e.message.includes('already registered'),
      'Should indicate email exists'
    );
    console.log('  ✓ Correctly rejected duplicate email');
  }

  // Test 3: Validation integration
  console.log('\nTest 3: Validation integration');
  try {
    await userService.registerUser({
      name: 'J',
      email: 'invalid',
      password: '123',
    });
    console.log('  ✗ Should have thrown validation error');
  } catch (e) {
    console.assert(e.message.includes('Name'), 'Should include name error');
    console.log('  ✓ Validation errors returned correctly');
  }

  // Test 4: Get user profile
  console.log('\nTest 4: Get user profile');
  try {
    const profile = await userService.getUserProfile(1);
    console.assert(profile.name === 'John Doe', 'Should return name');
    console.assert(profile.email === 'john@example.com', 'Should return email');
    console.assert(!profile.password, 'Should not return password');
    console.log('  ✓ Profile retrieved correctly');
  } catch (e) {
    console.log('  ✗ Error:', e.message);
  }

  // Test 5: User not found
  console.log('\nTest 5: User not found');
  try {
    await userService.getUserProfile(999);
    console.log('  ✗ Should have thrown error');
  } catch (e) {
    console.assert(e.message === 'User not found', 'Correct error message');
    console.log('  ✓ User not found handled correctly');
  }

  console.log('\n=== Integration Tests Complete ===\n');
}

// ============================================
// PART 4: API Integration Testing Pattern
// ============================================

/**
 * Mock HTTP Server for API testing
 */
class MockHTTPServer {
  constructor() {
    this.routes = new Map();
    this.middleware = [];
    this.requests = [];
  }

  use(fn) {
    this.middleware.push(fn);
  }

  get(path, handler) {
    this.routes.set(`GET:${path}`, handler);
  }

  post(path, handler) {
    this.routes.set(`POST:${path}`, handler);
  }

  put(path, handler) {
    this.routes.set(`PUT:${path}`, handler);
  }

  delete(path, handler) {
    this.routes.set(`DELETE:${path}`, handler);
  }

  async request(method, path, options = {}) {
    const key = `${method}:${path}`;
    const handler = this.routes.get(key);

    // Create request/response objects
    const req = {
      method,
      path,
      body: options.body || {},
      headers: options.headers || {},
      params: {},
      query: {},
    };

    const res = {
      statusCode: 200,
      body: null,
      headers: {},

      status(code) {
        this.statusCode = code;
        return this;
      },

      json(data) {
        this.body = data;
        this.headers['Content-Type'] = 'application/json';
        return this;
      },

      send(data) {
        this.body = data;
        return this;
      },
    };

    // Track request
    this.requests.push({ method, path, options, timestamp: Date.now() });

    // Run middleware
    for (const mw of this.middleware) {
      await mw(req, res, () => {});
    }

    // Run handler
    if (handler) {
      await handler(req, res);
    } else {
      res.status(404).json({ error: 'Not found' });
    }

    return {
      status: res.statusCode,
      body: res.body,
      headers: res.headers,
    };
  }

  getRequests() {
    return this.requests;
  }

  clearRequests() {
    this.requests = [];
  }
}

/**
 * API Integration Test Example
 */
function setupUserAPI(app, userService) {
  // GET /api/users/:id
  app.get('/api/users/:id', async (req, res) => {
    try {
      const user = await userService.getUserProfile(req.params.id);
      res.json(user);
    } catch (e) {
      if (e.message === 'User not found') {
        res.status(404).json({ error: e.message });
      } else {
        res.status(500).json({ error: 'Internal server error' });
      }
    }
  });

  // POST /api/users
  app.post('/api/users', async (req, res) => {
    try {
      const user = await userService.registerUser(req.body);
      res.status(201).json(user);
    } catch (e) {
      res.status(400).json({ error: e.message });
    }
  });
}

async function runAPIIntegrationTests() {
  console.log('=== API Integration Tests ===\n');

  // Setup complete system
  const db = new InMemoryDatabase();
  db.createTable('users', ['id', 'name', 'email', 'password']);

  const userRepo = new UserRepository(db);
  const emailService = { send: async () => true };
  const validator = {
    validate: () => ({ valid: true, errors: [] }),
  };
  const userService = new UserService(userRepo, emailService, validator);

  const app = new MockHTTPServer();

  // Simple path parameter extraction middleware
  app.use((req, res, next) => {
    const pathParts = req.path.split('/');
    if (pathParts[pathParts.length - 1].match(/^\d+$/)) {
      req.params.id = parseInt(pathParts[pathParts.length - 1]);
    }
    next();
  });

  // Setup API routes
  // Note: Using inline handlers since our mock doesn't support path params natively
  app.post('/api/users', async (req, res) => {
    try {
      const user = await userService.registerUser(req.body);
      res.status(201).json(user);
    } catch (e) {
      res.status(400).json({ error: e.message });
    }
  });

  app.get('/api/users/1', async (req, res) => {
    try {
      const user = await userService.getUserProfile(1);
      res.json(user);
    } catch (e) {
      if (e.message === 'User not found') {
        res.status(404).json({ error: e.message });
      } else {
        res.status(500).json({ error: 'Internal server error' });
      }
    }
  });

  app.get('/api/users/999', async (req, res) => {
    try {
      const user = await userService.getUserProfile(999);
      res.json(user);
    } catch (e) {
      if (e.message === 'User not found') {
        res.status(404).json({ error: e.message });
      } else {
        res.status(500).json({ error: 'Internal server error' });
      }
    }
  });

  // Test 1: Create user via API
  console.log('Test 1: POST /api/users - Create user');
  const createResponse = await app.request('POST', '/api/users', {
    body: {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'secret123',
    },
  });

  console.assert(createResponse.status === 201, 'Should return 201');
  console.assert(createResponse.body.id === 1, 'Should return user ID');
  console.assert(createResponse.body.name === 'John Doe', 'Should return name');
  console.log('  ✓ User created successfully');
  console.log(`    Status: ${createResponse.status}`);
  console.log(`    Body: ${JSON.stringify(createResponse.body)}`);

  // Test 2: Get user via API
  console.log('\nTest 2: GET /api/users/1 - Get user');
  const getResponse = await app.request('GET', '/api/users/1');

  console.assert(getResponse.status === 200, 'Should return 200');
  console.assert(
    getResponse.body.email === 'john@example.com',
    'Should return email'
  );
  console.log('  ✓ User retrieved successfully');
  console.log(`    Status: ${getResponse.status}`);
  console.log(`    Body: ${JSON.stringify(getResponse.body)}`);

  // Test 3: Get non-existent user
  console.log('\nTest 3: GET /api/users/999 - User not found');
  const notFoundResponse = await app.request('GET', '/api/users/999');

  console.assert(notFoundResponse.status === 404, 'Should return 404');
  console.assert(
    notFoundResponse.body.error === 'User not found',
    'Should return error'
  );
  console.log('  ✓ 404 returned correctly');
  console.log(`    Status: ${notFoundResponse.status}`);
  console.log(`    Body: ${JSON.stringify(notFoundResponse.body)}`);

  console.log('\n=== API Tests Complete ===\n');
}

// ============================================
// PART 5: E2E Testing Simulation
// ============================================

/**
 * Simulated Browser for E2E Testing
 * (Demonstrates concepts - use Playwright/Puppeteer in real tests)
 */
class SimulatedBrowser {
  constructor() {
    this.currentURL = '';
    this.dom = new Map();
    this.cookies = new Map();
    this.localStorage = new Map();
    this.events = [];
  }

  async goto(url) {
    this.currentURL = url;
    this.events.push({ type: 'navigate', url });
    // Simulate page load
    await this.delay(100);
    return this;
  }

  setDOM(elements) {
    this.dom = new Map(Object.entries(elements));
  }

  async $(selector) {
    await this.delay(10);
    return this.dom.get(selector) || null;
  }

  async $$(selector) {
    await this.delay(10);
    const results = [];
    for (const [key, value] of this.dom) {
      if (key.includes(selector.replace('.', '').replace('#', ''))) {
        results.push(value);
      }
    }
    return results;
  }

  async click(selector) {
    const element = await this.$(selector);
    if (!element) throw new Error(`Element not found: ${selector}`);

    this.events.push({ type: 'click', selector });

    // Trigger click handler if exists
    if (element.onClick) {
      await element.onClick();
    }

    return this;
  }

  async type(selector, text) {
    const element = await this.$(selector);
    if (!element) throw new Error(`Element not found: ${selector}`);

    element.value = text;
    this.events.push({ type: 'type', selector, text });

    return this;
  }

  async waitForSelector(selector, options = {}) {
    const timeout = options.timeout || 5000;
    const start = Date.now();

    while (Date.now() - start < timeout) {
      const element = await this.$(selector);
      if (element) return element;
      await this.delay(100);
    }

    throw new Error(`Timeout waiting for ${selector}`);
  }

  async waitForNavigation() {
    await this.delay(100);
    return this;
  }

  async screenshot(options = {}) {
    this.events.push({ type: 'screenshot', options });
    return Buffer.from('fake-image-data');
  }

  async evaluate(fn) {
    return fn();
  }

  delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  getEvents() {
    return this.events;
  }
}

/**
 * Page Object Pattern Example
 */
class LoginPage {
  constructor(browser) {
    this.browser = browser;
    this.selectors = {
      emailInput: '#email',
      passwordInput: '#password',
      submitButton: '#login-button',
      errorMessage: '.error-message',
      successMessage: '.success-message',
    };
  }

  async navigate() {
    await this.browser.goto('https://example.com/login');
    return this;
  }

  async login(email, password) {
    await this.browser.type(this.selectors.emailInput, email);
    await this.browser.type(this.selectors.passwordInput, password);
    await this.browser.click(this.selectors.submitButton);
    await this.browser.waitForNavigation();
    return this;
  }

  async getErrorMessage() {
    const element = await this.browser.$(this.selectors.errorMessage);
    return element ? element.textContent : null;
  }

  async isLoggedIn() {
    try {
      await this.browser.waitForSelector(this.selectors.successMessage, {
        timeout: 1000,
      });
      return true;
    } catch {
      return false;
    }
  }
}

class DashboardPage {
  constructor(browser) {
    this.browser = browser;
    this.selectors = {
      welcomeMessage: '.welcome',
      userMenu: '#user-menu',
      logoutButton: '#logout',
    };
  }

  async getWelcomeMessage() {
    const element = await this.browser.$(this.selectors.welcomeMessage);
    return element ? element.textContent : null;
  }

  async logout() {
    await this.browser.click(this.selectors.userMenu);
    await this.browser.click(this.selectors.logoutButton);
    await this.browser.waitForNavigation();
    return new LoginPage(this.browser);
  }
}

/**
 * E2E Test Example
 */
async function runE2ETests() {
  console.log('=== E2E Test Simulation ===\n');

  const browser = new SimulatedBrowser();

  // Setup simulated DOM for login page
  browser.setDOM({
    '#email': { value: '', tagName: 'INPUT' },
    '#password': { value: '', tagName: 'INPUT' },
    '#login-button': {
      tagName: 'BUTTON',
      onClick: async () => {
        // Simulate successful login
        browser.setDOM({
          '.success-message': { textContent: 'Login successful' },
          '.welcome': { textContent: 'Welcome, John!' },
          '#user-menu': { tagName: 'BUTTON' },
          '#logout': { tagName: 'BUTTON' },
        });
      },
    },
  });

  // Test using Page Objects
  console.log('Test: Login Flow');

  const loginPage = new LoginPage(browser);
  await loginPage.navigate();
  console.log('  ✓ Navigated to login page');

  await loginPage.login('john@example.com', 'secret123');
  console.log('  ✓ Filled login form and submitted');

  const isLoggedIn = await loginPage.isLoggedIn();
  console.assert(isLoggedIn, 'Should be logged in');
  console.log('  ✓ Login successful');

  const dashboardPage = new DashboardPage(browser);
  const welcome = await dashboardPage.getWelcomeMessage();
  console.assert(welcome === 'Welcome, John!', 'Should show welcome message');
  console.log(`  ✓ Dashboard shows: "${welcome}"`);

  // Log all browser events
  console.log('\nBrowser Events:');
  browser.getEvents().forEach((event) => {
    console.log(`  - ${event.type}: ${event.selector || event.url || ''}`);
  });

  console.log('\n=== E2E Tests Complete ===\n');
}

// ============================================
// PART 6: Test Fixture Pattern
// ============================================

/**
 * Test Data Fixtures
 */
const fixtures = {
  users: {
    validUser: {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'validPassword123',
    },
    adminUser: {
      name: 'Admin',
      email: 'admin@example.com',
      password: 'adminPassword123',
      role: 'admin',
    },
    invalidUser: {
      name: 'J',
      email: 'invalid',
      password: '123',
    },
  },

  products: {
    widget: {
      id: 1,
      name: 'Widget',
      price: 9.99,
      stock: 100,
    },
    gadget: {
      id: 2,
      name: 'Gadget',
      price: 19.99,
      stock: 50,
    },
    outOfStock: {
      id: 3,
      name: 'Rare Item',
      price: 99.99,
      stock: 0,
    },
  },

  orders: {
    pending: {
      id: 1,
      userId: 1,
      status: 'pending',
      items: [{ productId: 1, quantity: 2 }],
      total: 19.98,
    },
    completed: {
      id: 2,
      userId: 1,
      status: 'completed',
      items: [{ productId: 2, quantity: 1 }],
      total: 19.99,
    },
  },
};

/**
 * Test Data Factory
 */
class TestFactory {
  static createUser(overrides = {}) {
    return {
      id: TestFactory.generateId(),
      name: `Test User ${Date.now()}`,
      email: `test${Date.now()}@example.com`,
      password: 'defaultPassword',
      createdAt: new Date().toISOString(),
      ...overrides,
    };
  }

  static createProduct(overrides = {}) {
    return {
      id: TestFactory.generateId(),
      name: `Product ${Date.now()}`,
      price: Math.random() * 100,
      stock: Math.floor(Math.random() * 1000),
      ...overrides,
    };
  }

  static createOrder(userId, items = [], overrides = {}) {
    return {
      id: TestFactory.generateId(),
      userId,
      status: 'pending',
      items,
      total: items.reduce((sum, item) => sum + item.price * item.quantity, 0),
      createdAt: new Date().toISOString(),
      ...overrides,
    };
  }

  static generateId() {
    return Math.floor(Math.random() * 1000000);
  }
}

function demonstrateFixtures() {
  console.log('=== Test Fixtures Demo ===\n');

  // Using fixtures
  console.log('Static Fixture:');
  console.log(JSON.stringify(fixtures.users.validUser, null, 2));

  // Using factory
  console.log('\nFactory-generated User:');
  const user = TestFactory.createUser({ role: 'admin' });
  console.log(JSON.stringify(user, null, 2));

  console.log('\nFactory-generated Product:');
  const product = TestFactory.createProduct({ name: 'Custom Widget' });
  console.log(JSON.stringify(product, null, 2));

  console.log('\n=== Fixtures Demo Complete ===\n');
}

// ============================================
// RUN ALL TESTS
// ============================================

async function runAllTests() {
  console.log('╔════════════════════════════════════════════╗');
  console.log('║   Integration & E2E Testing Examples       ║');
  console.log('╚════════════════════════════════════════════╝\n');

  await runUserIntegrationTests();
  await runAPIIntegrationTests();
  await runE2ETests();
  demonstrateFixtures();

  console.log('All examples completed!');
}

// Run
runAllTests();

// Export for use
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    UserRepository,
    UserService,
    InMemoryDatabase,
    MockHTTPServer,
    SimulatedBrowser,
    LoginPage,
    DashboardPage,
    TestFactory,
    fixtures,
  };
}
Examples - JavaScript Tutorial | DeepML