aroma-mock
CoffeeScript automatic Mock objects with Scenarios: test *exactly* what you want
Last updated 5 years ago by haarcuba .
GPL · Repository · Bugs · Original npm · Tarball · package.json
$ cnpm install aroma-mock 
SYNC missed versions from official npm registry.

AromaMock - Mocking Framework for JavaScript and CoffeeScript

Aroma is a CoffeeScript Mock Framework. This of course means that it can be used to test and develop JavaScript, but in this document, I will address CoffeeScript, because I like it better. It provides a useful scenario-expectations construct and easy, on the fly mock object generation.

The scenario verifies the exact expected function call sequence including the function arguments. Scenarios are specified before the code is run, making the tests well organized, readable and more explicit.

The scenario abstraction verifies that things happen exactly as specified: i.e. we do not only verify that some mock function was called with specific paramenter, we verify that it has not been called more times that it should have been, with some other parameters.

Original concepts were carried over from Voodoo-Mock, a C++/Python unit test framework.

Installation

$ npm install aroma-mock

You will also probably want to

$ npm install mocha coffee-script

Running CoffeeScript Tests with Mocha

In the following examples I use Mocha for a test runner. In order for Mocha to work with CoffeeScript you must use it like this:

$ mocha --compilers coffee:coffee-script/register [tests...]

Using Aroma-Mock with JavaScript

Aroma-Mock is written in CoffeeScript. It can be used in JavaScript by requiring the coffee-script/register in your test code, and then requiring aroma-mock:

Quick Start

Here's an example test, that mocks the jQuery $ symbol and tests that it is used as expected. Before reading the entire thing, let's just look at the call expectation:

call( '$', [ "#input_element" ], fakeObject( 'element', ['val'] ) )

expresses our expectation that the tested code will call $ with "#input_element" as its first argument. We also arrange that the return value of this call will be a Mock Object called element.

We then expect that this element object's val method will be called with one argument, the string '123'. This is expressed in the expectation:

call( 'element.val', [ '123' ] )

Since we don't care about the return value from this call, we leave it unspecified. We can also use null

call( 'element.val', [ '123' ], null )

Since jQuery expectations are commonplace, we can use the shorthand expect_$ for this 2-call expectation - this is demonstrated below.

Another thing to note is the use of capture and captured to test functions called with callbacks. E.g., if our code calls fs.mkdir( 'somepath', onCreated ), we want to later call this onCreated function. We therefore use the capture feature of Aroma-Mock to get at it:

call( 'fs.mkdir', [ 'somepath', capture( 'onCreatedCallback' ) ] )

and later call it via the captured API:

captured.onCreatedCallback()

I hope this makes the following, complete listing, clear.

require 'aroma-mock' # import the Aroma-Mock framework
example = require '../example'

fakeGlobal( '$', [ 'getJSON', 'ajax' ] )
fakeGlobal( 'Point', [] )

describe 'example of aroma mocking framework', ->
	it 'should get JSON with jQuery', ->
		tested = new example.Example()
		scenario = new Scenario()
		scenario.expect call( '$.getJSON', [ 'www.google.com', {a:1, b:2}, capture( 'doneCallback' ) ], null )

		tested.getSomeJSON()
		scenario.end()

		captured.doneCallback()

	it 'should use jQuery on the DOM', ->
		tested = new example.Example()
		scenario = new Scenario()
		scenario.expect call( '$', [ "#input_element" ], fakeObject( 'element', ['val'] ) )
		scenario.expect call( 'element.val', [ '123' ], null )
		tested.useJQueryOnDOM( '123' )
		scenario.end()

	it 'should use jQuery on the DOM: example of expect_$ shorthand notation', ->
		tested = new example.Example()
		scenario = new Scenario()
		scenario.expect_$( '#input_element', 'val', [ '456' ], null )
		tested.useJQueryOnDOM( '456' )
		scenario.end()

here's the code that passes this test:

class Example
	useJQueryOnDOM: ( text ) =>
		$("#input_element").val( text )

	getSomeJSON: =>
		$.getJSON 'www.google.com', {a:1,b:2}, this._mycallback

	_mycallback: =>
		console.log( 'my callback called!' )

exports.Example = Example

Asynchronous Ajax JSON Retrieval Example

The AjaxTest class allows testing for an asynchronous ajax call. We define the parameters of the ajax call, the data supposedly returned from the server, and what we expect should happen on success.

Here's an asynchronous ajax test:


# we don't use getJSON in this example, but this is how to create a fake object
# with multiple methods.
fakeGlobal( '$', [ 'getJSON', 'ajax' ] )

describe 'example of aroma ajax mocking', ->
	it 'aroma can test asynchronous AJAX', ->
		tested = new example.Example()
		scenario = new Scenario()
		ajaxTest = new AjaxTest( scenario )
		ajaxTest.expect( { url: '/path/to/return_keys.json', data: { a: 1, b: 2 }, type: 'POST' } )
		ajaxTest.returnFromServer( { status: 'ok', answer: [ 'a', 'b' ] } )
		ajaxTest.onSuccessScenario = =>
			scenario.expect_$( "#status", 'val', [ 'ok' ], null )
			scenario.expect_$( "#output_element", 'val', [ [ 'a', 'b' ] ], null )

		tested.doAsyncAjax( { a: 1, b: 2 } )
		ajaxTest.verify()
		scenario.end()

this is the code that passes it

class Example
	doAsyncAjax: ( data ) =>
		success = (data) =>
			$("#status").val( data.status )
			$("#output_element").val( data.answer )
		$.ajax { url: '/path/to/return_keys.json', data: data, type: 'POST', success: success }

a test that checks the response to an error can also be written


# we don't use getJSON in this example, but this is how to create a fake object
# with multiple methods.
fakeGlobal( '$', [ 'getJSON', 'ajax' ] )

describe 'example of aroma ajax mocking', ->
	it 'aroma asynchronous AJAX error example', ->
		tested = new example.Example()
		scenario = new Scenario()
		ajaxTest = new AjaxTest( scenario )
		ajaxTest.expect( { url: '/path/to/return_keys.json', data: { a: 1, b: 2 }, type: 'POST' } )
		ajaxTest.errorFromServer( null, 'error text 123', null )
		ajaxTest.onErrorScenario = =>
			scenario.expect_$( "#status", 'val', [ 'error text 123' ], null )
			scenario.expect_$( "#output_element", 'val', [ [] ], null )

		tested.doAsyncAjax( { a: 1, b: 2 } )
		ajaxTest.verify( 'error' )
		scenario.end()

This code passes the test, as well as the previous one:

class Example
	doAsyncAjax: ( data ) =>
		success = (data) =>
			$("#status").val( data.status )
			$("#output_element").val( data.answer )
		error = ( unused, text, unused2 ) =>
			$("#status").val( text )
			$("#output_element").val( [] )
			
		$.ajax { 	url: '/path/to/return_keys.json',\
					data: data,
					type: 'POST',
					success: success,
					error: error }

The Callback Cascade Pattern

When writing JavaScript code, it often happens that we get a cascade of callback functions, since almost everything is done asynchronously. For example, say we want to check if some directory exists, and if not, create it, and then run some subprocess in the directory. If the directory already exists, we don't need to create it.

The callback cascade here is

fs.exists => fs.mkdir => childProcess.exec

We can use capture for this, but it gets ugly really fast. If the callback argument for each function in our cascade is the last one, which is often the case, e.g.

fs.exists(path, callback)

We can use the cascade pattern:

rewire = require 'rewire'

describe 'cascade of callbacks', ->
    beforeEach ->
        cascadeModule.__set__( 'fs', fakeObject( 'fs', [ 'exists', 'mkdir' ] ) )
        cascadeModule.__set__( 'childProcess', fakeObject( 'childProcess', [ 'exec' ] ) )

    it 'should work', ->
        myCallback = fakeObject( 'myCallback' )
        scenario.expectCascade [    cascade( 'fs.exists', [ 'some_path' ], null, [false] ),
                                    cascade( 'fs.mkdir', [ 'some_path' ] ),
                                    cascade( 'childProcess.exec', [ 'some command', {cwd: 'some_path'} ], null, [ null, "output string", "error string" ] ),
                                    call( 'myCallback', [ "output string" ] ) ]

        tested = new cascadeModule.Cascade()

        tested.go( 'some_path', myCallback )
        scenario.end()
        

NOTE the use of rewire to mock objects inside the module.

This expresses the expectation that the code will call fs.exists - passing it some_path. The fs.exists cascade specifies that fs.exists will return null, and then call its callback with the arguments specified in a list, in this case [false], so just one argument.

What happens when fs.exists's callback is called? Since the directory does not exist (we made fs.exists pass false to its callback, remember?) we expect it to call fs.mkdir, which will the call its callback.

We then demand that this callback call childProcess.exec and we specify the output and error strings passed to its callback, which should, finally, call the user's callback.

NOTE the last call in the cascade is a call, not a cascade - since we don't want it to automatically call a callback.

The code that passes this test would be:

fs = require 'fs'
childProcess = require 'child_process'

class Cascade
    go: ( path, callback ) =>
        @callback = callback
        fs.exists path, (exists) =>
            if exists
                this._runChild( path )
            else
                fs.mkdir path, =>
                    this._runChild( path )

    _runChild: ( path ) =>
        childProcess.exec 'some command', { cwd: path }, (err, output, error) =>
            @callback( output )

exports.Cascade = Cascade

Current Tags

  • 0.1.6                                ...           latest (5 years ago)

6 Versions

  • 0.1.6                                ...           5 years ago
  • 0.1.5                                ...           5 years ago
  • 0.1.4                                ...           5 years ago
  • 0.1.3                                ...           5 years ago
  • 0.1.2                                ...           5 years ago
  • 0.1.1                                ...           5 years ago
Maintainers (1)
Downloads
Today 0
This Week 0
This Month 0
Last Day 0
Last Week 0
Last Month 0
Dependencies (2)
Dev Dependencies (3)
Dependents (1)

Copyright 2014 - 2017 © taobao.org |