or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

activity-handling.mdbot-adapters.mdindex.mdmessage-factories.mdmiddleware.mdoauth-authentication.mdstate-management.mdstorage.mdtelemetry-logging.mdtesting-utilities.mdturn-context.md

testing-utilities.mddocs/

0

# Testing Utilities

1

2

Testing utilities for unit testing bots including test adapters, test flows, and assertion helpers. Enables comprehensive testing of bot logic without requiring actual Bot Framework channels.

3

4

## Capabilities

5

6

### TestAdapter

7

8

Test adapter for unit testing bots that simulates Bot Framework functionality without requiring actual HTTP endpoints or channel connections.

9

10

```python { .api }

11

class TestAdapter(BotAdapter):

12

def __init__(self, conversation_reference=None):

13

"""

14

Initialize test adapter.

15

16

Args:

17

conversation_reference (ConversationReference, optional): Default conversation reference

18

"""

19

20

async def send_activities(self, context: TurnContext, activities):

21

"""Send activities in test environment."""

22

23

async def update_activity(self, context: TurnContext, activity):

24

"""Update activity in test environment."""

25

26

async def delete_activity(self, context: TurnContext, reference):

27

"""Delete activity in test environment."""

28

29

async def process_activity(self, activity, logic):

30

"""

31

Process activity with bot logic.

32

33

Args:

34

activity (Activity): Activity to process

35

logic: Bot logic function to execute

36

37

Returns:

38

list: List of response activities

39

"""

40

41

async def test(self, user_says: str, expected_replies, description: str = None, timeout: int = 3):

42

"""

43

Test a single conversation turn.

44

45

Args:

46

user_says (str): User input text

47

expected_replies (str or list): Expected bot responses

48

description (str, optional): Test description

49

timeout (int): Timeout in seconds

50

51

Returns:

52

TestFlow: Test flow for chaining

53

"""

54

55

def make_activity(self, text: str = None):

56

"""

57

Create test activity.

58

59

Args:

60

text (str, optional): Activity text

61

62

Returns:

63

Activity: Test activity

64

"""

65

66

def get_next_reply(self):

67

"""

68

Get next reply from bot.

69

70

Returns:

71

Activity: Next bot reply or None

72

"""

73

74

def activity_buffer(self):

75

"""

76

Get all activities in buffer.

77

78

Returns:

79

list: List of activities

80

"""

81

```

82

83

### TestFlow

84

85

Fluent interface for testing bot conversations that allows chaining multiple conversation turns and assertions for comprehensive bot testing.

86

87

```python { .api }

88

class TestFlow:

89

def __init__(self, test_task, adapter):

90

"""Initialize test flow."""

91

92

def test(self, user_says: str, expected_replies, description: str = None, timeout: int = 3):

93

"""

94

Test a single turn of conversation.

95

96

Args:

97

user_says (str): User input text

98

expected_replies (str or list): Expected bot responses

99

description (str, optional): Test description

100

timeout (int): Timeout in seconds

101

102

Returns:

103

TestFlow: Self for method chaining

104

"""

105

106

def send(self, user_says: str):

107

"""

108

Send user message to bot.

109

110

Args:

111

user_says (str): User input text

112

113

Returns:

114

TestFlow: Self for method chaining

115

"""

116

117

def assert_reply(self, expected_reply, description: str = None, timeout: int = 3):

118

"""

119

Assert bot reply matches expected response.

120

121

Args:

122

expected_reply (str or callable): Expected reply or validator function

123

description (str, optional): Assertion description

124

timeout (int): Timeout in seconds

125

126

Returns:

127

TestFlow: Self for method chaining

128

"""

129

130

def assert_reply_one_of(self, expected_replies, description: str = None, timeout: int = 3):

131

"""

132

Assert bot reply matches one of expected responses.

133

134

Args:

135

expected_replies (list): List of possible expected replies

136

description (str, optional): Assertion description

137

timeout (int): Timeout in seconds

138

139

Returns:

140

TestFlow: Self for method chaining

141

"""

142

143

def assert_no_reply(self, description: str = None, timeout: int = 3):

144

"""

145

Assert bot sends no reply.

146

147

Args:

148

description (str, optional): Assertion description

149

timeout (int): Timeout in seconds

150

151

Returns:

152

TestFlow: Self for method chaining

153

"""

154

155

async def start_test(self):

156

"""

157

Start the test conversation flow.

158

159

Returns:

160

TestFlow: Self for method chaining

161

"""

162

```

163

164

## Usage Examples

165

166

### Basic Bot Testing

167

168

```python

169

import pytest

170

from botbuilder.core import TestAdapter, ActivityHandler, TurnContext, MessageFactory

171

172

class EchoBot(ActivityHandler):

173

async def on_message_activity(self, turn_context: TurnContext):

174

reply_text = f"You said: {turn_context.activity.text}"

175

await turn_context.send_activity(MessageFactory.text(reply_text))

176

177

class TestEchoBot:

178

@pytest.mark.asyncio

179

async def test_echo_response(self):

180

# Create test adapter and bot

181

adapter = TestAdapter()

182

bot = EchoBot()

183

184

# Test single interaction

185

await adapter.test("hello", "You said: hello") \

186

.start_test()

187

188

@pytest.mark.asyncio

189

async def test_multiple_turns(self):

190

adapter = TestAdapter()

191

bot = EchoBot()

192

193

# Test conversation flow

194

await adapter.test("hello", "You said: hello") \

195

.test("how are you?", "You said: how are you?") \

196

.test("goodbye", "You said: goodbye") \

197

.start_test()

198

```

199

200

### Complex Bot Testing

201

202

```python

203

class WeatherBot(ActivityHandler):

204

async def on_message_activity(self, turn_context: TurnContext):

205

text = turn_context.activity.text.lower()

206

207

if "weather" in text:

208

await turn_context.send_activity(MessageFactory.text("It's sunny today!"))

209

elif "hello" in text:

210

await turn_context.send_activity(MessageFactory.text("Hello! Ask me about the weather."))

211

else:

212

await turn_context.send_activity(MessageFactory.text("I can help with weather information."))

213

214

class TestWeatherBot:

215

@pytest.mark.asyncio

216

async def test_weather_query(self):

217

adapter = TestAdapter()

218

bot = WeatherBot()

219

220

await adapter.test("what's the weather?", "It's sunny today!") \

221

.start_test()

222

223

@pytest.mark.asyncio

224

async def test_greeting(self):

225

adapter = TestAdapter()

226

bot = WeatherBot()

227

228

await adapter.test("hello", "Hello! Ask me about the weather.") \

229

.start_test()

230

231

@pytest.mark.asyncio

232

async def test_unknown_input(self):

233

adapter = TestAdapter()

234

bot = WeatherBot()

235

236

await adapter.test("random text", "I can help with weather information.") \

237

.start_test()

238

239

@pytest.mark.asyncio

240

async def test_conversation_flow(self):

241

adapter = TestAdapter()

242

bot = WeatherBot()

243

244

await adapter.test("hi", "Hello! Ask me about the weather.") \

245

.test("weather", "It's sunny today!") \

246

.test("thanks", "I can help with weather information.") \

247

.start_test()

248

```

249

250

### Testing with State

251

252

```python

253

from botbuilder.core import ConversationState, UserState, MemoryStorage

254

255

class CounterBot(ActivityHandler):

256

def __init__(self, conversation_state: ConversationState):

257

self.conversation_state = conversation_state

258

self.count_accessor = conversation_state.create_property("CountProperty")

259

260

async def on_message_activity(self, turn_context: TurnContext):

261

count = await self.count_accessor.get(turn_context, lambda: 0)

262

count += 1

263

await self.count_accessor.set(turn_context, count)

264

265

await turn_context.send_activity(MessageFactory.text(f"Turn {count}"))

266

267

# Save state

268

await self.conversation_state.save_changes(turn_context)

269

270

class TestCounterBot:

271

@pytest.mark.asyncio

272

async def test_counter_increments(self):

273

# Create storage and state

274

storage = MemoryStorage()

275

conversation_state = ConversationState(storage)

276

277

# Create bot with state

278

bot = CounterBot(conversation_state)

279

280

# Create adapter

281

adapter = TestAdapter()

282

283

# Test counter increments

284

await adapter.test("anything", "Turn 1") \

285

.test("something", "Turn 2") \

286

.test("else", "Turn 3") \

287

.start_test()

288

```

289

290

### Testing with Middleware

291

292

```python

293

from botbuilder.core import AutoSaveStateMiddleware

294

295

class TestBotWithMiddleware:

296

@pytest.mark.asyncio

297

async def test_with_auto_save_middleware(self):

298

# Create storage and states

299

storage = MemoryStorage()

300

conversation_state = ConversationState(storage)

301

user_state = UserState(storage)

302

303

# Create adapter and add middleware

304

adapter = TestAdapter()

305

adapter.use(AutoSaveStateMiddleware([conversation_state, user_state]))

306

307

# Create bot

308

bot = CounterBot(conversation_state)

309

310

# Test - state should be auto-saved by middleware

311

await adapter.test("test", "Turn 1") \

312

.test("test2", "Turn 2") \

313

.start_test()

314

```

315

316

### Custom Assertions

317

318

```python

319

class TestAdvancedAssertions:

320

@pytest.mark.asyncio

321

async def test_custom_assertion(self):

322

adapter = TestAdapter()

323

bot = EchoBot()

324

325

# Custom assertion function

326

def validate_echo_response(activity):

327

assert activity.type == "message"

328

assert "You said:" in activity.text

329

assert len(activity.text) > 10

330

331

await adapter.send("hello world") \

332

.assert_reply(validate_echo_response) \

333

.start_test()

334

335

@pytest.mark.asyncio

336

async def test_multiple_possible_responses(self):

337

class RandomBot(ActivityHandler):

338

async def on_message_activity(self, turn_context: TurnContext):

339

import random

340

responses = ["Hello!", "Hi there!", "Greetings!"]

341

reply = random.choice(responses)

342

await turn_context.send_activity(MessageFactory.text(reply))

343

344

adapter = TestAdapter()

345

bot = RandomBot()

346

347

await adapter.send("hi") \

348

.assert_reply_one_of(["Hello!", "Hi there!", "Greetings!"]) \

349

.start_test()

350

351

@pytest.mark.asyncio

352

async def test_no_reply_scenario(self):

353

class SilentBot(ActivityHandler):

354

async def on_message_activity(self, turn_context: TurnContext):

355

# Bot doesn't respond to certain inputs

356

if turn_context.activity.text == "ignore":

357

return # No response

358

await turn_context.send_activity(MessageFactory.text("I heard you"))

359

360

adapter = TestAdapter()

361

bot = SilentBot()

362

363

await adapter.send("ignore") \

364

.assert_no_reply() \

365

.send("hello") \

366

.assert_reply("I heard you") \

367

.start_test()

368

```

369

370

### Testing Exception Handling

371

372

```python

373

class TestErrorHandling:

374

@pytest.mark.asyncio

375

async def test_bot_exception_handling(self):

376

class ErrorBot(ActivityHandler):

377

async def on_message_activity(self, turn_context: TurnContext):

378

if turn_context.activity.text == "error":

379

raise ValueError("Test error")

380

await turn_context.send_activity(MessageFactory.text("OK"))

381

382

adapter = TestAdapter()

383

bot = ErrorBot()

384

385

# Test that exception is properly handled

386

with pytest.raises(ValueError, match="Test error"):

387

await adapter.send("error").start_test()

388

389

# Test normal operation still works

390

await adapter.send("normal").assert_reply("OK").start_test()

391

```

392

393

### Performance Testing

394

395

```python

396

import time

397

398

class TestPerformance:

399

@pytest.mark.asyncio

400

async def test_response_time(self):

401

class SlowBot(ActivityHandler):

402

async def on_message_activity(self, turn_context: TurnContext):

403

# Simulate processing time

404

await asyncio.sleep(0.1)

405

await turn_context.send_activity(MessageFactory.text("Processed"))

406

407

adapter = TestAdapter()

408

bot = SlowBot()

409

410

start_time = time.time()

411

await adapter.test("test", "Processed").start_test()

412

duration = time.time() - start_time

413

414

# Assert response time is reasonable

415

assert duration < 1.0, f"Bot took too long to respond: {duration}s"

416

```

417

418

## Types

419

420

```python { .api }

421

class TestActivityInspector:

422

"""Helper for inspecting test activities."""

423

424

@staticmethod

425

def assert_message_activity(activity, text: str = None):

426

"""Assert activity is a message with optional text check."""

427

assert activity.type == "message"

428

if text:

429

assert activity.text == text

430

431

@staticmethod

432

def assert_suggested_actions(activity, expected_actions):

433

"""Assert activity has expected suggested actions."""

434

assert activity.suggested_actions is not None

435

actual_actions = [action.title for action in activity.suggested_actions.actions]

436

assert actual_actions == expected_actions

437

```