javascript

examples

examples.js
/**
 * 20.3 Mocking & Test Doubles - Examples
 *
 * Comprehensive examples of all test double types
 */

// ============================================
// PART 1: Dummy Objects
// ============================================

/**
 * Dummy: An object that is passed but never used
 */

class Logger {
  log(message) {
    console.log(`[LOG] ${message}`);
  }
  error(message) {
    console.error(`[ERROR] ${message}`);
  }
}

class UserService {
  constructor(repository, logger) {
    this.repository = repository;
    this.logger = logger;
  }

  getUser(id) {
    // Logger not used in this simple case
    return this.repository.findById(id);
  }
}

function demonstrateDummy() {
  console.log('=== Dummy Object Example ===\n');

  // Dummy logger - won't be used but is required
  const dummyLogger = null;

  // Or a more explicit dummy
  const explicitDummy = {
    log() {
      throw new Error('Dummy should not be used');
    },
    error() {
      throw new Error('Dummy should not be used');
    },
  };

  const mockRepo = {
    findById: (id) => ({ id, name: 'John' }),
  };

  // Using null as dummy (if logger isn't used)
  const service = new UserService(mockRepo, dummyLogger);
  const user = service.getUser(1);

  console.log('User retrieved:', user);
  console.log('Logger was NOT used (it was just a dummy)');
  console.log('');
}

// ============================================
// PART 2: Stubs
// ============================================

/**
 * Stub: Returns canned answers to calls
 */

class PaymentGateway {
  async charge(card, amount) {
    // Real implementation would call external API
    throw new Error('Not implemented');
  }

  async refund(transactionId, amount) {
    throw new Error('Not implemented');
  }
}

class OrderProcessor {
  constructor(paymentGateway) {
    this.payment = paymentGateway;
  }

  async processOrder(order, card) {
    const result = await this.payment.charge(card, order.total);

    if (result.success) {
      return {
        orderId: order.id,
        transactionId: result.transactionId,
        status: 'completed',
      };
    }

    throw new Error(`Payment failed: ${result.error}`);
  }
}

function demonstrateStub() {
  console.log('=== Stub Example ===\n');

  // Stub that returns successful payment
  const successStub = {
    charge: async (card, amount) => ({
      success: true,
      transactionId: 'txn_123456',
      amount,
    }),
    refund: async () => ({ success: true }),
  };

  // Stub that returns failed payment
  const failureStub = {
    charge: async () => ({
      success: false,
      error: 'Insufficient funds',
    }),
  };

  // Conditional stub
  const conditionalStub = {
    charge: async (card, amount) => {
      if (amount > 1000) {
        return { success: false, error: 'Amount too large' };
      }
      if (card.number === '0000') {
        return { success: false, error: 'Invalid card' };
      }
      return { success: true, transactionId: 'txn_' + Date.now() };
    },
  };

  // Test with success stub
  const processor = new OrderProcessor(successStub);
  processor
    .processOrder({ id: 1, total: 99.99 }, { number: '4242', exp: '12/25' })
    .then((result) => {
      console.log('Success stub result:', result);
    });

  // Test with conditional stub
  const processor2 = new OrderProcessor(conditionalStub);
  processor2
    .processOrder({ id: 2, total: 50 }, { number: '4242', exp: '12/25' })
    .then((result) => {
      console.log('Conditional stub (valid):', result);
    });

  console.log('');
}

// ============================================
// PART 3: Spies
// ============================================

/**
 * Spy: Records how it was called
 */

function createSpy(implementation = () => undefined) {
  const calls = [];

  const spy = function (...args) {
    const call = {
      args,
      thisArg: this,
      timestamp: Date.now(),
    };
    calls.push(call);

    try {
      const result = implementation.apply(this, args);
      call.returned = result;
      return result;
    } catch (e) {
      call.threw = e;
      throw e;
    }
  };

  // Spy inspection methods
  spy.calls = calls;
  spy.callCount = () => calls.length;
  spy.called = () => calls.length > 0;
  spy.calledWith = (...args) =>
    calls.some((call) => JSON.stringify(call.args) === JSON.stringify(args));
  spy.calledOnce = () => calls.length === 1;
  spy.firstCall = () => calls[0];
  spy.lastCall = () => calls[calls.length - 1];
  spy.getCall = (n) => calls[n];
  spy.reset = () => {
    calls.length = 0;
  };

  return spy;
}

function demonstrateSpy() {
  console.log('=== Spy Example ===\n');

  // Simple spy
  const spy = createSpy();

  spy('arg1', 'arg2');
  spy('arg3');
  spy();

  console.log('Call count:', spy.callCount());
  console.log('Was called:', spy.called());
  console.log('Called once:', spy.calledOnce());
  console.log('Called with (arg1, arg2):', spy.calledWith('arg1', 'arg2'));
  console.log('First call args:', spy.firstCall().args);
  console.log('Last call args:', spy.lastCall().args);

  // Spy with implementation
  console.log('\nSpy with implementation:');
  const addSpy = createSpy((a, b) => a + b);

  const result1 = addSpy(1, 2);
  const result2 = addSpy(3, 4);

  console.log('Results:', result1, result2);
  console.log(
    'All calls:',
    addSpy.calls.map((c) => ({
      args: c.args,
      returned: c.returned,
    }))
  );

  // Practical example: Email service spy
  console.log('\nPractical email spy example:');

  const emailSpy = createSpy((to, subject, body) => {
    return { sent: true, id: 'msg_' + Date.now() };
  });

  class NotificationService {
    constructor(emailer) {
      this.emailer = emailer;
    }

    notifyUser(user, message) {
      return this.emailer(user.email, 'Notification', message);
    }

    notifyAll(users, message) {
      return users.map((user) => this.notifyUser(user, message));
    }
  }

  const notifier = new NotificationService(emailSpy);

  notifier.notifyUser({ email: 'john@example.com' }, 'Hello John!');
  notifier.notifyAll(
    [{ email: 'jane@example.com' }, { email: 'bob@example.com' }],
    'Hello everyone!'
  );

  console.log('Total emails sent:', emailSpy.callCount());
  console.log(
    'Recipients:',
    emailSpy.calls.map((c) => c.args[0])
  );

  console.log('');
}

// ============================================
// PART 4: Mocks with Expectations
// ============================================

/**
 * Mock: Has expectations that can be verified
 */

function createMock() {
  const expectations = [];
  const calls = [];

  const mock = function (...args) {
    const call = { args, timestamp: Date.now() };
    calls.push(call);

    // Find matching expectation
    const exp = expectations.find((e) => {
      if (e.expectedArgs === null) return true;
      return JSON.stringify(e.expectedArgs) === JSON.stringify(args);
    });

    if (exp) {
      exp.actualCalls++;
      if (exp.throws) throw exp.throws;
      return exp.returns;
    }

    return undefined;
  };

  // Setup methods
  mock.expects = function (expectedArgs = null) {
    const expectation = {
      expectedArgs,
      returns: undefined,
      throws: null,
      expectedCalls: 1,
      actualCalls: 0,
    };
    expectations.push(expectation);

    return {
      returns(value) {
        expectation.returns = value;
        return this;
      },
      throws(error) {
        expectation.throws = error;
        return this;
      },
      times(n) {
        expectation.expectedCalls = n;
        return this;
      },
      never() {
        expectation.expectedCalls = 0;
        return this;
      },
    };
  };

  // Verification
  mock.verify = function () {
    const failures = [];

    for (const exp of expectations) {
      if (exp.actualCalls !== exp.expectedCalls) {
        const argsStr = exp.expectedArgs
          ? JSON.stringify(exp.expectedArgs)
          : 'any args';
        failures.push(
          `Expected ${exp.expectedCalls} calls with ${argsStr}, ` +
            `got ${exp.actualCalls}`
        );
      }
    }

    if (failures.length > 0) {
      throw new Error('Mock verification failed:\n' + failures.join('\n'));
    }

    return true;
  };

  mock.reset = function () {
    expectations.length = 0;
    calls.length = 0;
  };

  return mock;
}

function demonstrateMock() {
  console.log('=== Mock Example ===\n');

  // Create mock payment processor
  const paymentMock = createMock();

  // Set expectations
  paymentMock
    .expects(['card123', 100])
    .returns({ success: true, transactionId: 'txn_1' });

  paymentMock
    .expects(['card456', 200])
    .returns({ success: true, transactionId: 'txn_2' });

  // Use the mock
  const result1 = paymentMock('card123', 100);
  const result2 = paymentMock('card456', 200);

  console.log('Result 1:', result1);
  console.log('Result 2:', result2);

  // Verify expectations were met
  try {
    paymentMock.verify();
    console.log('✓ All expectations met');
  } catch (e) {
    console.log('✗ Verification failed:', e.message);
  }

  // Example with unmet expectation
  console.log('\nUnmet expectation example:');

  const unmetMock = createMock();
  unmetMock.expects(['never-called']).returns('value');

  try {
    unmetMock.verify();
  } catch (e) {
    console.log('✗ Verification failed:', e.message);
  }

  // Example with throws
  console.log('\nMock that throws:');

  const throwingMock = createMock();
  throwingMock.expects().throws(new Error('Simulated failure'));

  try {
    throwingMock();
  } catch (e) {
    console.log('✓ Mock threw as expected:', e.message);
  }

  console.log('');
}

// ============================================
// PART 5: Fakes
// ============================================

/**
 * Fake: Working but simplified implementation
 */

// Fake Database
class FakeDatabase {
  constructor() {
    this.tables = new Map();
  }

  createTable(name) {
    this.tables.set(name, {
      records: [],
      autoId: 1,
    });
  }

  insert(table, record) {
    const t = this.tables.get(table);
    if (!t) throw new Error(`Table ${table} not found`);

    const id = t.autoId++;
    const newRecord = { id, ...record };
    t.records.push(newRecord);
    return newRecord;
  }

  findById(table, id) {
    const t = this.tables.get(table);
    if (!t) return null;
    return t.records.find((r) => r.id === id) || null;
  }

  findAll(table, predicate = () => true) {
    const t = this.tables.get(table);
    if (!t) return [];
    return t.records.filter(predicate);
  }

  update(table, id, data) {
    const t = this.tables.get(table);
    if (!t) return null;

    const index = t.records.findIndex((r) => r.id === id);
    if (index === -1) return null;

    t.records[index] = { ...t.records[index], ...data };
    return t.records[index];
  }

  delete(table, id) {
    const t = this.tables.get(table);
    if (!t) return false;

    const index = t.records.findIndex((r) => r.id === id);
    if (index === -1) return false;

    t.records.splice(index, 1);
    return true;
  }

  clear(table) {
    const t = this.tables.get(table);
    if (t) {
      t.records = [];
      t.autoId = 1;
    }
  }

  clearAll() {
    for (const [name, t] of this.tables) {
      t.records = [];
      t.autoId = 1;
    }
  }
}

// Fake HTTP Client
class FakeHttpClient {
  constructor() {
    this.routes = new Map();
    this.requestLog = [];
  }

  register(method, url, response) {
    this.routes.set(`${method}:${url}`, response);
  }

  async request(method, url, options = {}) {
    const key = `${method}:${url}`;

    // Log the request
    this.requestLog.push({
      method,
      url,
      options,
      timestamp: Date.now(),
    });

    // Find registered response
    const response = this.routes.get(key);

    if (!response) {
      return {
        status: 404,
        data: { error: 'Not found' },
      };
    }

    // Support function responses for dynamic behavior
    if (typeof response === 'function') {
      return response(options);
    }

    return response;
  }

  async get(url, options) {
    return this.request('GET', url, options);
  }

  async post(url, data, options) {
    return this.request('POST', url, { ...options, data });
  }

  async put(url, data, options) {
    return this.request('PUT', url, { ...options, data });
  }

  async delete(url, options) {
    return this.request('DELETE', url, options);
  }

  getRequests() {
    return this.requestLog;
  }

  reset() {
    this.requestLog = [];
  }
}

// Fake Timer
class FakeTimer {
  constructor() {
    this.currentTime = Date.now();
    this.timers = [];
    this.nextId = 1;
  }

  now() {
    return this.currentTime;
  }

  setTimeout(fn, delay) {
    const id = this.nextId++;
    this.timers.push({
      id,
      fn,
      triggerAt: this.currentTime + delay,
      type: 'timeout',
    });
    return id;
  }

  setInterval(fn, interval) {
    const id = this.nextId++;
    this.timers.push({
      id,
      fn,
      triggerAt: this.currentTime + interval,
      interval,
      type: 'interval',
    });
    return id;
  }

  clearTimeout(id) {
    const index = this.timers.findIndex((t) => t.id === id);
    if (index !== -1) {
      this.timers.splice(index, 1);
    }
  }

  clearInterval(id) {
    this.clearTimeout(id);
  }

  tick(ms) {
    this.currentTime += ms;

    const toTrigger = this.timers.filter(
      (t) => t.triggerAt <= this.currentTime
    );

    for (const timer of toTrigger) {
      timer.fn();

      if (timer.type === 'interval') {
        timer.triggerAt = this.currentTime + timer.interval;
      } else {
        this.clearTimeout(timer.id);
      }
    }
  }

  runAllTimers() {
    while (this.timers.length > 0) {
      const next = this.timers.reduce((min, t) =>
        t.triggerAt < min.triggerAt ? t : min
      );
      this.tick(next.triggerAt - this.currentTime);
    }
  }
}

function demonstrateFake() {
  console.log('=== Fake Examples ===\n');

  // Fake Database
  console.log('Fake Database:');
  const db = new FakeDatabase();
  db.createTable('users');

  const user1 = db.insert('users', { name: 'John', email: 'john@example.com' });
  const user2 = db.insert('users', { name: 'Jane', email: 'jane@example.com' });

  console.log('Inserted:', user1);
  console.log('Find by ID:', db.findById('users', 1));
  console.log('Find all:', db.findAll('users'));

  db.update('users', 1, { name: 'John Updated' });
  console.log('After update:', db.findById('users', 1));

  // Fake HTTP Client
  console.log('\nFake HTTP Client:');
  const http = new FakeHttpClient();

  http.register('GET', '/api/users', {
    status: 200,
    data: [{ id: 1, name: 'John' }],
  });

  http.register('POST', '/api/users', (options) => ({
    status: 201,
    data: { id: 2, ...options.data },
  }));

  http.get('/api/users').then((res) => {
    console.log('GET /api/users:', res);
  });

  http.post('/api/users', { name: 'Jane' }).then((res) => {
    console.log('POST /api/users:', res);
  });

  // Fake Timer
  console.log('\nFake Timer:');
  const timer = new FakeTimer();

  let calls = 0;
  timer.setTimeout(() => {
    calls++;
    console.log('Timeout called');
  }, 1000);

  timer.setInterval(() => {
    calls++;
    console.log('Interval called');
  }, 500);

  console.log('Before tick:', calls);
  timer.tick(600); // Triggers interval once
  console.log('After 600ms:', calls);
  timer.tick(500); // Triggers interval again and timeout
  console.log('After 1100ms:', calls);

  console.log('');
}

// ============================================
// PART 6: Advanced Mock Patterns
// ============================================

/**
 * Advanced patterns for complex mocking scenarios
 */

// Spy on object methods
function spyOn(obj, methodName) {
  const original = obj[methodName];
  const calls = [];

  obj[methodName] = function (...args) {
    calls.push({ args, thisArg: this });
    return original.apply(this, args);
  };

  obj[methodName].calls = calls;
  obj[methodName].restore = () => {
    obj[methodName] = original;
  };

  return obj[methodName];
}

// Mock builder for complex objects
class MockBuilder {
  constructor() {
    this.methods = new Map();
  }

  method(name) {
    const methodConfig = {
      returns: undefined,
      throws: null,
      implementation: null,
      calls: [],
    };

    this.methods.set(name, methodConfig);

    return {
      returns: (value) => {
        methodConfig.returns = value;
        return this;
      },
      throws: (error) => {
        methodConfig.throws = error;
        return this;
      },
      callsFake: (fn) => {
        methodConfig.implementation = fn;
        return this;
      },
    };
  }

  build() {
    const mock = {};

    for (const [name, config] of this.methods) {
      mock[name] = (...args) => {
        config.calls.push({ args });

        if (config.throws) {
          throw config.throws;
        }

        if (config.implementation) {
          return config.implementation(...args);
        }

        return config.returns;
      };

      mock[name].calls = config.calls;
    }

    return mock;
  }
}

// Partial mock (mock some methods, keep others real)
function partialMock(RealClass, methodMocks) {
  const instance = new RealClass();

  for (const [method, mockFn] of Object.entries(methodMocks)) {
    instance[method] = mockFn;
  }

  return instance;
}

function demonstrateAdvancedPatterns() {
  console.log('=== Advanced Mock Patterns ===\n');

  // spyOn example
  console.log('spyOn:');
  const calculator = {
    add(a, b) {
      return a + b;
    },
    multiply(a, b) {
      return a * b;
    },
  };

  const addSpy = spyOn(calculator, 'add');

  calculator.add(1, 2);
  calculator.add(3, 4);

  console.log('add calls:', addSpy.calls);
  addSpy.restore();

  // MockBuilder example
  console.log('\nMockBuilder:');

  const userServiceMock = new MockBuilder()
    .method('getUser')
    .returns({ id: 1, name: 'John' })
    .method('saveUser')
    .callsFake((user) => ({ ...user, id: Date.now() }))
    .method('deleteUser')
    .throws(new Error('Delete not allowed'))
    .build();

  console.log('getUser:', userServiceMock.getUser(1));
  console.log('saveUser:', userServiceMock.saveUser({ name: 'Jane' }));

  try {
    userServiceMock.deleteUser(1);
  } catch (e) {
    console.log('deleteUser threw:', e.message);
  }

  console.log('getUser calls:', userServiceMock.getUser.calls);

  console.log('');
}

// ============================================
// PART 7: Mocking Common Dependencies
// ============================================

function demonstrateCommonMocks() {
  console.log('=== Common Mock Patterns ===\n');

  // Mock Date
  console.log('Date Mock:');
  const RealDate = Date;
  const fixedDate = new Date('2024-01-15T12:00:00Z');

  global.Date = class extends RealDate {
    constructor(...args) {
      if (args.length === 0) {
        return fixedDate;
      }
      return new RealDate(...args);
    }

    static now() {
      return fixedDate.getTime();
    }
  };

  console.log('Current date (mocked):', new Date());
  console.log('Date.now() (mocked):', Date.now());

  global.Date = RealDate; // Restore
  console.log('Current date (restored):', new Date());

  // Mock Math.random
  console.log('\nMath.random Mock:');
  const originalRandom = Math.random;

  // Deterministic "random" sequence
  let randomIndex = 0;
  const randomSequence = [0.1, 0.5, 0.9, 0.3];

  Math.random = () => randomSequence[randomIndex++ % randomSequence.length];

  console.log('Random values:', [
    Math.random(),
    Math.random(),
    Math.random(),
    Math.random(),
  ]);

  Math.random = originalRandom; // Restore

  // Mock console
  console.log('\nConsole Mock:');
  const logs = [];
  const originalLog = console.log;

  console.log = (...args) => {
    logs.push(args.join(' '));
  };

  console.log('This is captured');
  console.log('So is this');

  console.log = originalLog; // Restore
  console.log('Captured logs:', logs);

  // Mock localStorage
  console.log('\nLocalStorage Mock:');

  const createLocalStorageMock = () => {
    const store = new Map();

    return {
      getItem(key) {
        return store.get(key) ?? null;
      },
      setItem(key, value) {
        store.set(key, String(value));
      },
      removeItem(key) {
        store.delete(key);
      },
      clear() {
        store.clear();
      },
      get length() {
        return store.size;
      },
      key(index) {
        return Array.from(store.keys())[index] ?? null;
      },
      // Helper for testing
      _store: store,
    };
  };

  const mockStorage = createLocalStorageMock();
  mockStorage.setItem('user', JSON.stringify({ name: 'John' }));
  console.log('Stored user:', mockStorage.getItem('user'));
  console.log('Storage length:', mockStorage.length);

  console.log('');
}

// ============================================
// RUN ALL EXAMPLES
// ============================================

console.log('╔════════════════════════════════════════════╗');
console.log('║    Mocking & Test Doubles Examples         ║');
console.log('╚════════════════════════════════════════════╝\n');

demonstrateDummy();
demonstrateStub();
demonstrateSpy();
demonstrateMock();
demonstrateFake();
demonstrateAdvancedPatterns();
demonstrateCommonMocks();

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

// Export for use
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    createSpy,
    createMock,
    FakeDatabase,
    FakeHttpClient,
    FakeTimer,
    MockBuilder,
    spyOn,
  };
}
Examples - JavaScript Tutorial | DeepML