function testSendRecv(brokerOptions, inNodeOptions, outNodeOptions, options, hooks) {
    options = options || {};
    brokerOptions = brokerOptions || {};
    inNodeOptions = inNodeOptions || {};
    outNodeOptions = outNodeOptions || {};
    const sendMsg = options.sendMsg || {};
    sendMsg.topic = sendMsg.topic || nextTopic();
    const expectMsg = options.expectMsg || Object.assign({}, sendMsg);
    expectMsg.payload = inNodeOptions.payload === undefined ? expectMsg.payload : inNodeOptions.payload;
    if (inNodeOptions.topicType != "dynamic") {
        inNodeOptions.topic = inNodeOptions.topic || sendMsg.topic;
    }

    const { flow, nodes } = buildBasicMQTTSendRecvFlow(brokerOptions, inNodeOptions, outNodeOptions);
    if (hooks.beforeLoad) { hooks.beforeLoad(flow) }
    helper.load(mqttNodes, flow, function () {
        try {
            const helperNode = helper.getNode("helper.node");
            const mqttBroker = helper.getNode(brokerOptions.id);
            const mqttIn = helper.getNode(nodes.mqtt_in.id);
            const mqttOut = helper.getNode(nodes.mqtt_out.id);
            let afterLoadHandled = false, finished = false;
            if (hooks.afterLoad) {
                afterLoadHandled = hooks.afterLoad(helperNode, mqttBroker, mqttIn, mqttOut)
            }
            if (!afterLoadHandled) {
                helperNode.on("input", function (msg) {
                    finished = true
                    try {
                        compareMsgToExpected(msg, expectMsg);
                        if (hooks.done) { hooks.done(); }
                    } catch (err) {
                        if (hooks.done) { hooks.done(err); }
                        else { throw err; }
                    }
                });
            }
            waitBrokerConnect(mqttBroker)
            .then(() => {
                //finally, connected!
                if (hooks.afterConnect) {
                    let handled = hooks.afterConnect(helperNode, mqttBroker, mqttIn, mqttOut);
                    if (handled) { return }
                }
                if(sendMsg.topic) {
                    if (mqttIn.isDynamic) {
                        mqttIn.receive({ "action": "subscribe", "topic": sendMsg.topic })
                    }
                    mqttOut.receive(sendMsg);
                }
            })
            .catch((e) => {
                if(finished) { return }
                if (hooks.done) { hooks.done(e); }
                else { throw e; }
            });
        } catch (err) {
            if(finished) { return }
            if (hooks.done) { hooks.done(err); }
            else { throw err; }
        }
    });
}
A basic unit test that builds a flow containing 1 broker, 1 mqtt-in, one mqtt-out and a helper.  
 It performs the following steps: builds flow, loads flow, waits for connection, sends `sendMsg`, 
 waits for msg then compares `sendMsg` to `expectMsg`, and finally calls `done`
@param {object} brokerOptions anything that can be set in an MQTTBrokerNode (e.g. id, name, url, broker, server, port, protocolVersion, ...)
@param {object} inNodeOptions anything that can be set in an MQTTInNode (e.g. id, name, broker, topic, rh, nl, rap, ... )
@param {object} outNodeOptions anything that can be set in an MQTTOutNode (e.g. id, name, broker, ...)
@param {object} options an object for passing in test properties like `sendMsg` and `expectMsg`
@param {object} hooks an object containing hook functions...
  * [fn] `done()` - the tests done function. If excluded, an error will be thrown upon test error
  * [fn] `beforeLoad(flow)` - provides opportunity to adjust the flow JSON before loading into runtime
  * [fn] `afterLoad(helperNode, mqttBroker, mqttIn, mqttOut)` - called before connection attempt
  * [fn] `afterConnect(helperNode, mqttBroker, mqttIn, mqttOut)` - called before connection attempt