Initial Eventable conformance test case (#on/#addListener)

These conformance test cases will be an excellent disciplinary tool,
ensuring that any implementation of Eventable conforms strictly to its
specification; this will provide developers guarantees that the
implementation will work as expected polymorphically.
events
Mike Gerwitz 2014-08-10 01:04:48 -04:00
parent a2deba59ad
commit aab86a47cd
2 changed files with 166 additions and 1 deletions

View File

@ -74,7 +74,10 @@ module.exports = Interface( 'Eventable',
* synchronous; it is also undefined whether LISTENER will receive * synchronous; it is also undefined whether LISTENER will receive
* messages emitted by event EV before having hooked the event. * messages emitted by event EV before having hooked the event.
* *
* The validity of the event identifier EV is undefined. * The validity of the event identifier EV is undefined, except that it
* must be a string and an empty string shall throw a TypeError (as this
* may very well indicate a bug in the calling code). LISTENER shall not
* be required to declare any number of parameters.
* *
* An implementation shall not automatically unhook LISTENER (i.e. * An implementation shall not automatically unhook LISTENER (i.e.
* without use of #removeListener) from event EV unless the * without use of #removeListener) from event EV unless the

View File

@ -0,0 +1,162 @@
/**
* Tests for conforming Eventable implementation
*
* Copyright (C) 2014 Mike Gerwitz
*
* This file is part of jsTonic.
*
* jstonic is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* A conforming implementation must pass each of these tests. See the
* Eventable interface itself for additional requirements and documentation.
*
* To use these tests, invoke the exported function with a constructor
* function returning an instance of the SUT.
*/
let expect = require( 'chai' ).expect,
types = require( '../../' ).util.types;
let _fvoid = () => {},
_ev = 'ev',
_nonstr = types.typevals.nonstr,
_nonf = types.typevals.nonfunc;
// data passed to ctor to prepare for test
let _events = [ _ev ];
/**
* Invoke conformance tests
*
* CTOR will be passed an array of events that the conformance tests will
* make use of, allowing them to be registered if needed (for validation
* assertions).
*
* This test case will ideally be invoked after the top-level `describe` of
* the SUT test case so that the output reads as "SUT, conforming to
* Eventable, ...".
*
* @param {function(Array.<string>)} ctor SUT constructor function
*
* @return {undefined}
*/
module.exports = ctor =>
{
// shorthand for SUT construction
let meta_ctor = () => ctor( _events );
/**
* #on and #addListener are one and the same, but detecting that they
* alias each other is not reliable for various reasons; instead, we'll
* run the tests on both methods.
*/
describe( 'conforming to Eventable', () =>
{
[ 'on', 'addListener' ].forEach( x =>
_onTests( meta_ctor, x ) );
} );
};
function _onTests( ctor, on )
{
/**
* Note that by using _fvoid, we are implicitly testing the requirement
* that the listener shall not be required to declare any number of
* parameters (because we defined no parameters).
*/
describe( `#${on}`, () =>
{
/**
* We must test both inputs at once since we cannot otherwise say
* with confidence which parameter is non-conforming.
*/
it( 'accepts string event id with listener function', () =>
{
expect( () =>
{
ctor()[ on ]( _ev, _fvoid );
} ).to.not.throw( Error );
} );
/**
* A TypeError should be thrown when the event id is empty. The
* rationale behind this is that the event id may be dynamically
* determined at runtime, or may otherwise be stored in a variable,
* which probably is not supposed to be empty; if it is, this could
* represent a logic error (such as an incomplete conditional or
* table lookup failure). More likely, this would result in
* `undefined`, which is covered in below tests.
*
* Implementations wishing to use an empty string to indicate "all
* events" could instead use, for example, `*` (motivated by shell
* globbing).
*/
it( 'does not accept an empty event id', () =>
{
expect( () => ctor()[ on ]( '', _fvoid ) )
.to.throw( TypeError );
} );
/**
* All event identifiers should be strings; this provides
* consistency between all implementations and helps to weed out
* runtime bugs (see the "empty event id" test for examples).
*/
it( 'does not accept non-string event ids', () =>
{
_nonstr.forEach( badev =>
{
expect( () => ctor()[ on ]( badev, _fvoid ) )
.to.throw( TypeError );
} );
} );
/**
* Same rationale as the event string argument.
*/
it( 'does not accept non-function listeners', () =>
{
_nonf.forEach( badf =>
{
expect( () => ctor()[ on ]( _ev, badf ) )
.to.throw( TypeError );
} );
} );
/**
* When hooking multiple events on a single object, it is convenient
* to be able to do so concisely without having to repeat the name
* of the object reference.
*
* Since exceptions are used to indicate error conditions, there is
* also no other useful value to return that would not break
* encapsulation.
*/
it( 'returns self for method chaining', () =>
{
let inst = ctor();
expect( inst[ on ]( _ev, _fvoid ) )
.to.equal( inst );
} );
} );
}