diff --git a/src/event/Eventable.js b/src/event/Eventable.js
index d2d42cc..c64a971 100644
--- a/src/event/Eventable.js
+++ b/src/event/Eventable.js
@@ -74,7 +74,10 @@ module.exports = Interface( 'Eventable',
* synchronous; it is also undefined whether LISTENER will receive
* 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.
* without use of #removeListener) from event EV unless the
diff --git a/test/event/EventableTestConform.js b/test/event/EventableTestConform.js
new file mode 100644
index 0000000..656f941
--- /dev/null
+++ b/test/event/EventableTestConform.js
@@ -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 .
+ *
+ * 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.)} 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 );
+ } );
+ } );
+}
+