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