Mike Gerwitz

Activist for User Freedom

aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Gerwitz <gerwitzm@lovullo.com>2017-02-13 09:10:50 -0500
committerMike Gerwitz <gerwitzm@lovullo.com>2017-02-15 14:19:33 -0500
commit5f36d9272f30deec23efe58cad1c7d2dc9793786 (patch)
treeb4b9b1215b285747013fe0bbdd427fca981a3588
parente610372c8443ba5c5e73ed0791e642d82772d5cd (diff)
downloadliza-5f36d9272f30deec23efe58cad1c7d2dc9793786.tar.gz
liza-5f36d9272f30deec23efe58cad1c7d2dc9793786.tar.bz2
liza-5f36d9272f30deec23efe58cad1c7d2dc9793786.zip
Testing utilities for stubbing Program
This allows for testing assertions. It's fairly primitive, but will work for the time being. * src/test/README: Add file. * src/test/program/DummyClassifier.js: Add module. * src/test/program/Program.js: Add class. * src/test/program/util.js: Add module.
-rw-r--r--src/test/README10
-rw-r--r--src/test/program/DummyClassifier.js7
-rw-r--r--src/test/program/Program.js30
-rw-r--r--src/test/program/util.js252
4 files changed, 299 insertions, 0 deletions
diff --git a/src/test/README b/src/test/README
new file mode 100644
index 0000000..8e02014
--- /dev/null
+++ b/src/test/README
@@ -0,0 +1,10 @@
+Liza Testing Library
+====================
+
+This namespace contains a library to aid in the testing of code that _makes
+use of_ Liza. Especially due to legacy reasons, there are some parts of the
+system that are quite difficult to mock and work with.
+
+For test cases of Liza itself, see `/test' in the project root (sibling of
+`/src').
+
diff --git a/src/test/program/DummyClassifier.js b/src/test/program/DummyClassifier.js
new file mode 100644
index 0000000..871d4fe
--- /dev/null
+++ b/src/test/program/DummyClassifier.js
@@ -0,0 +1,7 @@
+/**
+ * Dummpy classifier for TestProgram
+ */
+
+module.exports = function() {};
+module.exports.knownFields = {};
+
diff --git a/src/test/program/Program.js b/src/test/program/Program.js
new file mode 100644
index 0000000..b62dc7d
--- /dev/null
+++ b/src/test/program/Program.js
@@ -0,0 +1,30 @@
+/**
+ * Mockable Program
+ *
+ * Copyright (C) 2017 LoVullo Associates, Inc.
+ *
+ * This file is part of liza.
+ *
+ * liza 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/>.
+ */
+
+"use strict";
+
+
+module.exports = program_path => require( program_path )
+ .extend(
+{
+ classifier: __dirname + '/DummyClassifier',
+} );
+
diff --git a/src/test/program/util.js b/src/test/program/util.js
new file mode 100644
index 0000000..f583032
--- /dev/null
+++ b/src/test/program/util.js
@@ -0,0 +1,252 @@
+/**
+ * Utility functions for Program testing
+ *
+ * Copyright (C) 2017 LoVullo Associates, Inc.
+ *
+ * This file is part of liza.
+ *
+ * liza 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/>.
+ */
+
+'use strict';
+
+
+// N.B.: if the step titles change, these keys will change; we consider this
+// to be acceptable because, if steps change, the tests will also likely
+// change, and this is the only unique identifier we have (perhaps another
+// can be added in the future that won't change)
+exports.stepNameIdMap = Sut => Sut().steps.reduce(
+ ( result, { title }, step_id ) =>
+ {
+ result[ title.replace( ' ', '_' ) ] = step_id;
+ return result;
+ },
+ {}
+);
+
+
+/**
+ * Run tests against Program assertions
+ *
+ * Provided will be an expect-style testing framework and a Program
+ * SUT, along with a descriptor-providing callback `descf`. The callback
+ * will be invoked with certain useful information (like a step map) and is
+ * expected to return an array of test descriptors, which are of the format:
+ *
+ * ```
+ * { label<string>,
+ * event<string>,
+ * step_id<string>,
+ * data<Object>,
+ * cmatch<Object>,
+ * trigger<function(string, string, string, Array<string>)>,
+ * expected<Object> }.
+ * ```
+ *
+ * `step_id` can be derived from the step map provided to `descf`. `data`
+ * is a key-value map of bucket data. `cmatch` is a key-value map of
+ * classification match data of the form:
+ *
+ * ```
+ * { any<boolean>,
+ * indexes<Array<number>> }
+ * ```
+ *
+ * `trigger` is an optional function to be invoked for any triggers that
+ * fire during assertion events. Its arguments, respectively, should be the
+ * event name; the name of the field on which the event was triggered; the
+ * trigger value; and any indexes associated with the trigger.
+ *
+ * `expected` is the expected failure result of the assertion, and is a
+ * key-value map of the field id on which the failure occurred to an array
+ * of string indexes of the failures (strings for legacy reasons; may change
+ * in the future).
+ *
+ * Each test will trigger the event `event` (e.g. `submit`, `change`) only
+ * for the given step `step_id`.
+ *
+ * N.B.: `expect` is required rather than chai being explicitly required in
+ * this module to prevent a dependency situation whereby chai is pulled in
+ * when parsing the dependency graph of /src.
+ *
+ * @param {*} expect expect-style BDD interface
+ * @param {Program} Sut Program SUT
+ * @param {function(Object<steps>)} descf descriptor-providing callback
+ *
+ * @return {undefined}
+ */
+exports.testAssertions = ( expect, Sut, descf ) =>
+{
+ const StubSut = exports.stubProgram( Sut );
+
+ // test descriptors
+ const descs = descf( {
+ steps: this.stepNameIdMap( StubSut )
+ } );
+
+ if ( !Array.isArray( descs ) )
+ {
+ throw TypeError( "Expected array of test descriptors" );
+ }
+
+ descs.forEach( ( {
+ expected,
+ label,
+ event: event_id,
+ data: given_data,
+ step_id,
+ cmatch = {}
+ } ) =>
+ {
+ it( label, () =>
+ {
+ const sut = StubSut();
+
+ const result = exports.handleEvent( sut, event_id, {
+ step_id: step_id,
+ bucket: exports.stubBucket( sut, given_data ),
+ cmatch: exports.stubCmatch( cmatch ),
+ } );
+
+ for ( let name in expected )
+ {
+ expect( result[ name ].map( c => ''+c.getField().getIndex() ) )
+ .to.deep.equal( expected[ name ] );
+ }
+ } );
+ } );
+}
+
+
+/**
+ * Produce a minimal bucket-like object suitable for Program assertions
+ *
+ * This is _not_ a general-purpose bucket mock; it supports only
+ * `#getDataByName`.
+ *
+ * @param {Program} sut program under test
+ * @param {Object} given_data stub bucket data (key/value)
+ *
+ * @return {Object} bucket-like object
+ */
+exports.stubBucket = ( sut, given_data ) =>
+{
+ const { defaults } = sut;
+
+ // provide default bucket data so tests don't blow up
+ const base_data = Object.keys( defaults )
+ .map( key => [ defaults[ key ] ] );
+
+ // the given_data will need to be converted into property descriptors
+ const data = Object.create(
+ base_data,
+ Object.keys( given_data ).reduce( ( bdata, name ) => {
+ bdata[ name ] = { value: given_data[ name ] };
+ return bdata;
+ }, {} )
+ );
+
+ return {
+ getDataByName( name )
+ {
+ return data[ name ] || [];
+ },
+ };
+};
+
+
+/**
+ * Trigger proper program event and return assertion results
+ *
+ * The final argument is a descriptor that must at least contain a step id,
+ * and may optionally contain bucket and cmatch data, and a trigger callback
+ * function. This is also the test descriptor format.
+ *
+ * @param {Program} sut program under test
+ * @param {string} event_id event id
+ * @param {Object} data descriptor
+ *
+ * @return {?Object} assertion results or null if no failures
+ */
+exports.handleEvent = (
+ sut,
+ event_id,
+ { step_id, bucket = {}, cmatch = {}, trigger = () => {} }
+) =>
+{
+ if ( typeof step_id !== 'number' )
+ {
+ throw TypeError( `Invalid step_id '${step_id}'`)
+ }
+
+ switch ( event_id )
+ {
+ case 'submit':
+ return sut.submit( step_id, bucket, cmatch, trigger );
+ break;
+
+ default:
+ throw Error( `Unknown event: ${event_id}` );
+ }
+}
+
+
+/**
+ * Produce a stub cmatch object suitable for Program assertions
+ *
+ * The `cmatch` array should contain an array of numbers with a `1`
+ * representing a match at that respective index and a `0` representing no
+ * match. This function will generate the appropriate cmatch object.
+ *
+ * This is _not_ a general-purpose cmatch mock.
+ *
+ * @param {Array<number>} cmatch key/value map of class to matching indexes
+ *
+ * @return {Object} stub cmatch
+ */
+exports.stubCmatch = ( cmatch_dfn ) =>
+{
+ const __classes = Object.keys( cmatch_dfn )
+ .reduce( ( classes, name ) =>
+ {
+ classes[ name ] = {
+ is: cmatch_dfn[ name ].some( matched => matched ),
+ indexes: cmatch_dfn[ name ].map( matched => +matched ),
+ };
+
+ return classes;
+ }, {} );
+
+ const cmatch = Object.keys( cmatch_dfn )
+ .reduce( ( classes, name ) =>
+ {
+ classes[ name ] = {
+ any: cmatch_dfn[ name ].some( i => +i === 1 ),
+ all: cmatch_dfn[ name ].every( i => +i === 1 ),
+ indexes: cmatch_dfn[ name ],
+ };
+
+ return classes;
+ }, {} );
+
+ cmatch.__classes = __classes;
+
+ return cmatch;
+};
+
+
+exports.stubProgram = Program => Program.extend(
+{
+ classifier: __dirname + '/DummyClassifier',
+} );