javascript

examples

examples.js⚔
/**
 * 20.1 Unit Testing Fundamentals - Examples
 *
 * Core concepts and patterns for unit testing
 * Note: These examples use Jest-like syntax
 */

// ============================================
// TEST STRUCTURE - AAA PATTERN
// ============================================

/**
 * The Arrange-Act-Assert pattern
 */

// Example function to test
function calculateDiscount(price, discountPercent) {
  if (price < 0) throw new Error('Price cannot be negative');
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error('Discount must be between 0 and 100');
  }
  return price * (1 - discountPercent / 100);
}

// Test examples
const testCalculateDiscount = {
  'should apply 10% discount correctly': () => {
    // Arrange
    const price = 100;
    const discount = 10;

    // Act
    const result = calculateDiscount(price, discount);

    // Assert
    console.assert(result === 90, `Expected 90, got ${result}`);
  },

  'should return full price for 0% discount': () => {
    // Arrange
    const price = 50;
    const discount = 0;

    // Act
    const result = calculateDiscount(price, discount);

    // Assert
    console.assert(result === 50, `Expected 50, got ${result}`);
  },

  'should return 0 for 100% discount': () => {
    // Arrange
    const price = 100;
    const discount = 100;

    // Act
    const result = calculateDiscount(price, discount);

    // Assert
    console.assert(result === 0, `Expected 0, got ${result}`);
  },

  'should throw for negative price': () => {
    // Arrange & Act & Assert
    try {
      calculateDiscount(-10, 10);
      console.assert(false, 'Should have thrown');
    } catch (e) {
      console.assert(e.message === 'Price cannot be negative');
    }
  },
};

// ============================================
// TEST DOUBLES - STUBS
// ============================================

/**
 * Stub: Returns predefined values
 */

// Original function with external dependency
function fetchUserData(userId, apiClient) {
  return apiClient.get(`/users/${userId}`);
}

// Create a stub
function createApiStub(responseData) {
  return {
    get: (url) => Promise.resolve({ data: responseData }),
    post: (url, data) => Promise.resolve({ data: { id: 1, ...data } }),
  };
}

// Test with stub
async function testFetchUserData() {
  // Arrange
  const stubData = { id: 1, name: 'John', email: 'john@example.com' };
  const apiStub = createApiStub(stubData);

  // Act
  const result = await fetchUserData(1, apiStub);

  // Assert
  console.assert(result.data.name === 'John', 'Should return stubbed data');
  console.log('āœ“ testFetchUserData passed');
}

// ============================================
// TEST DOUBLES - SPIES
// ============================================

/**
 * Spy: Records function calls for verification
 */

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

  const spy = function (...args) {
    calls.push({
      args,
      timestamp: Date.now(),
    });
    return implementation.apply(this, args);
  };

  spy.calls = calls;
  spy.callCount = () => calls.length;
  spy.calledWith = (...expectedArgs) => {
    return calls.some(
      (call) => JSON.stringify(call.args) === JSON.stringify(expectedArgs)
    );
  };
  spy.reset = () => {
    calls.length = 0;
  };

  return spy;
}

// Example: Testing event handlers
class EventEmitter {
  constructor() {
    this.listeners = {};
  }

  on(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(callback);
  }

  emit(event, data) {
    const callbacks = this.listeners[event] || [];
    callbacks.forEach((cb) => cb(data));
  }
}

function testEventEmitter() {
  // Arrange
  const emitter = new EventEmitter();
  const spy = createSpy();

  // Act
  emitter.on('test', spy);
  emitter.emit('test', { value: 42 });
  emitter.emit('test', { value: 100 });

  // Assert
  console.assert(spy.callCount() === 2, 'Should be called twice');
  console.assert(
    spy.calledWith({ value: 42 }),
    'Should be called with {value: 42}'
  );
  console.log('āœ“ testEventEmitter passed');
}

// ============================================
// TEST DOUBLES - MOCKS
// ============================================

/**
 * Mock: Pre-programmed with expectations
 */

function createMock(expectations = {}) {
  const calls = {};

  return new Proxy(
    {},
    {
      get(target, prop) {
        if (prop === '_verify') {
          return () => {
            for (const [method, expected] of Object.entries(expectations)) {
              const actual = calls[method] || 0;
              if (actual !== expected) {
                throw new Error(
                  `Expected ${method} to be called ${expected} times, ` +
                    `but was called ${actual} times`
                );
              }
            }
            return true;
          };
        }

        return function (...args) {
          calls[prop] = (calls[prop] || 0) + 1;
          return undefined;
        };
      },
    }
  );
}

function testWithMock() {
  // Arrange
  const loggerMock = createMock({
    info: 2,
    error: 1,
  });

  // Act
  loggerMock.info('Starting process');
  loggerMock.info('Processing item');
  loggerMock.error('Something went wrong');

  // Assert
  try {
    loggerMock._verify();
    console.log('āœ“ testWithMock passed');
  } catch (e) {
    console.log('āœ— testWithMock failed:', e.message);
  }
}

// ============================================
// TEST DOUBLES - FAKES
// ============================================

/**
 * Fake: Simplified working implementation
 */

// Fake database for testing
class FakeDatabase {
  constructor() {
    this.data = new Map();
    this.idCounter = 0;
  }

  async insert(table, record) {
    const id = ++this.idCounter;
    const newRecord = { id, ...record };

    if (!this.data.has(table)) {
      this.data.set(table, []);
    }

    this.data.get(table).push(newRecord);
    return newRecord;
  }

  async findById(table, id) {
    const records = this.data.get(table) || [];
    return records.find((r) => r.id === id) || null;
  }

  async findAll(table) {
    return this.data.get(table) || [];
  }

  async update(table, id, updates) {
    const records = this.data.get(table) || [];
    const index = records.findIndex((r) => r.id === id);

    if (index === -1) return null;

    records[index] = { ...records[index], ...updates };
    return records[index];
  }

  async delete(table, id) {
    const records = this.data.get(table) || [];
    const index = records.findIndex((r) => r.id === id);

    if (index === -1) return false;

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

  clear() {
    this.data.clear();
    this.idCounter = 0;
  }
}

// Service using the database
class UserService {
  constructor(db) {
    this.db = db;
  }

  async createUser(userData) {
    return this.db.insert('users', userData);
  }

  async getUser(id) {
    return this.db.findById('users', id);
  }

  async updateUser(id, updates) {
    return this.db.update('users', id, updates);
  }
}

async function testUserService() {
  // Arrange
  const fakeDb = new FakeDatabase();
  const userService = new UserService(fakeDb);

  // Act
  const created = await userService.createUser({
    name: 'John',
    email: 'john@example.com',
  });

  const fetched = await userService.getUser(created.id);

  const updated = await userService.updateUser(created.id, {
    name: 'John Doe',
  });

  // Assert
  console.assert(created.id === 1, 'Should have ID 1');
  console.assert(
    fetched.email === 'john@example.com',
    'Should fetch correct email'
  );
  console.assert(updated.name === 'John Doe', 'Should update name');
  console.log('āœ“ testUserService passed');
}

// ============================================
// WRITING TESTABLE CODE
// ============================================

/**
 * Example: Refactoring for testability
 */

// BAD: Hard to test
class OrderProcessor_Bad {
  processOrder(order) {
    // Hard-coded dependency
    const db = new Database();
    const emailer = new EmailService();

    // Direct date access
    order.processedAt = new Date();

    db.save('orders', order);
    emailer.send(order.customerEmail, 'Order processed');

    return order;
  }
}

// GOOD: Easy to test with dependency injection
class OrderProcessor_Good {
  constructor(db, emailer, dateProvider = () => new Date()) {
    this.db = db;
    this.emailer = emailer;
    this.getDate = dateProvider;
  }

  async processOrder(order) {
    order.processedAt = this.getDate();

    await this.db.save('orders', order);
    await this.emailer.send(order.customerEmail, 'Order processed');

    return order;
  }
}

// Test with all dependencies injected
async function testOrderProcessor() {
  // Arrange
  const fakeDb = {
    saved: [],
    save: async function (table, record) {
      this.saved.push({ table, record });
    },
  };

  const fakeEmailer = {
    sent: [],
    send: async function (to, subject) {
      this.sent.push({ to, subject });
    },
  };

  const fixedDate = new Date('2024-01-01');
  const processor = new OrderProcessor_Good(
    fakeDb,
    fakeEmailer,
    () => fixedDate
  );

  // Act
  const order = { id: 1, customerEmail: 'test@example.com' };
  await processor.processOrder(order);

  // Assert
  console.assert(fakeDb.saved.length === 1, 'Should save order');
  console.assert(fakeEmailer.sent.length === 1, 'Should send email');
  console.assert(
    order.processedAt.getTime() === fixedDate.getTime(),
    'Should use injected date'
  );
  console.log('āœ“ testOrderProcessor passed');
}

// ============================================
// SIMPLE TEST RUNNER
// ============================================

/**
 * Minimal test runner implementation
 */

class TestRunner {
  constructor() {
    this.suites = [];
    this.results = { passed: 0, failed: 0, errors: [] };
  }

  describe(name, fn) {
    this.suites.push({ name, fn });
  }

  async run() {
    for (const suite of this.suites) {
      console.log(`\nšŸ“¦ ${suite.name}`);

      const tests = [];
      const it = (description, testFn) => {
        tests.push({ description, testFn });
      };

      suite.fn(it);

      for (const test of tests) {
        try {
          await test.testFn();
          console.log(`  āœ“ ${test.description}`);
          this.results.passed++;
        } catch (error) {
          console.log(`  āœ— ${test.description}`);
          console.log(`    Error: ${error.message}`);
          this.results.failed++;
          this.results.errors.push({
            suite: suite.name,
            test: test.description,
            error: error.message,
          });
        }
      }
    }

    this.printSummary();
  }

  printSummary() {
    const total = this.results.passed + this.results.failed;
    console.log('\n' + '='.repeat(50));
    console.log(
      `Tests: ${this.results.passed} passed, ${this.results.failed} failed, ${total} total`
    );

    if (this.results.failed > 0) {
      console.log('\nFailed tests:');
      this.results.errors.forEach((err) => {
        console.log(`  - ${err.suite} > ${err.test}`);
      });
    }
  }
}

// ============================================
// ASSERTION LIBRARY
// ============================================

/**
 * Simple assertion functions
 */

const expect = (actual) => ({
  toBe(expected) {
    if (actual !== expected) {
      throw new Error(`Expected ${expected} but got ${actual}`);
    }
  },

  toEqual(expected) {
    if (JSON.stringify(actual) !== JSON.stringify(expected)) {
      throw new Error(
        `Expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`
      );
    }
  },

  toBeTruthy() {
    if (!actual) {
      throw new Error(`Expected truthy value but got ${actual}`);
    }
  },

  toBeFalsy() {
    if (actual) {
      throw new Error(`Expected falsy value but got ${actual}`);
    }
  },

  toBeNull() {
    if (actual !== null) {
      throw new Error(`Expected null but got ${actual}`);
    }
  },

  toBeUndefined() {
    if (actual !== undefined) {
      throw new Error(`Expected undefined but got ${actual}`);
    }
  },

  toContain(item) {
    if (!actual.includes(item)) {
      throw new Error(`Expected ${actual} to contain ${item}`);
    }
  },

  toHaveLength(length) {
    if (actual.length !== length) {
      throw new Error(`Expected length ${length} but got ${actual.length}`);
    }
  },

  toThrow(expectedMessage) {
    let threw = false;
    let message = '';

    try {
      actual();
    } catch (e) {
      threw = true;
      message = e.message;
    }

    if (!threw) {
      throw new Error('Expected function to throw');
    }

    if (expectedMessage && message !== expectedMessage) {
      throw new Error(
        `Expected error message "${expectedMessage}" but got "${message}"`
      );
    }
  },

  toBeInstanceOf(constructor) {
    if (!(actual instanceof constructor)) {
      throw new Error(`Expected instance of ${constructor.name}`);
    }
  },

  toBeGreaterThan(expected) {
    if (actual <= expected) {
      throw new Error(`Expected ${actual} to be greater than ${expected}`);
    }
  },

  toBeLessThan(expected) {
    if (actual >= expected) {
      throw new Error(`Expected ${actual} to be less than ${expected}`);
    }
  },
});

// ============================================
// RUN DEMONSTRATIONS
// ============================================

console.log('=== Unit Testing Fundamentals ===\n');

// Run test examples
Object.entries(testCalculateDiscount).forEach(([name, test]) => {
  try {
    test();
    console.log(`āœ“ ${name}`);
  } catch (e) {
    console.log(`āœ— ${name}: ${e.message}`);
  }
});

console.log('');

// Run other tests
testFetchUserData();
testEventEmitter();
testWithMock();
testUserService().then(() => {
  testOrderProcessor();

  console.log('\n--- Using expect assertions ---');

  try {
    expect(5).toBe(5);
    console.log('āœ“ expect(5).toBe(5)');

    expect([1, 2, 3]).toContain(2);
    console.log('āœ“ expect([1,2,3]).toContain(2)');

    expect({ a: 1 }).toEqual({ a: 1 });
    console.log('āœ“ expect({a:1}).toEqual({a:1})');

    expect(() => {
      throw new Error('test');
    }).toThrow('test');
    console.log('āœ“ expect(fn).toThrow("test")');
  } catch (e) {
    console.log('āœ— Assertion failed:', e.message);
  }
});

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