Comprehensive testing utilities for dialog testing, conversation simulation, and bot behavior validation.
A client for testing dialogs in isolation without requiring a full adapter setup.
/**
* A client for testing dialogs in isolation
*/
class BotkitTestClient {
/** Last dialog turn result from the test */
dialogTurnResult: DialogTurnResult;
/** Conversation state instance used in testing */
conversationState: ConversationState;
/**
* Create a BotkitTestClient to test a dialog
* @param channelId The channelId to be used for the test ('emulator' or 'test' recommended)
* @param bot The Botkit bot that has the dialog to test
* @param dialogToTest The identifier of the dialog to test
* @param initialDialogOptions Additional arguments to pass to the dialog
* @param middlewares A stack of middleware to run when testing
* @param conversationState A ConversationState instance to use
*/
constructor(
channelId: string,
bot: Botkit,
dialogToTest: string | string[],
initialDialogOptions?: any,
middlewares?: Middleware[],
conversationState?: ConversationState
);
/**
* Alternative constructor using TestAdapter
* @param testAdapter A configured TestAdapter instance
* @param bot The Botkit bot that has the dialog to test
* @param dialogToTest The identifier of the dialog to test
* @param initialDialogOptions Additional arguments to pass to the dialog
* @param middlewares A stack of middleware to run when testing
* @param conversationState A ConversationState instance to use
*/
constructor(
testAdapter: TestAdapter,
bot: Botkit,
dialogToTest: string | string[],
initialDialogOptions?: any,
middlewares?: Middleware[],
conversationState?: ConversationState
);
/**
* Send an activity into the dialog
* @param activity An activity potentially with text
* @returns The bot's response activity
*/
sendActivity(activity: Partial<Activity> | string): Promise<any>;
/**
* Get the next reply waiting to be delivered (if one exists)
*/
getNextReply(): Partial<Activity>;
}Usage Examples:
import { BotkitTestClient, Botkit, BotkitConversation } from "botkit";
// Create a test conversation
const controller = new Botkit({ disable_webserver: true });
const testConvo = new BotkitConversation("test_dialog", controller);
testConvo.say("Hello!");
testConvo.ask("What's your name?", async (response, convo, bot) => {
await bot.say(`Nice to meet you, ${response}!`);
}, { key: "name" });
controller.addDialog(testConvo);
// Create and use test client
const client = new BotkitTestClient("test", controller, "test_dialog");
// Test the dialog
describe("Test Dialog", () => {
it("should greet and ask for name", async () => {
// Send initial message to start dialog
let reply = await client.sendActivity("start");
expect(reply.text).toBe("Hello!");
// Get next message (the question)
reply = client.getNextReply();
expect(reply.text).toBe("What's your name?");
// Respond with name
reply = await client.sendActivity("Alice");
expect(reply.text).toBe("Nice to meet you, Alice!");
});
});Test straightforward dialog flows with basic assertions.
// Create a simple greeting dialog
const greetingDialog = new BotkitConversation("greeting", controller);
greetingDialog.say("Welcome to our service!");
greetingDialog.ask("How can I help you today?", [
{
pattern: "help",
handler: async (response, convo, bot) => {
await bot.say("Here's how I can help...");
}
},
{
pattern: "info",
handler: async (response, convo, bot) => {
await bot.say("Here's some information...");
}
},
{
default: true,
handler: async (response, convo, bot) => {
await bot.say("I didn't understand. Try 'help' or 'info'.");
await convo.repeat();
}
}
], { key: "request" });
controller.addDialog(greetingDialog);
// Test the dialog
describe("Greeting Dialog", () => {
let client;
beforeEach(() => {
client = new BotkitTestClient("test", controller, "greeting");
});
it("should welcome user and ask how to help", async () => {
let reply = await client.sendActivity("hi");
expect(reply.text).toBe("Welcome to our service!");
reply = client.getNextReply();
expect(reply.text).toBe("How can I help you today?");
});
it("should respond to help request", async () => {
await client.sendActivity("start");
client.getNextReply(); // welcome
client.getNextReply(); // question
const reply = await client.sendActivity("help");
expect(reply.text).toBe("Here's how I can help...");
});
it("should handle unknown responses", async () => {
await client.sendActivity("start");
client.getNextReply(); // welcome
client.getNextReply(); // question
const reply = await client.sendActivity("unknown");
expect(reply.text).toBe("I didn't understand. Try 'help' or 'info'.");
// Should repeat the question
const nextReply = client.getNextReply();
expect(nextReply.text).toBe("How can I help you today?");
});
});Test multi-threaded conversations with branching logic.
// Create a complex survey dialog
const surveyDialog = new BotkitConversation("survey", controller);
surveyDialog.say("Welcome to our customer survey!");
surveyDialog.ask("Are you a new customer?", [
{
pattern: "yes",
handler: async (response, convo, bot) => {
await convo.gotoThread("new_customer");
}
},
{
pattern: "no",
handler: async (response, convo, bot) => {
await convo.gotoThread("existing_customer");
}
}
], { key: "customer_type" });
// New customer thread
surveyDialog.addMessage("Great! We love new customers.", "new_customer");
surveyDialog.ask("How did you hear about us?", async (response, convo, bot) => {
convo.setVar("referral", response);
}, { key: "referral_source" }, "new_customer");
surveyDialog.addAction("complete", "new_customer");
// Existing customer thread
surveyDialog.addMessage("Thanks for being a loyal customer!", "existing_customer");
surveyDialog.ask("How long have you been with us?", async (response, convo, bot) => {
convo.setVar("tenure", response);
}, { key: "customer_tenure" }, "existing_customer");
surveyDialog.addAction("complete", "existing_customer");
controller.addDialog(surveyDialog);
// Test complex flows
describe("Survey Dialog", () => {
it("should handle new customer flow", async () => {
const client = new BotkitTestClient("test", controller, "survey");
// Start survey
let reply = await client.sendActivity("start");
expect(reply.text).toBe("Welcome to our customer survey!");
// Answer that they're new
reply = client.getNextReply();
expect(reply.text).toBe("Are you a new customer?");
reply = await client.sendActivity("yes");
expect(reply.text).toBe("Great! We love new customers.");
// Answer referral question
reply = client.getNextReply();
expect(reply.text).toBe("How did you hear about us?");
await client.sendActivity("Google search");
// Check that dialog completed with correct data
expect(client.dialogTurnResult.status).toBe(DialogTurnStatus.complete);
expect(client.dialogTurnResult.result.customer_type).toBe("yes");
expect(client.dialogTurnResult.result.referral_source).toBe("Google search");
});
it("should handle existing customer flow", async () => {
const client = new BotkitTestClient("test", controller, "survey");
await client.sendActivity("start");
client.getNextReply(); // welcome
client.getNextReply(); // question
let reply = await client.sendActivity("no");
expect(reply.text).toBe("Thanks for being a loyal customer!");
reply = client.getNextReply();
expect(reply.text).toBe("How long have you been with us?");
await client.sendActivity("2 years");
expect(client.dialogTurnResult.result.customer_type).toBe("no");
expect(client.dialogTurnResult.result.customer_tenure).toBe("2 years");
});
});Test dialogs with custom middleware and state management.
import { MemoryStorage, ConversationState, AutoSaveStateMiddleware } from "botbuilder";
// Create custom middleware for testing
class LoggingMiddleware {
async onTurn(context, next) {
console.log(`Received activity: ${context.activity.type}`);
await next();
}
}
// Test with middleware
describe("Dialog with Middleware", () => {
it("should work with custom middleware", async () => {
const storage = new MemoryStorage();
const conversationState = new ConversationState(storage);
const middlewares = [
new AutoSaveStateMiddleware(conversationState),
new LoggingMiddleware()
];
const client = new BotkitTestClient(
"test",
controller,
"greeting",
{},
middlewares,
conversationState
);
const reply = await client.sendActivity("hello");
expect(reply).toBeDefined();
// Verify state was saved
const context = client.conversationState;
expect(context).toBeDefined();
});
});Test dialogs that call other dialogs as children.
// Create parent and child dialogs
const childDialog = new BotkitConversation("child_dialog", controller);
childDialog.ask("What's your favorite color?", async (response, convo, bot) => {
convo.setVar("color", response);
}, { key: "favorite_color" });
const parentDialog = new BotkitConversation("parent_dialog", controller);
parentDialog.say("Let's collect some info about you.");
parentDialog.addChildDialog("child_dialog", "child_results");
parentDialog.say("Thanks! Your favorite color is {{vars.child_results.favorite_color}}.");
controller.addDialog(childDialog);
controller.addDialog(parentDialog);
// Test dialog composition
describe("Dialog Composition", () => {
it("should call child dialog and use results", async () => {
const client = new BotkitTestClient("test", controller, ["parent_dialog", "child_dialog"]);
// Start parent dialog
let reply = await client.sendActivity("start");
expect(reply.text).toBe("Let's collect some info about you.");
// Child dialog should start
reply = client.getNextReply();
expect(reply.text).toBe("What's your favorite color?");
// Answer child dialog
reply = await client.sendActivity("blue");
// Parent should resume with child results
expect(reply.text).toBe("Thanks! Your favorite color is blue.");
// Verify final results structure
const results = client.dialogTurnResult.result;
expect(results.child_results.favorite_color).toBe("blue");
});
});Test error handling and edge cases in dialogs.
describe("Error Handling", () => {
it("should handle unexpected responses gracefully", async () => {
const errorDialog = new BotkitConversation("error_test", controller);
errorDialog.ask("Enter a number between 1 and 10:", [
{
pattern: /^[1-9]|10$/,
handler: async (response, convo, bot) => {
await bot.say("Great choice!");
}
},
{
default: true,
handler: async (response, convo, bot) => {
await bot.say("Please enter a number between 1 and 10.");
await convo.repeat();
}
}
], { key: "number" });
controller.addDialog(errorDialog);
const client = new BotkitTestClient("test", controller, "error_test");
// Start dialog
await client.sendActivity("start");
client.getNextReply(); // question
// Send invalid response
let reply = await client.sendActivity("invalid");
expect(reply.text).toBe("Please enter a number between 1 and 10.");
// Should repeat question
reply = client.getNextReply();
expect(reply.text).toBe("Enter a number between 1 and 10:");
// Send valid response
reply = await client.sendActivity("5");
expect(reply.text).toBe("Great choice!");
});
it("should handle dialog cancellation", async () => {
const client = new BotkitTestClient("test", controller, "greeting");
await client.sendActivity("start");
// Simulate cancellation (this would normally be done by interrupt handler)
// In real scenario, you'd test the interrupt system
expect(client.dialogTurnResult).toBeDefined();
});
});Test complete bot behavior with message handlers and dialogs.
describe("Full Bot Integration", () => {
it("should respond to hears patterns and start dialogs", async () => {
// Set up bot with handlers
controller.hears("start survey", "message", async (bot, message) => {
await bot.beginDialog("survey");
});
controller.hears("hello", "message", async (bot, message) => {
await bot.reply(message, "Hi there!");
});
// Test simple response
const simpleTest = new BotkitTestClient("test", controller, []);
let reply = await simpleTest.sendActivity({
type: "message",
text: "hello"
});
expect(reply.text).toBe("Hi there!");
// Test dialog trigger
const dialogTest = new BotkitTestClient("test", controller, "survey");
reply = await dialogTest.sendActivity({
type: "message",
text: "start survey"
});
// Should start the survey dialog
expect(reply.text).toBe("Welcome to our customer survey!");
});
});