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();