/** * 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; } }