1
0
Fork 0

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.
master
Mike Gerwitz 2017-02-13 09:10:50 -05:00
parent e610372c84
commit 5f36d9272f
4 changed files with 299 additions and 0 deletions

10
src/test/README 100644
View File

@ -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').

View File

@ -0,0 +1,7 @@
/**
* Dummpy classifier for TestProgram
*/
module.exports = function() {};
module.exports.knownFields = {};

View File

@ -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',
} );

View File

@ -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',
} );