Home | History | Annotate | Download | only in mock4js
      1 /**
      2  * Mock4JS 0.2
      3  * http://mock4js.sourceforge.net/
      4  */
      5 
      6 Mock4JS = {
      7 	_mocksToVerify: [],
      8 	_convertToConstraint: function(constraintOrValue) {
      9 		if(constraintOrValue.argumentMatches) {
     10 			return constraintOrValue; // it's already an ArgumentMatcher
     11 		} else {
     12 			return new MatchExactly(constraintOrValue);	// default to eq(...)
     13 		}
     14 	},
     15 	addMockSupport: function(object) {
     16 		// mock creation
     17 		object.mock = function(mockedType) {
     18 			if(!mockedType) {
     19 				throw new Mock4JSException("Cannot create mock: type to mock cannot be found or is null");
     20 			}
     21 			var newMock = new Mock(mockedType);
     22 			Mock4JS._mocksToVerify.push(newMock);
     23 			return newMock;
     24 		}
     25 
     26 		// syntactic sugar for expects()
     27 		object.once = function() {
     28 			return new CallCounter(1);
     29 		}
     30 		object.never = function() {
     31 			return new CallCounter(0);
     32 		}
     33 		object.exactly = function(expectedCallCount) {
     34 			return new CallCounter(expectedCallCount);
     35 		}
     36 		object.atLeastOnce = function() {
     37 			return new InvokeAtLeastOnce();
     38 		}
     39 
     40 		// syntactic sugar for argument expectations
     41 		object.ANYTHING = new MatchAnything();
     42 		object.NOT_NULL = new MatchAnythingBut(new MatchExactly(null));
     43 		object.NOT_UNDEFINED = new MatchAnythingBut(new MatchExactly(undefined));
     44 		object.eq = function(expectedValue) {
     45 			return new MatchExactly(expectedValue);
     46 		}
     47 		object.not = function(valueNotExpected) {
     48 			var argConstraint = Mock4JS._convertToConstraint(valueNotExpected);
     49 			return new MatchAnythingBut(argConstraint);
     50 		}
     51 		object.and = function() {
     52 			var constraints = [];
     53 			for(var i=0; i<arguments.length; i++) {
     54 				constraints[i] = Mock4JS._convertToConstraint(arguments[i]);
     55 			}
     56 			return new MatchAllOf(constraints);
     57 		}
     58 		object.or = function() {
     59 			var constraints = [];
     60 			for(var i=0; i<arguments.length; i++) {
     61 				constraints[i] = Mock4JS._convertToConstraint(arguments[i]);
     62 			}
     63 			return new MatchAnyOf(constraints);
     64 		}
     65 		object.stringContains = function(substring) {
     66 			return new MatchStringContaining(substring);
     67 		}
     68 
     69 		// syntactic sugar for will()
     70 		object.returnValue = function(value) {
     71 			return new ReturnValueAction(value);
     72 		}
     73 		object.throwException = function(exception) {
     74 			return new ThrowExceptionAction(exception);
     75 		}
     76 	},
     77 	clearMocksToVerify: function() {
     78 		Mock4JS._mocksToVerify = [];
     79 	},
     80 	verifyAllMocks: function() {
     81 		for(var i=0; i<Mock4JS._mocksToVerify.length; i++) {
     82 			Mock4JS._mocksToVerify[i].verify();
     83 		}
     84 	}
     85 }
     86 
     87 Mock4JSUtil = {
     88 	hasFunction: function(obj, methodName) {
     89 		return typeof obj == 'object' && typeof obj[methodName] == 'function';
     90 	},
     91 	join: function(list) {
     92 		var result = "";
     93 		for(var i=0; i<list.length; i++) {
     94 			var item = list[i];
     95 			if(Mock4JSUtil.hasFunction(item, "describe")) {
     96 				result += item.describe();
     97 			}
     98 			else if(typeof list[i] == 'string') {
     99 				result += "\""+list[i]+"\"";
    100 			} else {
    101 				result += list[i];
    102 			}
    103 
    104 			if(i<list.length-1) result += ", ";
    105 		}
    106 		return result;
    107 	}
    108 }
    109 
    110 Mock4JSException = function(message) {
    111 	this.message = message;
    112 }
    113 
    114 Mock4JSException.prototype = {
    115 	toString: function() {
    116 		return this.message;
    117 	}
    118 }
    119 
    120 /**
    121  * Assert function that makes use of the constraint methods
    122  */
    123 assertThat = function(expected, argumentMatcher) {
    124 	if(!argumentMatcher.argumentMatches(expected)) {
    125 		fail("Expected '"+expected+"' to be "+argumentMatcher.describe());
    126 	}
    127 }
    128 
    129 /**
    130  * CallCounter
    131  */
    132 function CallCounter(expectedCount) {
    133 	this._expectedCallCount = expectedCount;
    134 	this._actualCallCount = 0;
    135 }
    136 
    137 CallCounter.prototype = {
    138 	addActualCall: function() {
    139 		this._actualCallCount++;
    140 		if(this._actualCallCount > this._expectedCallCount) {
    141 			throw new Mock4JSException("unexpected invocation");
    142 		}
    143 	},
    144 
    145 	verify: function() {
    146 		if(this._actualCallCount < this._expectedCallCount) {
    147 			throw new Mock4JSException("expected method was not invoked the expected number of times");
    148 		}
    149 	},
    150 
    151 	describe: function() {
    152 		if(this._expectedCallCount == 0) {
    153 			return "not expected";
    154 		} else if(this._expectedCallCount == 1) {
    155 			var msg = "expected once";
    156 			if(this._actualCallCount >= 1) {
    157 				msg += " and has been invoked";
    158 			}
    159 			return msg;
    160 		} else {
    161 			var msg = "expected "+this._expectedCallCount+" times";
    162 			if(this._actualCallCount > 0) {
    163 				msg += ", invoked "+this._actualCallCount + " times";
    164 			}
    165 			return msg;
    166 		}
    167 	}
    168 }
    169 
    170 function InvokeAtLeastOnce() {
    171 	this._hasBeenInvoked = false;
    172 }
    173 
    174 InvokeAtLeastOnce.prototype = {
    175 	addActualCall: function() {
    176 		this._hasBeenInvoked = true;
    177 	},
    178 
    179 	verify: function() {
    180 		if(this._hasBeenInvoked === false) {
    181 			throw new Mock4JSException(describe());
    182 		}
    183 	},
    184 
    185 	describe: function() {
    186 		var desc = "expected at least once";
    187 		if(this._hasBeenInvoked) desc+=" and has been invoked";
    188 		return desc;
    189 	}
    190 }
    191 
    192 /**
    193  * ArgumentMatchers
    194  */
    195 
    196 function MatchExactly(expectedValue) {
    197 	this._expectedValue = expectedValue;
    198 }
    199 
    200 MatchExactly.prototype = {
    201 	argumentMatches: function(actualArgument) {
    202 		if(this._expectedValue instanceof Array) {
    203 			if(!(actualArgument instanceof Array)) return false;
    204 			if(this._expectedValue.length != actualArgument.length) return false;
    205 			for(var i=0; i<this._expectedValue.length; i++) {
    206 				if(this._expectedValue[i] != actualArgument[i]) return false;
    207 			}
    208 			return true;
    209 		} else {
    210 			return this._expectedValue == actualArgument;
    211 		}
    212 	},
    213 	describe: function() {
    214 		if(typeof this._expectedValue == "string") {
    215 			return "eq(\""+this._expectedValue+"\")";
    216 		} else {
    217 			return "eq("+this._expectedValue+")";
    218 		}
    219 	}
    220 }
    221 
    222 function MatchAnything() {
    223 }
    224 
    225 MatchAnything.prototype = {
    226 	argumentMatches: function(actualArgument) {
    227 		return true;
    228 	},
    229 	describe: function() {
    230 		return "ANYTHING";
    231 	}
    232 }
    233 
    234 function MatchAnythingBut(matcherToNotMatch) {
    235 	this._matcherToNotMatch = matcherToNotMatch;
    236 }
    237 
    238 MatchAnythingBut.prototype = {
    239 	argumentMatches: function(actualArgument) {
    240 		return !this._matcherToNotMatch.argumentMatches(actualArgument);
    241 	},
    242 	describe: function() {
    243 		return "not("+this._matcherToNotMatch.describe()+")";
    244 	}
    245 }
    246 
    247 function MatchAllOf(constraints) {
    248 	this._constraints = constraints;
    249 }
    250 
    251 
    252 MatchAllOf.prototype = {
    253 	argumentMatches: function(actualArgument) {
    254 		for(var i=0; i<this._constraints.length; i++) {
    255 			var constraint = this._constraints[i];
    256 			if(!constraint.argumentMatches(actualArgument)) return false;
    257 		}
    258 		return true;
    259 	},
    260 	describe: function() {
    261 		return "and("+Mock4JSUtil.join(this._constraints)+")";
    262 	}
    263 }
    264 
    265 function MatchAnyOf(constraints) {
    266 	this._constraints = constraints;
    267 }
    268 
    269 MatchAnyOf.prototype = {
    270 	argumentMatches: function(actualArgument) {
    271 		for(var i=0; i<this._constraints.length; i++) {
    272 			var constraint = this._constraints[i];
    273 			if(constraint.argumentMatches(actualArgument)) return true;
    274 		}
    275 		return false;
    276 	},
    277 	describe: function() {
    278 		return "or("+Mock4JSUtil.join(this._constraints)+")";
    279 	}
    280 }
    281 
    282 
    283 function MatchStringContaining(stringToLookFor) {
    284 	this._stringToLookFor = stringToLookFor;
    285 }
    286 
    287 MatchStringContaining.prototype = {
    288 	argumentMatches: function(actualArgument) {
    289 		if(typeof actualArgument != 'string') throw new Mock4JSException("stringContains() must be given a string, actually got a "+(typeof actualArgument));
    290 		return (actualArgument.indexOf(this._stringToLookFor) != -1);
    291 	},
    292 	describe: function() {
    293 		return "a string containing \""+this._stringToLookFor+"\"";
    294 	}
    295 }
    296 
    297 
    298 /**
    299  * StubInvocation
    300  */
    301 function StubInvocation(expectedMethodName, expectedArgs, actionSequence) {
    302 	this._expectedMethodName = expectedMethodName;
    303 	this._expectedArgs = expectedArgs;
    304 	this._actionSequence = actionSequence;
    305 }
    306 
    307 StubInvocation.prototype = {
    308 	matches: function(invokedMethodName, invokedMethodArgs) {
    309 		if (invokedMethodName != this._expectedMethodName) {
    310 			return false;
    311 		}
    312 
    313 		if (invokedMethodArgs.length != this._expectedArgs.length) {
    314 			return false;
    315 		}
    316 
    317 		for(var i=0; i<invokedMethodArgs.length; i++) {
    318 			var expectedArg = this._expectedArgs[i];
    319 			var invokedArg = invokedMethodArgs[i];
    320 			if(!expectedArg.argumentMatches(invokedArg)) {
    321 				return false;
    322 			}
    323 		}
    324 
    325 		return true;
    326 	},
    327 
    328 	invoked: function() {
    329 		try {
    330 			return this._actionSequence.invokeNextAction();
    331 		} catch(e) {
    332 			if(e instanceof Mock4JSException) {
    333 				throw new Mock4JSException(this.describeInvocationNameAndArgs()+" - "+e.message);
    334 			} else {
    335 				throw e;
    336 			}
    337 		}
    338 	},
    339 
    340 	will: function() {
    341 		this._actionSequence.addAll.apply(this._actionSequence, arguments);
    342 	},
    343 
    344 	describeInvocationNameAndArgs: function() {
    345 		return this._expectedMethodName+"("+Mock4JSUtil.join(this._expectedArgs)+")";
    346 	},
    347 
    348 	describe: function() {
    349 		return "stub: "+this.describeInvocationNameAndArgs();
    350 	},
    351 
    352 	verify: function() {
    353 	}
    354 }
    355 
    356 /**
    357  * ExpectedInvocation
    358  */
    359 function ExpectedInvocation(expectedMethodName, expectedArgs, expectedCallCounter) {
    360 	this._stubInvocation = new StubInvocation(expectedMethodName, expectedArgs, new ActionSequence());
    361 	this._expectedCallCounter = expectedCallCounter;
    362 }
    363 
    364 ExpectedInvocation.prototype = {
    365 	matches: function(invokedMethodName, invokedMethodArgs) {
    366 		try {
    367 			return this._stubInvocation.matches(invokedMethodName, invokedMethodArgs);
    368 		} catch(e) {
    369 			throw new Mock4JSException("method "+this._stubInvocation.describeInvocationNameAndArgs()+": "+e.message);
    370 		}
    371 	},
    372 
    373 	invoked: function() {
    374 		try {
    375 			this._expectedCallCounter.addActualCall();
    376 		} catch(e) {
    377 			throw new Mock4JSException(e.message+": "+this._stubInvocation.describeInvocationNameAndArgs());
    378 		}
    379 		return this._stubInvocation.invoked();
    380 	},
    381 
    382 	will: function() {
    383 		this._stubInvocation.will.apply(this._stubInvocation, arguments);
    384 	},
    385 
    386 	describe: function() {
    387 		return this._expectedCallCounter.describe()+": "+this._stubInvocation.describeInvocationNameAndArgs();
    388 	},
    389 
    390 	verify: function() {
    391 		try {
    392 			this._expectedCallCounter.verify();
    393 		} catch(e) {
    394 			throw new Mock4JSException(e.message+": "+this._stubInvocation.describeInvocationNameAndArgs());
    395 		}
    396 	}
    397 }
    398 
    399 /**
    400  * MethodActions
    401  */
    402 function ReturnValueAction(valueToReturn) {
    403 	this._valueToReturn = valueToReturn;
    404 }
    405 
    406 ReturnValueAction.prototype = {
    407 	invoke: function() {
    408 		return this._valueToReturn;
    409 	},
    410 	describe: function() {
    411 		return "returns "+this._valueToReturn;
    412 	}
    413 }
    414 
    415 function ThrowExceptionAction(exceptionToThrow) {
    416 	this._exceptionToThrow = exceptionToThrow;
    417 }
    418 
    419 ThrowExceptionAction.prototype = {
    420 	invoke: function() {
    421 		throw this._exceptionToThrow;
    422 	},
    423 	describe: function() {
    424 		return "throws "+this._exceptionToThrow;
    425 	}
    426 }
    427 
    428 function ActionSequence() {
    429 	this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP";
    430 	this._actionSequence = this._ACTIONS_NOT_SETUP;
    431 	this._indexOfNextAction = 0;
    432 }
    433 
    434 ActionSequence.prototype = {
    435 	invokeNextAction: function() {
    436 		if(this._actionSequence === this._ACTIONS_NOT_SETUP) {
    437 			return;
    438 		} else {
    439 			if(this._indexOfNextAction >= this._actionSequence.length) {
    440 				throw new Mock4JSException("no more values to return");
    441 			} else {
    442 				var action = this._actionSequence[this._indexOfNextAction];
    443 				this._indexOfNextAction++;
    444 				return action.invoke();
    445 			}
    446 		}
    447 	},
    448 
    449 	addAll: function() {
    450 		this._actionSequence = [];
    451 		for(var i=0; i<arguments.length; i++) {
    452 			if(typeof arguments[i] != 'object' && arguments[i].invoke === undefined) {
    453 				throw new Error("cannot add a method action that does not have an invoke() method");
    454 			}
    455 			this._actionSequence.push(arguments[i]);
    456 		}
    457 	}
    458 }
    459 
    460 function StubActionSequence() {
    461 	this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP";
    462 	this._actionSequence = this._ACTIONS_NOT_SETUP;
    463 	this._indexOfNextAction = 0;
    464 }
    465 
    466 StubActionSequence.prototype = {
    467 	invokeNextAction: function() {
    468 		if(this._actionSequence === this._ACTIONS_NOT_SETUP) {
    469 			return;
    470 		} else if(this._actionSequence.length == 1) {
    471 			// if there is only one method action, keep doing that on every invocation
    472 			return this._actionSequence[0].invoke();
    473 		} else {
    474 			if(this._indexOfNextAction >= this._actionSequence.length) {
    475 				throw new Mock4JSException("no more values to return");
    476 			} else {
    477 				var action = this._actionSequence[this._indexOfNextAction];
    478 				this._indexOfNextAction++;
    479 				return action.invoke();
    480 			}
    481 		}
    482 	},
    483 
    484 	addAll: function() {
    485 		this._actionSequence = [];
    486 		for(var i=0; i<arguments.length; i++) {
    487 			if(typeof arguments[i] != 'object' && arguments[i].invoke === undefined) {
    488 				throw new Error("cannot add a method action that does not have an invoke() method");
    489 			}
    490 			this._actionSequence.push(arguments[i]);
    491 		}
    492 	}
    493 }
    494 
    495 
    496 /**
    497  * Mock
    498  */
    499 function Mock(mockedType) {
    500 	if(mockedType === undefined || mockedType.prototype === undefined) {
    501 		throw new Mock4JSException("Unable to create Mock: must create Mock using a class not prototype, eg. 'new Mock(TypeToMock)' or using the convenience method 'mock(TypeToMock)'");
    502 	}
    503 	this._mockedType = mockedType.prototype;
    504 	this._expectedCallCount;
    505 	this._isRecordingExpectations = false;
    506 	this._expectedInvocations = [];
    507 
    508 	// setup proxy
    509 	var IntermediateClass = new Function();
    510 	IntermediateClass.prototype = mockedType.prototype;
    511 	var ChildClass = new Function();
    512 	ChildClass.prototype = new IntermediateClass();
    513 	this._proxy = new ChildClass();
    514 	this._proxy.mock = this;
    515 
    516 	for(property in mockedType.prototype) {
    517 		if(this._isPublicMethod(mockedType.prototype, property)) {
    518 			var publicMethodName = property;
    519 			this._proxy[publicMethodName] = this._createMockedMethod(publicMethodName);
    520 			this[publicMethodName] = this._createExpectationRecordingMethod(publicMethodName);
    521 		}
    522 	}
    523 }
    524 
    525 Mock.prototype = {
    526 
    527 	proxy: function() {
    528 		return this._proxy;
    529 	},
    530 
    531 	expects: function(expectedCallCount) {
    532 		this._expectedCallCount = expectedCallCount;
    533 		this._isRecordingExpectations = true;
    534 		this._isRecordingStubs = false;
    535 		return this;
    536 	},
    537 
    538 	stubs: function() {
    539 		this._isRecordingExpectations = false;
    540 		this._isRecordingStubs = true;
    541 		return this;
    542 	},
    543 
    544 	verify: function() {
    545 		for(var i=0; i<this._expectedInvocations.length; i++) {
    546 			var expectedInvocation = this._expectedInvocations[i];
    547 			try {
    548 				expectedInvocation.verify();
    549 			} catch(e) {
    550 				var failMsg = e.message+this._describeMockSetup();
    551 				throw new Mock4JSException(failMsg);
    552 			}
    553 		}
    554 	},
    555 
    556 	_isPublicMethod: function(mockedType, property) {
    557 		try {
    558 			var isMethod = typeof(mockedType[property]) == 'function';
    559 			var isPublic = property.charAt(0) != "_";
    560 			return isMethod && isPublic;
    561 		} catch(e) {
    562 			return false;
    563 		}
    564 	},
    565 
    566 	_createExpectationRecordingMethod: function(methodName) {
    567 		return function() {
    568 			// ensure all arguments are instances of ArgumentMatcher
    569 			var expectedArgs = [];
    570 			for(var i=0; i<arguments.length; i++) {
    571 				if(arguments[i] !== null && arguments[i] !== undefined && arguments[i].argumentMatches) {
    572 					expectedArgs[i] = arguments[i];
    573 				} else {
    574 					expectedArgs[i] = new MatchExactly(arguments[i]);
    575 				}
    576 			}
    577 
    578 			// create stub or expected invocation
    579 			var expectedInvocation;
    580 			if(this._isRecordingExpectations) {
    581 				expectedInvocation = new ExpectedInvocation(methodName, expectedArgs, this._expectedCallCount);
    582 			} else {
    583 				expectedInvocation = new StubInvocation(methodName, expectedArgs, new StubActionSequence());
    584 			}
    585 
    586 			this._expectedInvocations.push(expectedInvocation);
    587 
    588 			this._isRecordingExpectations = false;
    589 			this._isRecordingStubs = false;
    590 			return expectedInvocation;
    591 		}
    592 	},
    593 
    594 	_createMockedMethod: function(methodName) {
    595 		return function() {
    596 			// go through expectation list backwards to ensure later expectations override earlier ones
    597 			for(var i=this.mock._expectedInvocations.length-1; i>=0; i--) {
    598 				var expectedInvocation = this.mock._expectedInvocations[i];
    599 				if(expectedInvocation.matches(methodName, arguments)) {
    600 					try {
    601 						return expectedInvocation.invoked();
    602 					} catch(e) {
    603 						if(e instanceof Mock4JSException) {
    604 							throw new Mock4JSException(e.message+this.mock._describeMockSetup());
    605 						} else {
    606 							// the user setup the mock to throw a specific error, so don't modify the message
    607 							throw e;
    608 						}
    609 					}
    610 				}
    611 			}
    612 			var failMsg = "unexpected invocation: "+methodName+"("+Mock4JSUtil.join(arguments)+")"+this.mock._describeMockSetup();
    613 			throw new Mock4JSException(failMsg);
    614 		};
    615 	},
    616 
    617 	_describeMockSetup: function() {
    618 		var msg = "\nAllowed:";
    619 		for(var i=0; i<this._expectedInvocations.length; i++) {
    620 			var expectedInvocation = this._expectedInvocations[i];
    621 			msg += "\n" + expectedInvocation.describe();
    622 		}
    623 		return msg;
    624 	}
    625 }