javascript
examples
examples.jsā”javascript
/**
* 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,
};
}