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
```