Skip to content

Commit

Permalink
Add message filter and in-line options update (#28)
Browse files Browse the repository at this point in the history
* added ability to filter messages

* added in line options modifier
  • Loading branch information
microsoftly committed Oct 6, 2017
1 parent 98b2170 commit 519f235
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 67 deletions.
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ config can be set one of 2 ways:
1. creating a bot-tester.json file in the root directory of your project.
2. passing in a config object into the options param, which is the last, when creating a BotTester instance


Passing in the config overrides any default values or values set by bot-tester.json. At the moment, the options are:
```javascript
```typescript
{
defaultAddress: botbuilder.IAddress,
timeout: number // in milliseconds
// each filter returns false for messages that the BotTester should ignore
messageFilters: ((message:IMessage) => boolean)[]
}
```
if timeout is defined, then a particular ```runTest()``` call will fail if it does not receive each expected message within the timeout period of time set in the options.

additionally, config can be set by using the config updating options in the build chain, documented below in the example use

For a more in depth view, check out [the Bot Tester Framework Config doc](https://microsoftly.github.io/BotTester/interfaces/_config_.iconfig.html)
# Example Usage
```javascript
Expand Down Expand Up @@ -357,3 +362,54 @@ describe('BotTester', () => {
.runTest();
});
```
# can apply one or more message filters in the BotTester options for messages to ignore
``` javascript
it('can apply one or more message filters in the BotTester options for messages to ignore', () => {
bot.dialog('/', (session) => {
session.send('hello');
session.send('how');
session.send('are');
session.send('you?');
});

const ignoreHowMessage = (message) => !message.text.includes('how');
const ignoreAreMessage = (message) => !message.text.includes('are');

return new BotTester(bot, { messageFilters: [ignoreHowMessage, ignoreAreMessage]})
.sendMessageToBot('intro', 'hello', 'you?')
.runTest();
});
```
# can modify BotTester options
``` javascript
describe('can modifiy options in line in builder chain', () => {
it('add a message filter', () => {
bot.dialog('/', (session) => {
session.send('hello');
session.send('there');
session.send('green');
});

return new BotTester(bot)
.addMessageFilter((msg) => !msg.text.includes('hello'))
.addMessageFilter((msg) => !msg.text.includes('there'))
.sendMessageToBot('hey', 'green')
.runTest();
});

it('change timeout time', (done) => {
const timeout = 750;
bot.dialog('/', (session) => {
setTimeout(() => session.send('hi there'), timeout * 2 );
});

expect(new BotTester(bot)
.setTimeout(timeout)
.sendMessageToBot('hey', 'hi there')
.runTest()
).to.be.rejected.notify(done);
});
});
```
170 changes: 111 additions & 59 deletions src/BotTester.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Promise from 'bluebird';
import { IAddress, IMessage, Message, Session, UniversalBot } from 'botbuilder';
import { config, IConfig } from './config';
import { config, IConfig, MessageFilter } from './config';
import { ExpectedMessage, PossibleExpectedMessageCollections, PossibleExpectedMessageType } from './ExpectedMessage';
import { botToUserMessageCheckerFunction, MessageService } from './MessageService';
import { SessionService } from './SessionService';
Expand All @@ -10,70 +10,141 @@ export type checkSessionFunction = (s: Session) => void;
type TestStep = () => Promise<any>;
//tslint:enable

/**
* methods that the Test builder can call to edit/modify the options. These can be called until any of the IBotTester method are called.
*/
export interface IOptionsModifier {
/**
* adds a message filter to the options.
* @param filter message filter to add
*/
addMessageFilter(filter: MessageFilter): BotTester;

/**
* sets the timeout time that the BotTester will wait for any particular message before failing
*/
setTimeout(milliseconds: number): BotTester;
}

/**
* Test builder/runner suite. After any of these are called, no functions in IConfigModified should be accessible
*/
export interface IBotTester {
/**
* executes each test step serially.
*/
runTest(): Promise<{}>;

/**
* loads a session associated with an address and passes it to a user defined function
* @param sessionCheckerFunction function passed in to inspect message
* @param address (Optional) address of the session to load. Defaults to bot's default address if not defined
*/
checkSession(sessionCheckerFunction: checkSessionFunction, address?: IAddress): IBotTester;

/**
* sends a message to a bot and compares bot responses against expectedResponsess. Expected responses can be a variable number of args,
* each of which can be a single expected response of any PossibleExpectedMessageType or a collection of PossibleExpectedMessageType
* that mocks a randomly selected response by the bot
* @param msg message to send to bot
* @param expectedResponses (Optional) responses the bot-tester framework checks against
*/
sendMessageToBot(
msg: IMessage | string,
// currently only supports string RegExp IMessage
...expectedResponses: (PossibleExpectedMessageType | PossibleExpectedMessageType[])[]): IBotTester;

/**
* same as sendMessageToBot, but the order of responses is not checked. This will cause the test to hang until all messages it expects
* are returned
*/
sendMessageToBotIgnoringResponseOrder(
msg: IMessage | string,
// currently only supports string RegExp IMessage
...expectedResponses: (PossibleExpectedMessageType | PossibleExpectedMessageType[])[]
): IBotTester;

/**
* sends a message to the bot. This should be used whenever session.save() is used without sending a reply to the user. This exists due
* to a limitation in the current implementation of the botbuilder framework
*
* @param msg message to send to bot
*/
sendMessageToBotAndExpectSaveWithNoResponse(msg: IMessage | string): IBotTester;

/**
* Works exactly like Promise's .then function, except that the return value is not passed as an arg to the next function (even if its
* another .then)
* @param fn some function to run
*/
then(fn: Function): IBotTester;

/**
* Waits for the given delay between test steps.
* @param delayInMiliseconds time to wait in milliseconds
*/
wait(delayInMilliseconds: number): IBotTester;
}

/**
* Test builder and runner for botbuilder bots
*/
export class BotTester {
export class BotTester implements IBotTester, IOptionsModifier {
private bot: UniversalBot;
private defaultAddress: IAddress;
private sessionLoader: SessionService;

// this is instantiated in the runTest function. This is done to allow any changes to the config to accumulate
private messageService: MessageService;
private testSteps: TestStep[];
private config: IConfig;

//tslint:disable
/**
*
* @param bot bot that will be tested against
* @param options (optional) options to pass to bot. Sets the default address and test timeout
*/
//tslint:enable
constructor(bot: UniversalBot, options: IConfig = config) {
const defaultAndInputOptionMix = Object.assign({}, config, options);
this.config = Object.assign({}, config, options);
this.config.messageFilters = this.config.messageFilters.slice();
this.bot = bot;
this.defaultAddress = defaultAndInputOptionMix.defaultAddress;
this.messageService = new MessageService(bot, defaultAndInputOptionMix);
this.messageService = new MessageService(bot, this.config);
this.sessionLoader = new SessionService(bot);
this.testSteps = [] as TestStep[];
}

public addMessageFilter(messageFilter: MessageFilter): BotTester {
this.config.messageFilters.push(messageFilter);

return this;
}

public setTimeout(milliseconds: number): BotTester {
this.config.timeout = milliseconds;

return this;
}

/**
* executes each test step serially
* Initializes the MessegeService here to allow config changes to accumulate
*/
//tslint:disable
public runTest(): Promise<any> {
//tslint:enable
public runTest(): Promise<{}> {
this.messageService = new MessageService(this.bot, this.config);

return Promise.mapSeries(this.testSteps, (fn: TestStep) => fn());
}

/**
* loads a session associated with an address and passes it to a user defined function
* @param sessionCheckerFunction function passed in to inspect message
* @param address (Optional) address of the session to load. Defaults to bot's default address if not defined
*/
public checkSession(
sessionCheckerFunction: checkSessionFunction,
address?: IAddress
): BotTester {
const runSessionChecker = () => this.sessionLoader.getSession(address || this.defaultAddress)
): IBotTester {
const runSessionChecker = () => this.sessionLoader.getSession(address || this.config.defaultAddress)
.then(sessionCheckerFunction);

this.testSteps.push(runSessionChecker);

return this;
}

/**
* sends a message to a bot and compares bot responses against expectedResponsess. Expected responses can be a variable number of args,
* each of which can be a single expected response of any PossibleExpectedMessageType or a collection of PossibleExpectedMessageType
* that mocks a randomly selected response by the bot
* @param msg message to send to bot
* @param expectedResponses (Optional) responses the bot-tester framework checks against
*/
public sendMessageToBot(
msg: IMessage | string,
// currently only supports string RegExp IMessage
...expectedResponses: (PossibleExpectedMessageType | PossibleExpectedMessageType[])[]
): BotTester {
): IBotTester {
const message = this.convertToIMessage(msg);

// possible that expected responses may be undefined. Remove them
Expand All @@ -86,7 +157,7 @@ export class BotTester {
msg: IMessage | string,
// currently only supports string RegExp IMessage
...expectedResponses: (PossibleExpectedMessageType | PossibleExpectedMessageType[])[]
): BotTester {
): IBotTester {
const message = this.convertToIMessage(msg);

// possible that expected responses may be undefined. Remove them
Expand All @@ -95,38 +166,19 @@ export class BotTester {
return this.sendMessageToBotInternal(message, expectedResponses, true);
}

/**
* sends a message to the bot. This should be used whenever session.save() is used without sending a reply to the user. This exists due
* to a limitation in the current implementation of the botbuilder framework
*
* @param msg message to send to bot
*/
public sendMessageToBotAndExpectSaveWithNoResponse(
msg: IMessage | string
): BotTester {
public sendMessageToBotAndExpectSaveWithNoResponse(msg: IMessage | string): IBotTester {
const message = this.convertToIMessage(msg);

return this.sendMessageToBotInternal(message, [this.sessionLoader.getInternalSaveMessage(message.address)]);
}

/**
* Works exactly like Promise's .then function, except that the return value is not passed as an arg to the next function (even if its
* another .then)
* @param fn some function to run
*/
//tslint:disable
public then(fn: Function): BotTester {
//tslint:enable
public then(fn: Function): IBotTester {
this.testSteps.push(() => Promise.method(fn)());

return this;
}

/**
* Waits for the given delay between test steps.
* @param delayInMiliseconds time to wait in milliseconds
*/
public wait(delayInMilliseconds: number): BotTester {
public wait(delayInMilliseconds: number): IBotTester {
this.testSteps.push(() => Promise.delay(delayInMilliseconds));

return this;
Expand All @@ -136,7 +188,7 @@ export class BotTester {
if (typeof(msg) === 'string') {
return new Message()
.text(msg as string)
.address(this.defaultAddress)
.address(this.config.defaultAddress)
.toMessage();
}

Expand All @@ -163,9 +215,9 @@ export class BotTester {
} else if (expectedResponses instanceof Array) {
if (expectedResponses.length > 0) {
expectedMessages = (expectedResponses as PossibleExpectedMessageCollections[])
// tslint:disable
.map((currentExpectedResponseCollection: PossibleExpectedMessageCollections) => new ExpectedMessage(currentExpectedResponseCollection));
// tslint:enable

.map((currentExpectedResponseCollection: PossibleExpectedMessageCollections) =>
new ExpectedMessage(currentExpectedResponseCollection));
}
}

Expand Down
13 changes: 8 additions & 5 deletions src/MessageService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Promise from 'bluebird';
import { IAddress, IMessage, Message, Session, UniversalBot } from 'botbuilder';
import { setTimeout } from 'timers';
import { IConfig, NO_TIMEOUT } from './config';
import { IConfig, MessageFilter, NO_TIMEOUT } from './config';
import { ExpectedMessage } from './ExpectedMessage';
import { OutgoingMessageComparator } from './OutgoingMessageComparator';

Expand Down Expand Up @@ -101,12 +101,15 @@ export class MessageService {
* Inject middleware to intercept outgoing messages to check their content
*/
private applyOutgoingMessageListener(): void {
this.bot.on('outgoing', (e: IMessage | IMessage[]) => {
if (!(e instanceof Array)) {
e = [e];
this.bot.on('outgoing', (outgoingMessages: IMessage | IMessage[]) => {
if (!(outgoingMessages instanceof Array)) {
outgoingMessages = [outgoingMessages];
}

this.botToUserMessageChecker(e);
this.config.messageFilters.forEach(
(messageFilter: MessageFilter) => outgoingMessages = (outgoingMessages as IMessage[]).filter(messageFilter));

this.botToUserMessageChecker(outgoingMessages);
});
}
}
14 changes: 13 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { IAddress } from 'botbuilder';
import { IAddress, IMessage } from 'botbuilder';
import * as fs from 'fs';

/**
* Returns true if a message
*/
export type MessageFilter = (message: IMessage) => boolean;

export interface IConfig {
/**
* timeout in milliseconds before a BotTester runner will fail a test (when not overriden)
Expand All @@ -10,6 +15,11 @@ export interface IConfig {
* default address bot will use for all communication (when not overriden)
*/
defaultAddress?: IAddress;

/**
* filters for messages that the BotTester framework should use
*/
messageFilters?: MessageFilter[];
}

const configFilePath = `bot-tester.json`;
Expand All @@ -34,4 +44,6 @@ if (configFileExists) {
configInternal = JSON.parse(fs.readFileSync(configFilePath, { encoding: 'utf8' }));
}

configInternal.messageFilters = [];

export const config = configInternal;

0 comments on commit 519f235

Please sign in to comment.