or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

bot-controller.mdbot-worker.mdconversations.mddialog-wrapper.mdindex.mdmessage-handling.mdteams-integration.mdtesting.md
tile.json

testing.mddocs/

Testing

Comprehensive testing utilities for dialog testing, conversation simulation, and bot behavior validation.

Capabilities

BotkitTestClient

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!");
  });
});

Testing Simple Dialogs

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?");
  });
});

Testing Complex Dialog Flows

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");
  });
});

Testing with Custom Middleware

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();
  });
});

Testing Dialog Composition

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");
  });
});

Testing Error Scenarios

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();
  });
});

Integration Testing

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!");
  });
});