Skip to content
Browse files

Implemented check for call counts

  • Loading branch information...
1 parent 67c96d2 commit 65bc33c340e2978e1d2156316e87d2783c5bb416 @dhasenan committed
Showing with 240 additions and 29 deletions.
  1. +3 −3 README.md
  2. +70 −25 lib/maryjane.coffee
  3. +167 −1 test/maryjanetests.coffee
View
6 README.md
@@ -94,12 +94,14 @@ Just use the same process, but with `verify` rather than `when`:
This will require that someone called `well.consume(apple)` at some point.
-Currently, you can't check the number of times this was called. When this is implemented, it will appear as:
+You can also verify the number of times something should be called:
verify(well, never).consume(apple);
verify(well, once).consume(apple);
verify(well, times(17)).consume(apple);
verify(well, atLeast(17)).consume(apple);
+ verify(well, atMost(3)).consume(apple);
+ verify(well, between(3, 7)).consume(apple);
Currently, you can't check that a mock had no interactions or no unverified interactions. When this is available, it will appear as:
@@ -119,8 +121,6 @@ This will replace the `add` function on `math` with one that returns the bitwise
TODO
====
- * Verify number of times a method was called
* verifyNoMoreInteractions, verifyZeroInteractions
- * Multiple expectations for methods (throw the first time, return null the second, return this the third time)
* Ordering
* Argument matchers
View
95 lib/maryjane.coffee
@@ -1,5 +1,10 @@
# MaryJane: a mock object library supporting Arrange Act Assert syntax.
+# Standard mock methods
+# Create a new mock based on:
+# - a prototype of an existing object
+# - an existing object
+# - a constructor
exports.mock = (type) ->
if !type?
throw new Error 'You must provide a type'
@@ -8,6 +13,46 @@ exports.mock = (type) ->
else
new Mock type
+exports.when = (mock) ->
+ onMock mock, (m) -> m._mockInternals.newExpectation()
+
+exports.verify = (mock, times) ->
+ if !times?
+ times = new Range(1, Infinity)
+ onMock mock, (m) -> m._mockInternals.verify(times)
+
+class Range
+ constructor: (@min, @max) ->
+ if !@max?
+ @max = @min
+
+ toString: ->
+ if @min == @max
+ return 'exactly ' + @min + ' times'
+ if @min <= 0
+ if @max == Infinity
+ return 'any number of times'
+ else
+ return 'at most ' + @max + ' times'
+ else
+ if @max == Infinity
+ return 'at least ' + @min + ' times'
+ else
+ return 'between ' + @min + ' and ' + @max + ' times'
+
+ match: (point) -> point <= @max and point >= @min
+
+
+# Repeat functions
+exports.times = (min, max) -> new Range(min, max)
+exports.never = new Range(0, 0)
+exports.once = new Range(1, 1)
+exports.twice = new Range(2, 2)
+exports.thrice = new Range(3, 3)
+exports.atLeast = (min) -> new Range(min, Infinity)
+exports.atMost = (max) -> new Range(0, max)
+exports.range = (min, max) -> new Range(min, max)
+
onMock = (mock, cb) ->
if !(mock instanceof Mock)
throw new Error 'You can only use this with mock objects'
@@ -22,19 +67,10 @@ getName = (type) ->
f = f.split('(', 2)[0]
f.replace ' ', ''
-
-exports.when = (mock) ->
- onMock mock, (m) -> m._mockInternals.newExpectation()
-
-exports.verify = (mock) ->
- onMock mock, (m) -> m._mockInternals.verify()
-
-
class MockInternals
constructor: (@type, @mock) ->
if @type == null or @type == undefined
throw new Error 'You must provide a type'
- #console.log 'working on type %s', @type.constructor.toString()
for key, value of @type
addFieldOrMethod(@mock, @type, key)
@@ -55,20 +91,26 @@ class MockInternals
if m?
return m.execute args
- m = new MockOptions(@mock, field, args)
- @unexpectedMethodCalls.push m
+ m = @findCall @unexpectedMethodCalls, field, args
+ if !m?
+ m = new MockOptions(@mock, field, args)
+ @unexpectedMethodCalls.push m
+ m.alreadyRan()
null
check: (field, args) ->
@checking = false
m = @findCall @expectedMethodCalls, field, args
if m?
- if m.count == 0
- @failCheck field, args
+ if ! (m instanceof MockOptions)
+ throw new Error 'malformed recorded expectation'
+ if !@range.match m.count()
+ @failCheck field, args, m
else
m = @findCall @unexpectedMethodCalls, field, args
- if !m?
- @failCheck field, args
+ count = if !m? then 0 else m.count()
+ if !@range.match count
+ @failCheck field, args, m
null
record: (field, args) ->
@@ -83,7 +125,8 @@ class MockInternals
return call
return null
- failCheck: (field, args) ->
+ failCheck: (field, args, match) ->
+ count = if match? then match.count() else 0
argString = '('
first = true
for arg in args
@@ -94,14 +137,15 @@ class MockInternals
argString += arg
argString += ')'
- throw new Error 'Expected ' + @typeName + '.' + field + argString + ' to be called at least once, but it was never called'
+ throw new Error 'Expected ' + @typeName + '.' + field + argString + ' to be called ' + @range.toString() + ', but it was called ' + count + ' times'
newExpectation: ->
@recording = true
@mock
- verify: ->
+ verify: (times) ->
@checking = true
+ @range = times
@mock
class Mock
@@ -115,7 +159,6 @@ addFieldOrMethod = (mock, type, field) ->
if typeof f == 'function'
mock[field] = () ->
t = mock._mockInternals.checkExpectedCall field, arguments
- #console.log 'outer wrapper returning %s', t.toString()
return t
else if type.hasOwnProperty field
mock[field] = type[field]
@@ -131,6 +174,7 @@ class MockOperation
else
return @retval
+expectation_count = 0
class MockOptions
constructor: (@_mock, @_name, @_args) ->
# Use constructor assignment; otherwise the prototype fields
@@ -139,6 +183,7 @@ class MockOptions
@_strict = true
@_ops = []
@_count = 0
+ @_id = expectation_count++
lax: ->
@_strict = false
@@ -159,7 +204,7 @@ class MockOptions
execute: (args) ->
op = null
if @_ops.length == 0
- # Error?
+ @_count++
return null
if @_count > @_ops.length
@@ -170,18 +215,18 @@ class MockOptions
op.execute @_mock, args
matches: (name, args) ->
- #console.log 'checking %s vs expected %s', args, @_args
if (@_args != null)
if (@_strict and @_args.length != args.length)
- #console.log 'strict and wrong number of parameters'
return false
if @_args.length > args.length
- #console.log 'too few parameters'
return false
for i in [0 ... @_args.length]
unless args[i] == @_args[i]
- #console.log "argument %d didn't match; expected %s but got %s", i, @_args[i], args[i]
return false
- #console.log 'nothing for it but to match'
return true
+ alreadyRan: ->
+ @_count++
+
+ count: ->
+ @_count
View
168 test/maryjanetests.coffee
@@ -68,7 +68,7 @@ exports['verify call'] = ->
exports['verify call that was not called'] = ->
mock = mj.mock(new UnderTest())
cb = -> mj.verify(mock).frob 1, 8
- assert.throws cb, (ex) -> ex.message == 'Expected UnderTest.frob(1, 8) to be called at least once, but it was never called'
+ assert.throws cb, (ex) -> ex.message == 'Expected UnderTest.frob(1, 8) to be called at least 1 times, but it was called 0 times'
exports['mock from base function prototype'] = ->
mock = mj.mock(UnderTest.prototype)
@@ -92,3 +92,169 @@ exports['chained expectations'] = ->
assert.eql mock.frob(1, 7), 8
assert.eql mock.frob(1, 7), 18
assert.eql mock.frob(1, 7), 'no thanks'
+
+exports['number of times called'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mj.verify(mock, mj.times(3)).frob(1, 7)
+
+exports['number of times called: never'] = ->
+ mock = mj.mock(new UnderTest())
+ mj.verify(mock, mj.never).frob(1, 7)
+
+exports['number of times called: once'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mj.verify(mock, mj.once).frob(1, 7)
+
+exports['number of times called: twice'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mj.verify(mock, mj.twice).frob(1, 7)
+
+exports['number of times called: thrice'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mj.verify(mock, mj.thrice).frob(1, 7)
+
+exports['number of times called, failure, too few'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.times(3, 3)).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called exactly 3 times, but it was called 2 times'
+
+exports['number of times called, failure, too many'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.times(3)).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called exactly 3 times, but it was called 4 times'
+
+exports['number of times called: once, failure - too few'] = ->
+ mock = mj.mock(new UnderTest())
+ cb = -> mj.verify(mock, mj.once).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called exactly 1 times, but it was called 0 times'
+
+exports['number of times called: twice, failure - too few'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.twice).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called exactly 2 times, but it was called 1 times'
+
+exports['number of times called: thrice, failure - too few'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.thrice).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called exactly 3 times, but it was called 2 times'
+
+exports['number of times called: never, but actually was called'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.never).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called exactly 0 times, but it was called 1 times'
+
+exports['number of times called: once, failure - too many'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.once).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called exactly 1 times, but it was called 2 times'
+
+exports['number of times called: twice, failure - too many'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.twice).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called exactly 2 times, but it was called 3 times'
+
+exports['number of times called: thrice, failure - too many'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.thrice).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called exactly 3 times, but it was called 4 times'
+
+exports['number of times called, at most'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mj.verify(mock, mj.atMost(6)).frob(1, 7)
+
+exports['number of times called, at most, failure'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.atMost(3)).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called at most 3 times, but it was called 4 times'
+
+exports['number of times called, at least'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mj.verify(mock, mj.atLeast(3)).frob(1, 7)
+
+exports['number of times called, at least, failure'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.atLeast(6)).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called at least 6 times, but it was called 4 times'
+
+exports['number of times called, range'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mj.verify(mock, mj.range(3, 5)).frob(1, 7)
+
+exports['number of times called, range, too high'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.range(6, 19)).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called between 6 and 19 times, but it was called 4 times'
+
+exports['number of times called, range, too low'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ mock.frob(1, 7)
+ cb = -> mj.verify(mock, mj.range(1, 3)).frob(1, 7)
+ assert.throws cb, (ex) ->
+ ex.message == 'Expected UnderTest.frob(1, 7) to be called between 1 and 3 times, but it was called 4 times'

0 comments on commit 65bc33c

Please sign in to comment.
Something went wrong with that request. Please try again.