| /** |
| * Mock4JS 0.2 |
| * http://mock4js.sourceforge.net/ |
| */ |
| |
| Mock4JS = { |
| _mocksToVerify: [], |
| _convertToConstraint: function(constraintOrValue) { |
| if(constraintOrValue.argumentMatches) { |
| return constraintOrValue; // it's already an ArgumentMatcher |
| } else { |
| return new MatchExactly(constraintOrValue); // default to eq(...) |
| } |
| }, |
| addMockSupport: function(object) { |
| // mock creation |
| object.mock = function(mockedType) { |
| if(!mockedType) { |
| throw new Mock4JSException("Cannot create mock: type to mock cannot be found or is null"); |
| } |
| var newMock = new Mock(mockedType); |
| Mock4JS._mocksToVerify.push(newMock); |
| return newMock; |
| } |
| |
| // syntactic sugar for expects() |
| object.once = function() { |
| return new CallCounter(1); |
| } |
| object.never = function() { |
| return new CallCounter(0); |
| } |
| object.exactly = function(expectedCallCount) { |
| return new CallCounter(expectedCallCount); |
| } |
| object.atLeastOnce = function() { |
| return new InvokeAtLeastOnce(); |
| } |
| |
| // syntactic sugar for argument expectations |
| object.ANYTHING = new MatchAnything(); |
| object.NOT_NULL = new MatchAnythingBut(new MatchExactly(null)); |
| object.NOT_UNDEFINED = new MatchAnythingBut(new MatchExactly(undefined)); |
| object.eq = function(expectedValue) { |
| return new MatchExactly(expectedValue); |
| } |
| object.not = function(valueNotExpected) { |
| var argConstraint = Mock4JS._convertToConstraint(valueNotExpected); |
| return new MatchAnythingBut(argConstraint); |
| } |
| object.and = function() { |
| var constraints = []; |
| for(var i=0; i<arguments.length; i++) { |
| constraints[i] = Mock4JS._convertToConstraint(arguments[i]); |
| } |
| return new MatchAllOf(constraints); |
| } |
| object.or = function() { |
| var constraints = []; |
| for(var i=0; i<arguments.length; i++) { |
| constraints[i] = Mock4JS._convertToConstraint(arguments[i]); |
| } |
| return new MatchAnyOf(constraints); |
| } |
| object.stringContains = function(substring) { |
| return new MatchStringContaining(substring); |
| } |
| |
| // syntactic sugar for will() |
| object.returnValue = function(value) { |
| return new ReturnValueAction(value); |
| } |
| object.throwException = function(exception) { |
| return new ThrowExceptionAction(exception); |
| } |
| }, |
| clearMocksToVerify: function() { |
| Mock4JS._mocksToVerify = []; |
| }, |
| verifyAllMocks: function() { |
| for(var i=0; i<Mock4JS._mocksToVerify.length; i++) { |
| Mock4JS._mocksToVerify[i].verify(); |
| } |
| } |
| } |
| |
| Mock4JSUtil = { |
| hasFunction: function(obj, methodName) { |
| return typeof obj == 'object' && typeof obj[methodName] == 'function'; |
| }, |
| join: function(list) { |
| var result = ""; |
| for(var i=0; i<list.length; i++) { |
| var item = list[i]; |
| if(Mock4JSUtil.hasFunction(item, "describe")) { |
| result += item.describe(); |
| } |
| else if(typeof list[i] == 'string') { |
| result += "\""+list[i]+"\""; |
| } else { |
| result += list[i]; |
| } |
| |
| if(i<list.length-1) result += ", "; |
| } |
| return result; |
| } |
| } |
| |
| Mock4JSException = function(message) { |
| this.message = message; |
| } |
| |
| Mock4JSException.prototype = { |
| toString: function() { |
| return this.message; |
| } |
| } |
| |
| /** |
| * Assert function that makes use of the constraint methods |
| */ |
| assertThat = function(expected, argumentMatcher) { |
| if(!argumentMatcher.argumentMatches(expected)) { |
| throw new Mock4JSException("Expected '"+expected+"' to be "+argumentMatcher.describe()); |
| } |
| } |
| |
| /** |
| * CallCounter |
| */ |
| function CallCounter(expectedCount) { |
| this._expectedCallCount = expectedCount; |
| this._actualCallCount = 0; |
| } |
| |
| CallCounter.prototype = { |
| addActualCall: function() { |
| this._actualCallCount++; |
| if(this._actualCallCount > this._expectedCallCount) { |
| throw new Mock4JSException("unexpected invocation"); |
| } |
| }, |
| |
| verify: function() { |
| if(this._actualCallCount < this._expectedCallCount) { |
| throw new Mock4JSException("expected method was not invoked the expected number of times"); |
| } |
| }, |
| |
| describe: function() { |
| if(this._expectedCallCount == 0) { |
| return "not expected"; |
| } else if(this._expectedCallCount == 1) { |
| var msg = "expected once"; |
| if(this._actualCallCount >= 1) { |
| msg += " and has been invoked"; |
| } |
| return msg; |
| } else { |
| var msg = "expected "+this._expectedCallCount+" times"; |
| if(this._actualCallCount > 0) { |
| msg += ", invoked "+this._actualCallCount + " times"; |
| } |
| return msg; |
| } |
| } |
| } |
| |
| function InvokeAtLeastOnce() { |
| this._hasBeenInvoked = false; |
| } |
| |
| InvokeAtLeastOnce.prototype = { |
| addActualCall: function() { |
| this._hasBeenInvoked = true; |
| }, |
| |
| verify: function() { |
| if(this._hasBeenInvoked === false) { |
| throw new Mock4JSException(describe()); |
| } |
| }, |
| |
| describe: function() { |
| var desc = "expected at least once"; |
| if(this._hasBeenInvoked) desc+=" and has been invoked"; |
| return desc; |
| } |
| } |
| |
| /** |
| * ArgumentMatchers |
| */ |
| |
| function MatchExactly(expectedValue) { |
| this._expectedValue = expectedValue; |
| } |
| |
| MatchExactly.prototype = { |
| argumentMatches: function(actualArgument) { |
| if(this._expectedValue instanceof Array) { |
| if(!(actualArgument instanceof Array)) return false; |
| if(this._expectedValue.length != actualArgument.length) return false; |
| for(var i=0; i<this._expectedValue.length; i++) { |
| if(this._expectedValue[i] != actualArgument[i]) return false; |
| } |
| return true; |
| } else { |
| return this._expectedValue == actualArgument; |
| } |
| }, |
| describe: function() { |
| if(typeof this._expectedValue == "string") { |
| return "eq(\""+this._expectedValue+"\")"; |
| } else { |
| return "eq("+this._expectedValue+")"; |
| } |
| } |
| } |
| |
| function MatchAnything() { |
| } |
| |
| MatchAnything.prototype = { |
| argumentMatches: function(actualArgument) { |
| return true; |
| }, |
| describe: function() { |
| return "ANYTHING"; |
| } |
| } |
| |
| function MatchAnythingBut(matcherToNotMatch) { |
| this._matcherToNotMatch = matcherToNotMatch; |
| } |
| |
| MatchAnythingBut.prototype = { |
| argumentMatches: function(actualArgument) { |
| return !this._matcherToNotMatch.argumentMatches(actualArgument); |
| }, |
| describe: function() { |
| return "not("+this._matcherToNotMatch.describe()+")"; |
| } |
| } |
| |
| function MatchAllOf(constraints) { |
| this._constraints = constraints; |
| } |
| |
| |
| MatchAllOf.prototype = { |
| argumentMatches: function(actualArgument) { |
| for(var i=0; i<this._constraints.length; i++) { |
| var constraint = this._constraints[i]; |
| if(!constraint.argumentMatches(actualArgument)) return false; |
| } |
| return true; |
| }, |
| describe: function() { |
| return "and("+Mock4JSUtil.join(this._constraints)+")"; |
| } |
| } |
| |
| function MatchAnyOf(constraints) { |
| this._constraints = constraints; |
| } |
| |
| MatchAnyOf.prototype = { |
| argumentMatches: function(actualArgument) { |
| for(var i=0; i<this._constraints.length; i++) { |
| var constraint = this._constraints[i]; |
| if(constraint.argumentMatches(actualArgument)) return true; |
| } |
| return false; |
| }, |
| describe: function() { |
| return "or("+Mock4JSUtil.join(this._constraints)+")"; |
| } |
| } |
| |
| |
| function MatchStringContaining(stringToLookFor) { |
| this._stringToLookFor = stringToLookFor; |
| } |
| |
| MatchStringContaining.prototype = { |
| argumentMatches: function(actualArgument) { |
| if(typeof actualArgument != 'string') throw new Mock4JSException("stringContains() must be given a string, actually got a "+(typeof actualArgument)); |
| return (actualArgument.indexOf(this._stringToLookFor) != -1); |
| }, |
| describe: function() { |
| return "a string containing \""+this._stringToLookFor+"\""; |
| } |
| } |
| |
| |
| /** |
| * StubInvocation |
| */ |
| function StubInvocation(expectedMethodName, expectedArgs, actionSequence) { |
| this._expectedMethodName = expectedMethodName; |
| this._expectedArgs = expectedArgs; |
| this._actionSequence = actionSequence; |
| } |
| |
| StubInvocation.prototype = { |
| matches: function(invokedMethodName, invokedMethodArgs) { |
| if (invokedMethodName != this._expectedMethodName) { |
| return false; |
| } |
| |
| if (invokedMethodArgs.length != this._expectedArgs.length) { |
| return false; |
| } |
| |
| for(var i=0; i<invokedMethodArgs.length; i++) { |
| var expectedArg = this._expectedArgs[i]; |
| var invokedArg = invokedMethodArgs[i]; |
| if(!expectedArg.argumentMatches(invokedArg)) { |
| return false; |
| } |
| } |
| |
| return true; |
| }, |
| |
| invoked: function() { |
| try { |
| return this._actionSequence.invokeNextAction(); |
| } catch(e) { |
| if(e instanceof Mock4JSException) { |
| throw new Mock4JSException(this.describeInvocationNameAndArgs()+" - "+e.message); |
| } else { |
| throw e; |
| } |
| } |
| }, |
| |
| will: function() { |
| this._actionSequence.addAll.apply(this._actionSequence, arguments); |
| }, |
| |
| describeInvocationNameAndArgs: function() { |
| return this._expectedMethodName+"("+Mock4JSUtil.join(this._expectedArgs)+")"; |
| }, |
| |
| describe: function() { |
| return "stub: "+this.describeInvocationNameAndArgs(); |
| }, |
| |
| verify: function() { |
| } |
| } |
| |
| /** |
| * ExpectedInvocation |
| */ |
| function ExpectedInvocation(expectedMethodName, expectedArgs, expectedCallCounter) { |
| this._stubInvocation = new StubInvocation(expectedMethodName, expectedArgs, new ActionSequence()); |
| this._expectedCallCounter = expectedCallCounter; |
| } |
| |
| ExpectedInvocation.prototype = { |
| matches: function(invokedMethodName, invokedMethodArgs) { |
| try { |
| return this._stubInvocation.matches(invokedMethodName, invokedMethodArgs); |
| } catch(e) { |
| throw new Mock4JSException("method "+this._stubInvocation.describeInvocationNameAndArgs()+": "+e.message); |
| } |
| }, |
| |
| invoked: function() { |
| try { |
| this._expectedCallCounter.addActualCall(); |
| } catch(e) { |
| throw new Mock4JSException(e.message+": "+this._stubInvocation.describeInvocationNameAndArgs()); |
| } |
| return this._stubInvocation.invoked(); |
| }, |
| |
| will: function() { |
| this._stubInvocation.will.apply(this._stubInvocation, arguments); |
| }, |
| |
| describe: function() { |
| return this._expectedCallCounter.describe()+": "+this._stubInvocation.describeInvocationNameAndArgs(); |
| }, |
| |
| verify: function() { |
| try { |
| this._expectedCallCounter.verify(); |
| } catch(e) { |
| throw new Mock4JSException(e.message+": "+this._stubInvocation.describeInvocationNameAndArgs()); |
| } |
| } |
| } |
| |
| /** |
| * MethodActions |
| */ |
| function ReturnValueAction(valueToReturn) { |
| this._valueToReturn = valueToReturn; |
| } |
| |
| ReturnValueAction.prototype = { |
| invoke: function() { |
| return this._valueToReturn; |
| }, |
| describe: function() { |
| return "returns "+this._valueToReturn; |
| } |
| } |
| |
| function ThrowExceptionAction(exceptionToThrow) { |
| this._exceptionToThrow = exceptionToThrow; |
| } |
| |
| ThrowExceptionAction.prototype = { |
| invoke: function() { |
| throw this._exceptionToThrow; |
| }, |
| describe: function() { |
| return "throws "+this._exceptionToThrow; |
| } |
| } |
| |
| function ActionSequence() { |
| this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP"; |
| this._actionSequence = this._ACTIONS_NOT_SETUP; |
| this._indexOfNextAction = 0; |
| } |
| |
| ActionSequence.prototype = { |
| invokeNextAction: function() { |
| if(this._actionSequence === this._ACTIONS_NOT_SETUP) { |
| return; |
| } else { |
| if(this._indexOfNextAction >= this._actionSequence.length) { |
| throw new Mock4JSException("no more values to return"); |
| } else { |
| var action = this._actionSequence[this._indexOfNextAction]; |
| this._indexOfNextAction++; |
| return action.invoke(); |
| } |
| } |
| }, |
| |
| addAll: function() { |
| this._actionSequence = []; |
| for(var i=0; i<arguments.length; i++) { |
| if(typeof arguments[i] != 'object' && arguments[i].invoke === undefined) { |
| throw new Error("cannot add a method action that does not have an invoke() method"); |
| } |
| this._actionSequence.push(arguments[i]); |
| } |
| } |
| } |
| |
| function StubActionSequence() { |
| this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP"; |
| this._actionSequence = this._ACTIONS_NOT_SETUP; |
| this._indexOfNextAction = 0; |
| } |
| |
| StubActionSequence.prototype = { |
| invokeNextAction: function() { |
| if(this._actionSequence === this._ACTIONS_NOT_SETUP) { |
| return; |
| } else if(this._actionSequence.length == 1) { |
| // if there is only one method action, keep doing that on every invocation |
| return this._actionSequence[0].invoke(); |
| } else { |
| if(this._indexOfNextAction >= this._actionSequence.length) { |
| throw new Mock4JSException("no more values to return"); |
| } else { |
| var action = this._actionSequence[this._indexOfNextAction]; |
| this._indexOfNextAction++; |
| return action.invoke(); |
| } |
| } |
| }, |
| |
| addAll: function() { |
| this._actionSequence = []; |
| for(var i=0; i<arguments.length; i++) { |
| if(typeof arguments[i] != 'object' && arguments[i].invoke === undefined) { |
| throw new Error("cannot add a method action that does not have an invoke() method"); |
| } |
| this._actionSequence.push(arguments[i]); |
| } |
| } |
| } |
| |
| |
| /** |
| * Mock |
| */ |
| function Mock(mockedType) { |
| if(mockedType === undefined || mockedType.prototype === undefined) { |
| 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)'"); |
| } |
| this._mockedType = mockedType.prototype; |
| this._expectedCallCount; |
| this._isRecordingExpectations = false; |
| this._expectedInvocations = []; |
| |
| // setup proxy |
| var IntermediateClass = new Function(); |
| IntermediateClass.prototype = mockedType.prototype; |
| var ChildClass = new Function(); |
| ChildClass.prototype = new IntermediateClass(); |
| this._proxy = new ChildClass(); |
| this._proxy.mock = this; |
| |
| for(property in mockedType.prototype) { |
| if(this._isPublicMethod(mockedType.prototype, property)) { |
| var publicMethodName = property; |
| this._proxy[publicMethodName] = this._createMockedMethod(publicMethodName); |
| this[publicMethodName] = this._createExpectationRecordingMethod(publicMethodName); |
| } |
| } |
| } |
| |
| Mock.prototype = { |
| |
| proxy: function() { |
| return this._proxy; |
| }, |
| |
| expects: function(expectedCallCount) { |
| this._expectedCallCount = expectedCallCount; |
| this._isRecordingExpectations = true; |
| this._isRecordingStubs = false; |
| return this; |
| }, |
| |
| stubs: function() { |
| this._isRecordingExpectations = false; |
| this._isRecordingStubs = true; |
| return this; |
| }, |
| |
| verify: function() { |
| for(var i=0; i<this._expectedInvocations.length; i++) { |
| var expectedInvocation = this._expectedInvocations[i]; |
| try { |
| expectedInvocation.verify(); |
| } catch(e) { |
| var failMsg = e.message+this._describeMockSetup(); |
| throw new Mock4JSException(failMsg); |
| } |
| } |
| }, |
| |
| _isPublicMethod: function(mockedType, property) { |
| try { |
| var isMethod = typeof(mockedType[property]) == 'function'; |
| var isPublic = property.charAt(0) != "_"; |
| return isMethod && isPublic; |
| } catch(e) { |
| return false; |
| } |
| }, |
| |
| _createExpectationRecordingMethod: function(methodName) { |
| return function() { |
| // ensure all arguments are instances of ArgumentMatcher |
| var expectedArgs = []; |
| for(var i=0; i<arguments.length; i++) { |
| if(arguments[i] !== null && arguments[i] !== undefined && arguments[i].argumentMatches) { |
| expectedArgs[i] = arguments[i]; |
| } else { |
| expectedArgs[i] = new MatchExactly(arguments[i]); |
| } |
| } |
| |
| // create stub or expected invocation |
| var expectedInvocation; |
| if(this._isRecordingExpectations) { |
| expectedInvocation = new ExpectedInvocation(methodName, expectedArgs, this._expectedCallCount); |
| } else { |
| expectedInvocation = new StubInvocation(methodName, expectedArgs, new StubActionSequence()); |
| } |
| |
| this._expectedInvocations.push(expectedInvocation); |
| |
| this._isRecordingExpectations = false; |
| this._isRecordingStubs = false; |
| return expectedInvocation; |
| } |
| }, |
| |
| _createMockedMethod: function(methodName) { |
| return function() { |
| // go through expectation list backwards to ensure later expectations override earlier ones |
| for(var i=this.mock._expectedInvocations.length-1; i>=0; i--) { |
| var expectedInvocation = this.mock._expectedInvocations[i]; |
| if(expectedInvocation.matches(methodName, arguments)) { |
| try { |
| return expectedInvocation.invoked(); |
| } catch(e) { |
| if(e instanceof Mock4JSException) { |
| throw new Mock4JSException(e.message+this.mock._describeMockSetup()); |
| } else { |
| // the user setup the mock to throw a specific error, so don't modify the message |
| throw e; |
| } |
| } |
| } |
| } |
| var failMsg = "unexpected invocation: "+methodName+"("+Mock4JSUtil.join(arguments)+")"+this.mock._describeMockSetup(); |
| throw new Mock4JSException(failMsg); |
| }; |
| }, |
| |
| _describeMockSetup: function() { |
| var msg = "\nAllowed:"; |
| for(var i=0; i<this._expectedInvocations.length; i++) { |
| var expectedInvocation = this._expectedInvocations[i]; |
| msg += "\n" + expectedInvocation.describe(); |
| } |
| return msg; |
| } |
| } |