From 5f36d9272f30deec23efe58cad1c7d2dc9793786 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 13 Feb 2017 09:10:50 -0500 Subject: [PATCH] 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. --- src/test/README | 10 ++ src/test/program/DummyClassifier.js | 7 + src/test/program/Program.js | 30 ++++ src/test/program/util.js | 252 ++++++++++++++++++++++++++++ 4 files changed, 299 insertions(+) create mode 100644 src/test/README create mode 100644 src/test/program/DummyClassifier.js create mode 100644 src/test/program/Program.js create mode 100644 src/test/program/util.js 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 . + */ + +"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 . + */ + +'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, + * event, + * step_id, + * data, + * cmatch, + * trigger)>, + * expected }. + * ``` + * + * `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, + * indexes> } + * ``` + * + * `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)} 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} 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', +} );