// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assertDeepEquals, assertEquals} from './chai_assert.js';
/**
* Create a mock function that records function calls and validates against
* expectations.
*/
export class MockMethod {
/** List of signatures for function calls. */
private calls_: any[][] = [];
/** List of expected call signatures. */
private expectations_: any[][] = [];
/** Value returned from call to function. */
returnValue: any|undefined = undefined;
/** List of arguments for callback function. */
callbackData: any[][] = [];
/** Name of the function being replaced. */
functionName: string|null = null;
constructor() {
const fn: MockMethod|Function = function() {
const args = Array.prototype.slice.call(arguments);
const callbacks = args.filter(function(arg) {
return (typeof arg === 'function');
});
if (callbacks.length > 1) {
console.error(
'Only support mocking function with at most one callback.');
return;
}
const fnAsMethod = fn as unknown as MockMethod;
fnAsMethod.recordCall(args);
if (callbacks.length === 1) {
callbacks[0].apply(undefined, fnAsMethod.callbackData);
return;
}
return fnAsMethod.returnValue;
};
const fnAsMethod = fn as unknown as MockMethod;
Object.assign(fnAsMethod, this);
Object.setPrototypeOf(fnAsMethod, MockMethod.prototype);
return fnAsMethod;
}
/**
* Adds an expected call signature.
* @param args Expected arguments for the function call.
*/
addExpectation(...args: any[]) {
this.expectations_.push(args.filter(this.notFunction_));
}
/**
* Adds a call signature.
*/
recordCall(args: any[]) {
this.calls_.push(args.filter(this.notFunction_));
}
/**
* Verifies that the function is called the expected number of times and with
* the correct signature for each call.
*/
verifyMock() {
let errorMessage = 'Number of method calls did not match expectation.';
if (this.functionName) {
errorMessage = 'Error in ' + this.functionName + ':\n' + errorMessage;
}
assertEquals(this.expectations_.length, this.calls_.length, errorMessage);
for (let i = 0; i < this.expectations_.length; i++) {
this.validateCall(i, this.expectations_[i]!, this.calls_[i]!);
}
}
/**
* Verifies that the observed function arguments match expectations.
* Override if strict equality is not required.
* @param index Canonical index of the function call. Unused in the
* base implementation, but provides context that may be useful for
* overrides.
* @param expected The expected arguments.
* @param observed The observed arguments.
*/
validateCall(_index: number, expected: any[], observed: any[]) {
assertDeepEquals(expected, observed);
}
/**
* Test if arg is a function.
* @param arg The argument to test.
* @return True if arg is not function type.
*/
private notFunction_(arg: any): boolean {
return typeof arg !== 'function';
}
}
interface Override {
parent: {[key: string]: any};
functionName: string;
originalFunction: Function;
}
/**
* Controller for mocking methods. Tracks calls to mocked methods and verifies
* that call signatures match expectations.
*/
export class MockController {
/**
* Original functions implementations, which are restored when |reset| is
* called.
*/
private overrides_: Override[] = [];
/** List of registered mocks. */
private mocks_: MockMethod[] = [];
/**
* Creates a mock function.
* @param parent Optional parent object for the function.
* @param functionName Optional name of the function being
* mocked. If the parent and function name are both provided, the
* mock is automatically substituted for the original and replaced on
* reset.
*/
createFunctionMock(parent?: {[key: string]: any}, functionName?: string) {
const fn = new MockMethod();
// Register mock.
if (parent && functionName) {
this.overrides_.push({
parent: parent,
functionName: functionName,
originalFunction: parent[functionName],
});
parent[functionName] = fn;
fn.functionName = functionName;
}
this.mocks_.push(fn);
return fn;
}
/**
* Validates all mocked methods. An exception is thrown if the
* expected and actual calls to a mocked function to not align.
*/
verifyMocks() {
for (let i = 0; i < this.mocks_.length; i++) {
this.mocks_[i]!.verifyMock();
}
}
/**
* Discard mocks reestoring default behavior.
*/
reset() {
for (let i = 0; i < this.overrides_.length; i++) {
const override = this.overrides_[i]!;
override.parent[override.functionName] = override.originalFunction;
}
}
}