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 throw new Mock4JSException("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 } 626