1/**
2 * Mock4JS 0.2
3 * http://mock4js.sourceforge.net/
4 */
5
6Mock4JS = {
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
87Mock4JSUtil = {
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
110Mock4JSException = function(message) {
111	this.message = message;
112}
113
114Mock4JSException.prototype = {
115	toString: function() {
116		return this.message;
117	}
118}
119
120/**
121 * Assert function that makes use of the constraint methods
122 */
123assertThat = function(expected, argumentMatcher) {
124	if(!argumentMatcher.argumentMatches(expected)) {
125		fail("Expected '"+expected+"' to be "+argumentMatcher.describe());
126	}
127}
128
129/**
130 * CallCounter
131 */
132function CallCounter(expectedCount) {
133	this._expectedCallCount = expectedCount;
134	this._actualCallCount = 0;
135}
136
137CallCounter.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
170function InvokeAtLeastOnce() {
171	this._hasBeenInvoked = false;
172}
173
174InvokeAtLeastOnce.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
196function MatchExactly(expectedValue) {
197	this._expectedValue = expectedValue;
198}
199
200MatchExactly.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
222function MatchAnything() {
223}
224
225MatchAnything.prototype = {
226	argumentMatches: function(actualArgument) {
227		return true;
228	},
229	describe: function() {
230		return "ANYTHING";
231	}
232}
233
234function MatchAnythingBut(matcherToNotMatch) {
235	this._matcherToNotMatch = matcherToNotMatch;
236}
237
238MatchAnythingBut.prototype = {
239	argumentMatches: function(actualArgument) {
240		return !this._matcherToNotMatch.argumentMatches(actualArgument);
241	},
242	describe: function() {
243		return "not("+this._matcherToNotMatch.describe()+")";
244	}
245}
246
247function MatchAllOf(constraints) {
248	this._constraints = constraints;
249}
250
251
252MatchAllOf.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
265function MatchAnyOf(constraints) {
266	this._constraints = constraints;
267}
268
269MatchAnyOf.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
283function MatchStringContaining(stringToLookFor) {
284	this._stringToLookFor = stringToLookFor;
285}
286
287MatchStringContaining.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 */
301function StubInvocation(expectedMethodName, expectedArgs, actionSequence) {
302	this._expectedMethodName = expectedMethodName;
303	this._expectedArgs = expectedArgs;
304	this._actionSequence = actionSequence;
305}
306
307StubInvocation.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 */
359function ExpectedInvocation(expectedMethodName, expectedArgs, expectedCallCounter) {
360	this._stubInvocation = new StubInvocation(expectedMethodName, expectedArgs, new ActionSequence());
361	this._expectedCallCounter = expectedCallCounter;
362}
363
364ExpectedInvocation.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 */
402function ReturnValueAction(valueToReturn) {
403	this._valueToReturn = valueToReturn;
404}
405
406ReturnValueAction.prototype = {
407	invoke: function() {
408		return this._valueToReturn;
409	},
410	describe: function() {
411		return "returns "+this._valueToReturn;
412	}
413}
414
415function ThrowExceptionAction(exceptionToThrow) {
416	this._exceptionToThrow = exceptionToThrow;
417}
418
419ThrowExceptionAction.prototype = {
420	invoke: function() {
421		throw this._exceptionToThrow;
422	},
423	describe: function() {
424		return "throws "+this._exceptionToThrow;
425	}
426}
427
428function ActionSequence() {
429	this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP";
430	this._actionSequence = this._ACTIONS_NOT_SETUP;
431	this._indexOfNextAction = 0;
432}
433
434ActionSequence.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
460function StubActionSequence() {
461	this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP";
462	this._actionSequence = this._ACTIONS_NOT_SETUP;
463	this._indexOfNextAction = 0;
464}
465
466StubActionSequence.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 */
499function 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
525Mock.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}