Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added README, extra mocking options

You can mock from a constructor or a prototype or an object.
Mocks include fields.
  • Loading branch information...
commit de151e2c35d1216cc8aaed0dd8d86f388bf57fb6 1 parent 4882911
@dhasenan authored
Showing with 305 additions and 127 deletions.
  1. +74 −0 README.md
  2. +164 −106 lib/maryjane.coffee
  3. +67 −21 test/maryjanetests.coffee
View
74 README.md
@@ -0,0 +1,74 @@
+MaryJane
+========
+A mock objects library for javascript.
+
+Platform Support
+================
+I don't intend to exclude any platforms, but my development is all on Linux with Node.js. I will accept patches to fix broken functionality in any other environment, if they don't break Node.js.
+
+I might extend platform support in the future, with the next likely target being Jurassic, but this is far from guaranteed.
+
+Despite these warnings about platform support, there shouldn't be anything strange about MaryJane that would prevent you from using it in any reasonably compliant environment.
+
+A Note on Types
+===============
+MaryJane takes a rather strong stance toward types. Specifically, you should deal with types that exist. If you dynamically add methods to your type, this should be accomplished in some global setup.
+
+Regardless, there's almost no way to properly mock an object method that will be assigned during the course of a test. (Implementing that would eliminate any chance of multiplatform support.) So just don't, kay?
+
+Usage
+=====
+
+Creating Mocks
+--------------
+MaryJane will create a mock from:
+ * an existing object
+ * an object prototype
+ * a constructor
+
+It will mock its methods and make a shallow copy of its fields. It will *not* run the constructor, even if you pass a constructor or a prototype. Since JavaScript objects typically get instance fields from the constructor, it's recommended that you pass in a newly constructed instance, such as: `MaryJane.mock(new ClassUnderTest())`. (This usage will call a constructor, but it isn't Mary Jane doing so.)
+
+There's not much point in using an object prototype. It's identical to passing the constructor.
+
+To create a mock:
+`require('maryjane');
+
+var mock1 = mock(new MyObject());
+var mock2 = mock(ObjectWithUntrustedConstructor);
+var mock3 = mock(ObjectWithUntrustedConstructor.prototype);
+`
+
+Using Mocks
+-----------
+MaryJane uses the Arrange-Act-Assert system. Let's say you have a function that takes an apple from a tree and chucks it down a well:
+`
+var iHateApples = function(appleTree, well)
+{
+ var apple = appleTree.pluckApple();
+ well.consume(apple);
+}
+`
+
+Let's look at the test:
+`
+// Arrange -- how does the world look and act?
+var appleTree = mock(new AppleTree());
+var well = mock(new Well());
+var apple = new Apple();
+when(appleTree).pluckApple().thenReturn(apple);
+
+// Act: run the test method
+iHateApples(appleTree, well);
+
+// Assert: what happened?
+verify(well).consume(apple);
+`
+
+
+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
270 lib/maryjane.coffee
@@ -1,117 +1,175 @@
# MaryJane: a mock object library supporting Arrange Act Assert syntax.
exports.mock = (type) ->
- if !type?
- throw new Error 'You must provide a type'
- new Mock(type)
+ if !type?
+ throw new Error 'You must provide a type'
+ if typeof type == 'function'
+ new Mock type.prototype
+ else
+ new Mock type
+
+onMock = (mock, cb) ->
+ if !(mock instanceof Mock)
+ throw new Error 'You can only use this with mock objects'
+ if mock._mockInternals?
+ return cb mock
+ else
+ throw new Error 'Malformed mock object'
+
+getName = (type) ->
+ f = type.constructor.toString()
+ f = f.split(' ', 2)[1]
+ f = f.split('(', 2)[0]
+ f.replace ' ', ''
+
exports.when = (mock) ->
- if !(mock instanceof Mock)
- throw new Error 'You can only use this with mock objects'
- if mock._mockInternals?
- return mock._mockInternals.newExpectation()
- else
- throw new Error 'Malformed mock object'
+ 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
- addIfMethod(@mock, @type, key)
- @expectedMethodCalls = []
- @unexpectedMethodCalls = []
- @recording = false
-
- checkExpectedCall: (field, args) ->
- if @recording
- m = new MockOptions(@mock, field, args)
- @expectedMethodCalls.push m
- @recording = false
- return m
- for call in @expectedMethodCalls
- if call.matches(field, args)
- return call.execute(field, args)
- m = new MockOptions(@mock, field, args)
- @unexpectedMethodCalls.push m
- null
-
- newExpectation: ->
- @recording = true
- @mock
+ 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)
+
+ @expectedMethodCalls = []
+ @unexpectedMethodCalls = []
+ @recording = false
+ @checking = false
+ @typeName = getName(@type)
+
+ checkExpectedCall: (field, args) ->
+ if @recording
+ return @record field, args
+
+ if @checking
+ return @check field, args
+
+ m = @findCall @expectedMethodCalls, field, args
+ if m?
+ return m.execute args
+
+ m = new MockOptions(@mock, field, args)
+ @unexpectedMethodCalls.push m
+ null
+
+ check: (field, args) ->
+ @checking = false
+ m = @findCall @expectedMethodCalls, field, args
+ if m?
+ if m.count == 0
+ @failCheck field, args
+ else
+ m = @findCall @unexpectedMethodCalls, field, args
+ if !m?
+ @failCheck field, args
+ null
+
+ record: (field, args) ->
+ @recording = false
+ m = new MockOptions(@mock, field, args)
+ @expectedMethodCalls.push m
+ m
+
+ findCall: (list, field, args) ->
+ for call in list
+ if call.matches field, args
+ return call
+ return null
+
+ failCheck: (field, args) ->
+ argString = '('
+ first = true
+ for arg in args
+ if first
+ first = false
+ else
+ argString += ', '
+ argString += arg
+ argString += ')'
+
+ throw new Error 'Expected ' + @typeName + '.' + field + argString + ' to be called at least once, but it was never called'
+
+ newExpectation: ->
+ @recording = true
+ @mock
+
+ verify: ->
+ @checking = true
+ @mock
class Mock
- constructor: (type) ->
- if type == null or type == undefined
- throw new Error 'You must provide a type'
- @_mockInternals = new MockInternals(type, @)
-
-addIfMethod = (mock, type, field) ->
- f = type[field]
- if typeof f == 'function'
- mock[field] = () ->
- t = mock._mockInternals.checkExpectedCall field, arguments
- #console.log 'outer wrapper returning %s', t.toString()
- return t
+ constructor: (type) ->
+ if type == null or type == undefined
+ throw new Error 'You must provide a type'
+ @_mockInternals = new MockInternals(type, @)
+
+addFieldOrMethod = (mock, type, field) ->
+ f = 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]
class MockOptions
- constructor: (@_mock, @_name, @_args) ->
- # Use constructor assignment; otherwise the prototype fields
- # leak and you end up setting all mocks ever to strict rather
- # than just this one
- @_strict = true
- @_returns = null
- @_ex = null
- @_count = 0
- @_userFunc = null
-
- lax: ->
- @_strict = false
- return @
- thenThrow: (ex) ->
- @_ex = ex
- return @
- thenReturn: (value) ->
- @_returns = value
- return @
- check: (fn) ->
- @_userFunc = fn
- return @
-
- execute: (args) ->
- @_count++
- if (@_userFunc != null)
- #console.log 'calling user func'
- return @_userFunc(args)
- if (@_ex != null)
- #console.log 'throwing exception'
- throw @_ex
- if @_returns != null
- #console.log 'returning %s', @_returns.toString()
- return @_returns
- return null
-
- throwUnexpectedMethod: (args) ->
- throw new UnexpectedMethodCallError('Unexpected method call: ' + @_name + '(' + args + ')\nExpected: ' + @_name + '(' + @_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
-
-
-class UnexpectedMethodCallError extends Error
- constructor: (msg) ->
- super(msg)
+ constructor: (@_mock, @_name, @_args) ->
+ # Use constructor assignment; otherwise the prototype fields
+ # leak and you end up setting all mocks ever to strict rather
+ # than just this one
+ @_strict = true
+ @_returns = null
+ @_ex = null
+ @_count = 0
+ @_userFunc = null
+
+ lax: ->
+ @_strict = false
+ return @
+ thenThrow: (ex) ->
+ @_ex = ex
+ return @
+ thenReturn: (value) ->
+ @_returns = value
+ return @
+ thenDo: (fn) ->
+ @_userFunc = fn
+ return @
+
+ execute: (args) ->
+ @_count++
+ if (@_userFunc != null)
+ #console.log 'calling user func'
+ return @_userFunc(args)
+ if (@_ex != null)
+ #console.log 'throwing exception'
+ throw @_ex
+ if @_returns != null
+ #console.log 'returning %s', @_returns.toString()
+ return @_returns
+ return null
+
+ 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
+
View
88 test/maryjanetests.coffee
@@ -2,37 +2,83 @@ mj = require 'maryjane'
assert = require 'assert'
class UnderTest
- frob: (a, b) ->
- throw 'DIVIDE BY CUCUMBER ERROR'
+ frob: (a, b) ->
+ throw 'DIVIDE BY CUCUMBER ERROR'
- fizz: (a) ->
- a + 17
+ fizz: (a) ->
+ a + 17
+
+class HasFields
+ constructor: ->
+ @foo = 17
+ @bar = 'drinky winky!'
exports['regurgitate an expected answer'] = ->
- mock = mj.mock(new UnderTest())
- mj.when(mock).frob(1, 7).thenReturn 15
- assert.eql mock.frob(1, 7), 15
+ mock = mj.mock(new UnderTest())
+ mj.when(mock).frob(1, 7).thenReturn 15
+ assert.eql mock.frob(1, 7), 15
exports['leave the original type alone'] = ->
- b = new UnderTest()
- assert.throws((-> b.frob(1, 7)), 'DIVIDE BY CUCUMBER ERROR')
+ a = mj.mock(new UnderTest())
+ mj.when(a).frob(1, 7).thenReturn 15
+ b = new UnderTest()
+ assert.throws((-> b.frob(1, 7)), 'DIVIDE BY CUCUMBER ERROR')
exports['return null by default'] = ->
- b = mj.mock(new UnderTest())
- assert.eql b._mockInternals.expectedMethodCalls.length, 0
- assert.isNull b.frob(1, 7)
+ b = mj.mock(new UnderTest())
+ assert.eql b._mockInternals.expectedMethodCalls.length, 0
+ assert.isNull b.frob(1, 7)
exports['wrong arguments ignored'] = ->
- mock = mj.mock(new UnderTest())
- mj.when(mock).frob(1, 7).thenReturn 15
- assert.isNull mock.frob(1, 8)
+ mock = mj.mock(new UnderTest())
+ mj.when(mock).frob(1, 7).thenReturn 15
+ assert.isNull mock.frob(1, 8)
exports['not strict and i didn\'t specify one argument'] = ->
- mock = mj.mock(new UnderTest())
- mj.when(mock).frob(1).lax().thenReturn 15
- assert.eql mock.frob(1, 8), 15
+ mock = mj.mock(new UnderTest())
+ mj.when(mock).frob(1).lax().thenReturn 15
+ assert.eql mock.frob(1, 8), 15
exports['strict and i didn\'t specify one argument'] = ->
- mock = mj.mock(new UnderTest())
- mj.when(mock).frob(1).thenReturn 15
- assert.eql mock.frob(1, 8), null
+ mock = mj.mock(new UnderTest())
+ mj.when(mock).frob(1).thenReturn 15
+ assert.eql mock.frob(1, 8), null
+
+exports['throw an exception'] = ->
+ mock = mj.mock(new UnderTest())
+ ex = "who's the what now?"
+ mj.when(mock).frob(1, 8).thenThrow ex
+ assert.throws((-> mock.frob(1, 8)), ex)
+
+exports['user callback'] = ->
+ mock = mj.mock(new UnderTest())
+ count = 0
+ mj.when(mock).frob(1, 8).thenDo -> count++
+ mock.frob 1, 8
+ assert.eql count, 1
+
+exports['verify call'] = ->
+ mock = mj.mock(new UnderTest())
+ mock.frob 1, 8
+ mj.verify(mock).frob 1, 8
+
+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'
+
+exports['mock from base function prototype'] = ->
+ mock = mj.mock(UnderTest.prototype)
+ mock.frob 1, 8
+ mj.verify(mock).frob 1, 8
+
+exports['mock from base function'] = ->
+ mock = mj.mock(UnderTest)
+ mock.frob 1, 8
+ mj.verify(mock).frob 1, 8
+
+exports['mock copies fields'] = ->
+ h = new HasFields()
+ mock = mj.mock(h)
+ assert.eql mock.foo, h.foo
+ assert.eql mock.bar, h.bar
Please sign in to comment.
Something went wrong with that request. Please try again.