3 * http://mock4js.sourceforge.net/
8 _convertToConstraint: function(constraintOrValue) {
9 if(constraintOrValue.argumentMatches) {
10 return constraintOrValue; // it's already an ArgumentMatcher
12 return new MatchExactly(constraintOrValue); // default to eq(...)
15 addMockSupport: function(object) {
17 object.mock = function(mockedType) {
19 throw new Mock4JSException("Cannot create mock: type to mock cannot be found or is null");
21 var newMock = new Mock(mockedType);
22 Mock4JS._mocksToVerify.push(newMock);
26 // syntactic sugar for expects()
27 object.once = function() {
28 return new CallCounter(1);
30 object.never = function() {
31 return new CallCounter(0);
33 object.exactly = function(expectedCallCount) {
34 return new CallCounter(expectedCallCount);
36 object.atLeastOnce = function() {
37 return new InvokeAtLeastOnce();
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);
47 object.not = function(valueNotExpected) {
48 var argConstraint = Mock4JS._convertToConstraint(valueNotExpected);
49 return new MatchAnythingBut(argConstraint);
51 object.and = function() {
53 for(var i=0; i<arguments.length; i++) {
54 constraints[i] = Mock4JS._convertToConstraint(arguments[i]);
56 return new MatchAllOf(constraints);
58 object.or = function() {
60 for(var i=0; i<arguments.length; i++) {
61 constraints[i] = Mock4JS._convertToConstraint(arguments[i]);
63 return new MatchAnyOf(constraints);
65 object.stringContains = function(substring) {
66 return new MatchStringContaining(substring);
69 // syntactic sugar for will()
70 object.returnValue = function(value) {
71 return new ReturnValueAction(value);
73 object.throwException = function(exception) {
74 return new ThrowExceptionAction(exception);
77 clearMocksToVerify: function() {
78 Mock4JS._mocksToVerify = [];
80 verifyAllMocks: function() {
81 for(var i=0; i<Mock4JS._mocksToVerify.length; i++) {
82 Mock4JS._mocksToVerify[i].verify();
88 hasFunction: function(obj, methodName) {
89 return typeof obj == 'object' && typeof obj[methodName] == 'function';
91 join: function(list) {
93 for(var i=0; i<list.length; i++) {
95 if(Mock4JSUtil.hasFunction(item, "describe")) {
96 result += item.describe();
98 else if(typeof list[i] == 'string') {
99 result += "\""+list[i]+"\"";
104 if(i<list.length-1) result += ", ";
110 Mock4JSException = function(message) {
111 this.message = message;
114 Mock4JSException.prototype = {
115 toString: function() {
121 * Assert function that makes use of the constraint methods
123 assertThat = function(expected, argumentMatcher) {
124 if(!argumentMatcher.argumentMatches(expected)) {
125 throw new Mock4JSException("Expected '"+expected+"' to be "+argumentMatcher.describe());
132 function CallCounter(expectedCount) {
133 this._expectedCallCount = expectedCount;
134 this._actualCallCount = 0;
137 CallCounter.prototype = {
138 addActualCall: function() {
139 this._actualCallCount++;
140 if(this._actualCallCount > this._expectedCallCount) {
141 throw new Mock4JSException("unexpected invocation");
146 if(this._actualCallCount < this._expectedCallCount) {
147 throw new Mock4JSException("expected method was not invoked the expected number of times");
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";
161 var msg = "expected "+this._expectedCallCount+" times";
162 if(this._actualCallCount > 0) {
163 msg += ", invoked "+this._actualCallCount + " times";
170 function InvokeAtLeastOnce() {
171 this._hasBeenInvoked = false;
174 InvokeAtLeastOnce.prototype = {
175 addActualCall: function() {
176 this._hasBeenInvoked = true;
180 if(this._hasBeenInvoked === false) {
181 throw new Mock4JSException(describe());
185 describe: function() {
186 var desc = "expected at least once";
187 if(this._hasBeenInvoked) desc+=" and has been invoked";
196 function MatchExactly(expectedValue) {
197 this._expectedValue = expectedValue;
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;
210 return this._expectedValue == actualArgument;
213 describe: function() {
214 if(typeof this._expectedValue == "string") {
215 return "eq(\""+this._expectedValue+"\")";
217 return "eq("+this._expectedValue+")";
222 function MatchAnything() {
225 MatchAnything.prototype = {
226 argumentMatches: function(actualArgument) {
229 describe: function() {
234 function MatchAnythingBut(matcherToNotMatch) {
235 this._matcherToNotMatch = matcherToNotMatch;
238 MatchAnythingBut.prototype = {
239 argumentMatches: function(actualArgument) {
240 return !this._matcherToNotMatch.argumentMatches(actualArgument);
242 describe: function() {
243 return "not("+this._matcherToNotMatch.describe()+")";
247 function MatchAllOf(constraints) {
248 this._constraints = constraints;
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;
260 describe: function() {
261 return "and("+Mock4JSUtil.join(this._constraints)+")";
265 function MatchAnyOf(constraints) {
266 this._constraints = constraints;
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;
277 describe: function() {
278 return "or("+Mock4JSUtil.join(this._constraints)+")";
283 function MatchStringContaining(stringToLookFor) {
284 this._stringToLookFor = stringToLookFor;
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);
292 describe: function() {
293 return "a string containing \""+this._stringToLookFor+"\"";
301 function StubInvocation(expectedMethodName, expectedArgs, actionSequence) {
302 this._expectedMethodName = expectedMethodName;
303 this._expectedArgs = expectedArgs;
304 this._actionSequence = actionSequence;
307 StubInvocation.prototype = {
308 matches: function(invokedMethodName, invokedMethodArgs) {
309 if (invokedMethodName != this._expectedMethodName) {
313 if (invokedMethodArgs.length != this._expectedArgs.length) {
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)) {
328 invoked: function() {
330 return this._actionSequence.invokeNextAction();
332 if(e instanceof Mock4JSException) {
333 throw new Mock4JSException(this.describeInvocationNameAndArgs()+" - "+e.message);
341 this._actionSequence.addAll.apply(this._actionSequence, arguments);
344 describeInvocationNameAndArgs: function() {
345 return this._expectedMethodName+"("+Mock4JSUtil.join(this._expectedArgs)+")";
348 describe: function() {
349 return "stub: "+this.describeInvocationNameAndArgs();
359 function ExpectedInvocation(expectedMethodName, expectedArgs, expectedCallCounter) {
360 this._stubInvocation = new StubInvocation(expectedMethodName, expectedArgs, new ActionSequence());
361 this._expectedCallCounter = expectedCallCounter;
364 ExpectedInvocation.prototype = {
365 matches: function(invokedMethodName, invokedMethodArgs) {
367 return this._stubInvocation.matches(invokedMethodName, invokedMethodArgs);
369 throw new Mock4JSException("method "+this._stubInvocation.describeInvocationNameAndArgs()+": "+e.message);
373 invoked: function() {
375 this._expectedCallCounter.addActualCall();
377 throw new Mock4JSException(e.message+": "+this._stubInvocation.describeInvocationNameAndArgs());
379 return this._stubInvocation.invoked();
383 this._stubInvocation.will.apply(this._stubInvocation, arguments);
386 describe: function() {
387 return this._expectedCallCounter.describe()+": "+this._stubInvocation.describeInvocationNameAndArgs();
392 this._expectedCallCounter.verify();
394 throw new Mock4JSException(e.message+": "+this._stubInvocation.describeInvocationNameAndArgs());
402 function ReturnValueAction(valueToReturn) {
403 this._valueToReturn = valueToReturn;
406 ReturnValueAction.prototype = {
408 return this._valueToReturn;
410 describe: function() {
411 return "returns "+this._valueToReturn;
415 function ThrowExceptionAction(exceptionToThrow) {
416 this._exceptionToThrow = exceptionToThrow;
419 ThrowExceptionAction.prototype = {
421 throw this._exceptionToThrow;
423 describe: function() {
424 return "throws "+this._exceptionToThrow;
428 function ActionSequence() {
429 this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP";
430 this._actionSequence = this._ACTIONS_NOT_SETUP;
431 this._indexOfNextAction = 0;
434 ActionSequence.prototype = {
435 invokeNextAction: function() {
436 if(this._actionSequence === this._ACTIONS_NOT_SETUP) {
439 if(this._indexOfNextAction >= this._actionSequence.length) {
440 throw new Mock4JSException("no more values to return");
442 var action = this._actionSequence[this._indexOfNextAction];
443 this._indexOfNextAction++;
444 return action.invoke();
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");
455 this._actionSequence.push(arguments[i]);
460 function StubActionSequence() {
461 this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP";
462 this._actionSequence = this._ACTIONS_NOT_SETUP;
463 this._indexOfNextAction = 0;
466 StubActionSequence.prototype = {
467 invokeNextAction: function() {
468 if(this._actionSequence === this._ACTIONS_NOT_SETUP) {
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();
474 if(this._indexOfNextAction >= this._actionSequence.length) {
475 throw new Mock4JSException("no more values to return");
477 var action = this._actionSequence[this._indexOfNextAction];
478 this._indexOfNextAction++;
479 return action.invoke();
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");
490 this._actionSequence.push(arguments[i]);
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)'");
503 this._mockedType = mockedType.prototype;
504 this._expectedCallCount;
505 this._isRecordingExpectations = false;
506 this._expectedInvocations = [];
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;
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);
531 expects: function(expectedCallCount) {
532 this._expectedCallCount = expectedCallCount;
533 this._isRecordingExpectations = true;
534 this._isRecordingStubs = false;
539 this._isRecordingExpectations = false;
540 this._isRecordingStubs = true;
545 for(var i=0; i<this._expectedInvocations.length; i++) {
546 var expectedInvocation = this._expectedInvocations[i];
548 expectedInvocation.verify();
550 var failMsg = e.message+this._describeMockSetup();
551 throw new Mock4JSException(failMsg);
556 _isPublicMethod: function(mockedType, property) {
558 var isMethod = typeof(mockedType[property]) == 'function';
559 var isPublic = property.charAt(0) != "_";
560 return isMethod && isPublic;
566 _createExpectationRecordingMethod: function(methodName) {
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];
574 expectedArgs[i] = new MatchExactly(arguments[i]);
578 // create stub or expected invocation
579 var expectedInvocation;
580 if(this._isRecordingExpectations) {
581 expectedInvocation = new ExpectedInvocation(methodName, expectedArgs, this._expectedCallCount);
583 expectedInvocation = new StubInvocation(methodName, expectedArgs, new StubActionSequence());
586 this._expectedInvocations.push(expectedInvocation);
588 this._isRecordingExpectations = false;
589 this._isRecordingStubs = false;
590 return expectedInvocation;
594 _createMockedMethod: function(methodName) {
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)) {
601 return expectedInvocation.invoked();
603 if(e instanceof Mock4JSException) {
604 throw new Mock4JSException(e.message+this.mock._describeMockSetup());
606 // the user setup the mock to throw a specific error, so don't modify the message
612 var failMsg = "unexpected invocation: "+methodName+"("+Mock4JSUtil.join(arguments)+")"+this.mock._describeMockSetup();
613 throw new Mock4JSException(failMsg);
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();