Skip to content

Instantly share code, notes, and snippets.

@davemo
Last active December 21, 2015 16:39
Show Gist options
  • Save davemo/6335388 to your computer and use it in GitHub Desktop.
Save davemo/6335388 to your computer and use it in GitHub Desktop.
An example of using a disassociated object to reveal the intent of an API in a test.
# this is a mixin from backbone app that allows callbacks and conventions around attaching them to
# views to work in a couple different ways.
# the specfile included here reveals the intent of how the mixin should be used
def 'mixins.addCallbackConventionTo', (target, receiver) ->
ensureBackboneEventsHaveBeenAddedTo(target)
mixins.mix target, with:
callbackNameFor: (name) -> 'on'+_(name).titleize()
callback: (name, stuff...) ->
receiver?[@callbackNameFor(name)]?.apply(receiver, stuff)
@trigger(name, stuff...)
ensureBackboneEventsHaveBeenAddedTo = (obj) ->
_(obj).extend(Backbone.Events) unless obj.trigger?
# none of the objects in this test are used in the actual system code, they are simply built here to show how the system code
# is expected to behave.
describe "mixins.addCallbackConventionTo", ->
FANCY_MEDICAL_SYMBOL = "⚕"
# the idea here is to create an object that may not even have association to the intended use in the app
# but use it to explore how the mixin API (or, interface) should work and what features it provides
class e911
constructor: (dialer) ->
mixins.addCallbackConventionTo(@, dialer)
dial: ->
@callback('dispatch', FANCY_MEDICAL_SYMBOL)
Given -> @spy = jasmine.createSpy()
sharedExamplesFor "a callback receiver", (receiver) ->
Given -> @subject = new receiver(@spy)
When -> @subject.e911.dial()
Then -> expect(@spy).toHaveBeenCalledWith(FANCY_MEDICAL_SYMBOL)
describe "Use case #1 - caller defines 'onCallback' functions", ->
# in this test, we create a localized object that utilizes our system under test, the e911 class which implements the mixin
class Victim
constructor: (spy) ->
@spy = spy
@e911 = new e911(@)
onDispatch: (msg) -> # a callback can be attached via simply prefixing a function name with "on" right on Victim
@spy(msg)
itBehavesLike "a callback receiver", Victim
describe "Use case #2 - caller passes in anonymous 'onCallback' functions", ->
class Victim
constructor: (spy) ->
@e911 = new e911
onDispatch: (msg) -> # a callback can also be assigned directly onto e911 by providing it as an argument
spy(msg)
itBehavesLike "a callback receiver", Victim
describe "Use case #3 - caller simply binds to 'callback' events", ->
class Victim
constructor: (spy) ->
@e911 = new e911
@e911.on('dispatch', (msg) -> spy(msg)) # the third way, is to allow listening via Backbone.Events to a dispatch event
itBehavesLike "a callback receiver", Victim
# this is a test helper that eliminates some duplication in tests
# and provides a way to better reveal intent of the system under test in the test itself
# inspired by rspec's sharedExamples: https://www.relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples
root = this
knownExamples = {}
root.sharedExamplesFor = (name, examples) ->
throw "shared examples already defined for name \"#{name}\"" if knownExamples[name]?
knownExamples[name] = examples
root.itBehavesLike = (name, args...) ->
throw "No examples known with name \"#{name}\"" unless knownExamples[name]?
knownExamples[name](args...)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment