0

I am using the following construct as a Mock object which functions as an object with chainable methods:

PlayerNull =
  find : ->
    populate : ->
      exec : (callback) ->
        callback false, false

In my tests I then substitute the real model for this Mock object and my controller calls each of the functions in turn such as:

Model.find().populate().exec(callback)

As I'm finding myself using this many times, I was curious as to whether I could create a helper function to simplify this (slightly), using a helper function in the following form:

PlayerNull = helper.mockNest ['find', 'populate', 'exec'], (callback) ->
  callback false, false

I've come to the following function code, however this is not working:

exports.mockNest = (func_names, func_final) ->
  func_names.reverse()
  func_next = func_final
  for func_name in func_names
    _func_next = func_next.bind({})
    _next = {}
    _next[func_name] = ->
      _func_next
    func_next = _func_next
  func_next

I've come to the realization that I need to clone func_next each loop or else the reference seems to be maintained and assigning to func_next just seems to alter all previous assignments.

DanH
  • 5,498
  • 4
  • 49
  • 72

2 Answers2

2

Yes, you've identified the problem correctly that the reference is not maintained, it's the classical closure in a loop problem.

However, cloning the function doesn't really help here, your actual problem is

_next[func_name] = ->
  _func_next

which creates a closure over the _func_next variable, which is modified in each loop iteration.

It should rather be

exports.mockNest = (func_names, func_final) ->
  func_names.reverse()
  func_next = func_final
  for func_name in func_names
    next = {}
    next[func_name] = func_next
    func_next = do (_next = next) ->
      () ->
        _next
  func_next
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks very much for explaining the problem and bringing my attention to the `do` keyword, very cool, and got me over the finish line. Your final code didn't work exactly, I'll add an answer and accept this. – DanH Oct 05 '14 at 05:36
  • Oops, you're right it didn't work; but the fix is rather small. In contrast to your code, it even works with an empty `func_names` array now. – Bergi Oct 05 '14 at 10:46
0

Thanks to Bergi's explanation, I came to the following solution:

exports.mockNest = (func_names, func_final) ->

  # Assign the last function to the provided func_final
  func_name_final = func_names.pop()
  func_next = {}
  func_next[func_name_final] = func_final

  # Reverse the array so we build the object from last to first.
  func_names.reverse()
  for func_name in func_names
    ignore = do (func_name, _func_next = func_next) ->
      func_next = {}
      func_next[func_name] = ->
        _func_next
      return
  func_next

To explain the changes, I needed to preserve the supplied func_final, so I had to assign before the loop.

Also, I needed to assign the do block to variable ignore or else Coffeescript compiled the function to different location where changes made to func_next were made out-of-order. Assigning ensures the placement of the do block.

DanH
  • 5,498
  • 4
  • 49
  • 72