forked from themanojdesai/python-a2a
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtesting_agents.py
More file actions
553 lines (446 loc) · 19.3 KB
/
testing_agents.py
File metadata and controls
553 lines (446 loc) · 19.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
#!/usr/bin/env python
"""
Testing A2A Agents Example
This example demonstrates how to properly test A2A agents using various
testing strategies including unit tests, mock clients, and test fixtures.
It shows best practices for verifying agent functionality programmatically.
To run:
python testing_agents.py
Requirements:
pip install "python-a2a[server]" pytest
Note: This script includes examples of unit tests that would typically be
run with pytest, but also includes functionality to run them directly.
"""
import sys
import os
import argparse
import unittest
import json
from unittest.mock import MagicMock, patch
import time
def check_dependencies():
"""Check if required dependencies are installed"""
missing_deps = []
try:
import python_a2a
except ImportError:
missing_deps.append("python-a2a")
try:
import pytest
except ImportError:
missing_deps.append("pytest")
if missing_deps:
print("❌ Missing dependencies:")
for dep in missing_deps:
print(f" - {dep}")
print("\nPlease install the required packages:")
print(" pip install \"python-a2a[server]\" pytest")
print("\nThen run this example again.")
return False
print("✅ Dependencies installed correctly!")
return True
def parse_arguments():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(description="Testing A2A Agents Example")
parser.add_argument(
"--run-tests", action="store_true",
help="Run all tests"
)
parser.add_argument(
"--test-type", type=str, choices=["unit", "mock", "integration", "all"],
default="all",
help="Type of tests to run (default: all)"
)
return parser.parse_args()
# --- Define a Simple Calculator Agent for Testing ---
class CalculatorAgent:
"""A simple calculator agent for testing purposes"""
def __init__(self):
from python_a2a import AgentCard, AgentSkill, A2AServer
# Create an agent card
self.agent_card = AgentCard(
name="Calculator Agent",
description="Performs basic calculations",
url="http://localhost:5000",
version="1.0.0",
skills=[
AgentSkill(
name="Add",
description="Add two numbers",
examples=["Add 5 and 3", "What is 2 + 2?"]
),
AgentSkill(
name="Subtract",
description="Subtract one number from another",
examples=["Subtract 7 from 10", "What is 20 - 5?"]
),
AgentSkill(
name="Multiply",
description="Multiply two numbers",
examples=["Multiply 4 by 6", "What is 7 * 8?"]
),
AgentSkill(
name="Divide",
description="Divide one number by another",
examples=["Divide 15 by 3", "What is 100 / 5?"]
)
]
)
def add(self, a, b):
"""Add two numbers"""
return a + b
def subtract(self, a, b):
"""Subtract b from a"""
return a - b
def multiply(self, a, b):
"""Multiply two numbers"""
return a * b
def divide(self, a, b):
"""Divide a by b"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def handle_task(self, task):
"""Process incoming tasks"""
from python_a2a import TaskStatus, TaskState
# Extract message text
message_data = task.message or {}
content = message_data.get("content", {})
text = content.get("text", "") if isinstance(content, dict) else ""
# Default response
response_text = "I can perform basic calculations. Try asking something like 'Add 5 and 3' or 'What is 7 * 8?'"
# Parse for operation and numbers
import re
# Extract numbers from the text
numbers = re.findall(r"[-+]?\d*\.?\d+", text)
if len(numbers) >= 2:
a = float(numbers[0])
b = float(numbers[1])
# Determine operation from text
text_lower = text.lower()
try:
if any(op in text_lower for op in ["add", "plus", "sum", "+"]):
result = self.add(a, b)
response_text = f"{a} + {b} = {result}"
elif any(op in text_lower for op in ["subtract", "minus", "difference", "-"]):
result = self.subtract(a, b)
response_text = f"{a} - {b} = {result}"
elif any(op in text_lower for op in ["multiply", "times", "product", "*", "x"]):
result = self.multiply(a, b)
response_text = f"{a} * {b} = {result}"
elif any(op in text_lower for op in ["divide", "quotient", "/"]):
result = self.divide(a, b)
response_text = f"{a} / {b} = {result}"
except Exception as e:
response_text = f"Error: {str(e)}"
# Create response artifact
task.artifacts = [{
"parts": [{"type": "text", "text": response_text}]
}]
# Set task status
task.status = TaskStatus(state=TaskState.COMPLETED)
return task
# --- Unit Tests for Calculator Agent ---
class TestCalculatorAgent(unittest.TestCase):
"""Unit tests for the Calculator Agent"""
def setUp(self):
"""Set up test fixtures"""
from python_a2a import Task, TaskStatus, TaskState
# Create an instance of the agent
self.agent = CalculatorAgent()
# Create a mock task for testing
self.task = MagicMock(spec=Task)
self.task.artifacts = None
self.task.status = None
def test_add(self):
"""Test the add method"""
# Test integer addition
self.assertEqual(self.agent.add(5, 3), 8)
# Test float addition
self.assertEqual(self.agent.add(2.5, 3.5), 6.0)
# Test negative numbers
self.assertEqual(self.agent.add(-5, 10), 5)
def test_subtract(self):
"""Test the subtract method"""
# Test integer subtraction
self.assertEqual(self.agent.subtract(10, 4), 6)
# Test float subtraction
self.assertEqual(self.agent.subtract(5.5, 2.5), 3.0)
# Test negative result
self.assertEqual(self.agent.subtract(5, 10), -5)
def test_multiply(self):
"""Test the multiply method"""
# Test integer multiplication
self.assertEqual(self.agent.multiply(6, 7), 42)
# Test float multiplication
self.assertEqual(self.agent.multiply(2.5, 4.0), 10.0)
# Test with zero
self.assertEqual(self.agent.multiply(5, 0), 0)
def test_divide(self):
"""Test the divide method"""
# Test integer division
self.assertEqual(self.agent.divide(10, 2), 5)
# Test float division
self.assertEqual(self.agent.divide(5, 2), 2.5)
# Test division by zero
with self.assertRaises(ValueError):
self.agent.divide(5, 0)
def test_handle_task_add(self):
"""Test task handling for addition"""
# Set up task message
self.task.message = {"content": {"type": "text", "text": "Add 5 and 3"}}
# Call handle_task
result = self.agent.handle_task(self.task)
# Check task was updated with correct response
self.assertIsNotNone(result.artifacts)
self.assertEqual(len(result.artifacts), 1)
self.assertEqual(result.artifacts[0]["parts"][0]["text"], "5.0 + 3.0 = 8.0")
# Check task status was updated
from python_a2a import TaskState
self.assertEqual(result.status.state, TaskState.COMPLETED)
def test_handle_task_invalid(self):
"""Test task handling for invalid input"""
# Set up task message without numbers
self.task.message = {"content": {"type": "text", "text": "Hello, how are you?"}}
# Call handle_task
result = self.agent.handle_task(self.task)
# Check default response was used
self.assertIsNotNone(result.artifacts)
self.assertEqual(len(result.artifacts), 1)
self.assertTrue("I can perform basic calculations" in result.artifacts[0]["parts"][0]["text"])
# --- Mock Client Tests ---
class TestCalculatorAgentWithMockClient(unittest.TestCase):
"""Tests using a mock A2A client to simulate client interactions"""
def setUp(self):
"""Set up test fixtures"""
# Create a patched A2AClient
self.patcher = patch('python_a2a.A2AClient')
self.mock_client_class = self.patcher.start()
# Create a mock client instance
self.mock_client = self.mock_client_class.return_value
# Set up agent_card property on the mock
from python_a2a import AgentCard
self.mock_client.agent_card = AgentCard(
name="Calculator Agent",
description="Performs basic calculations",
url="http://mock-url",
version="1.0.0"
)
def tearDown(self):
"""Clean up after tests"""
self.patcher.stop()
def test_client_ask_add(self):
"""Test client.ask for addition"""
# Set up the mock to return a specific response
self.mock_client.ask.return_value = "5 + 3 = 8"
# Use the mock client
response = self.mock_client.ask("Add 5 and 3")
# Verify the response
self.assertEqual(response, "5 + 3 = 8")
# Verify the mock was called with the right argument
self.mock_client.ask.assert_called_once_with("Add 5 and 3")
def test_client_ask_error(self):
"""Test client error handling"""
# Set up the mock to raise an exception
self.mock_client.ask.side_effect = Exception("Connection error")
# Check that the exception is propagated
with self.assertRaises(Exception):
response = self.mock_client.ask("Add 5 and 3")
# --- Integration Tests ---
def setup_test_server():
"""Set up a test server with our calculator agent"""
import multiprocessing
from python_a2a import A2AServer, run_server
# Create a proper A2A server using our calculator agent
class A2ACalculatorAgent(A2AServer):
def __init__(self):
calculator = CalculatorAgent()
super().__init__(agent_card=calculator.agent_card)
self.calculator = calculator
def handle_task(self, task):
return self.calculator.handle_task(task)
# Find an available port
import socket
def find_free_port():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', 0))
return s.getsockname()[1]
port = find_free_port()
# Start the server in a separate process
server = A2ACalculatorAgent()
process = multiprocessing.Process(
target=run_server,
args=(server,),
kwargs={"port": port, "host": "localhost"}
)
process.start()
# Wait for server to start
time.sleep(1)
return process, port
def run_integration_tests(port):
"""Run integration tests against a live server"""
from python_a2a import A2AClient
print("Running integration tests against live server...")
results = []
try:
# Create a real client connected to our test server
client = A2AClient(f"http://localhost:{port}")
# Test 1: Get agent card
try:
agent_card = client.agent_card
if agent_card.name == "Calculator Agent":
results.append(("Get agent card", "✅ Success"))
else:
results.append(("Get agent card", f"❌ Failed - Expected 'Calculator Agent', got '{agent_card.name}'"))
except Exception as e:
results.append(("Get agent card", f"❌ Failed - {str(e)}"))
# Test 2: Addition
try:
response = client.ask("Add 5 and 3")
if "5" in response and "3" in response and "8" in response:
results.append(("Addition", "✅ Success"))
else:
results.append(("Addition", f"❌ Failed - Expected response with '8', got '{response}'"))
except Exception as e:
results.append(("Addition", f"❌ Failed - {str(e)}"))
# Test 3: Subtraction
try:
response = client.ask("What is 10 - 4?")
if "10" in response and "4" in response and "6" in response:
results.append(("Subtraction", "✅ Success"))
else:
results.append(("Subtraction", f"❌ Failed - Expected response with '6', got '{response}'"))
except Exception as e:
results.append(("Subtraction", f"❌ Failed - {str(e)}"))
# Test 4: Multiplication
try:
response = client.ask("Multiply 7 by 8")
if "7" in response and "8" in response and "56" in response:
results.append(("Multiplication", "✅ Success"))
else:
results.append(("Multiplication", f"❌ Failed - Expected response with '56', got '{response}'"))
except Exception as e:
results.append(("Multiplication", f"❌ Failed - {str(e)}"))
# Test 5: Division
try:
response = client.ask("Divide 20 by 5")
if "20" in response and "5" in response and "4" in response:
results.append(("Division", "✅ Success"))
else:
results.append(("Division", f"❌ Failed - Expected response with '4', got '{response}'"))
except Exception as e:
results.append(("Division", f"❌ Failed - {str(e)}"))
# Test 6: Division by zero
try:
response = client.ask("Divide 10 by 0")
if "error" in response.lower() or "cannot" in response.lower():
results.append(("Division by zero", "✅ Success - Error handled correctly"))
else:
results.append(("Division by zero", f"❌ Failed - Expected error message, got '{response}'"))
except Exception as e:
# If this raises an exception, that's also acceptable
results.append(("Division by zero", "✅ Success - Exception raised as expected"))
except Exception as e:
results.append(("Client connection", f"❌ Failed - {str(e)}"))
return results
def main():
# Check dependencies
if not check_dependencies():
return 1
# Parse command line arguments
args = parse_arguments()
print("\n🧪 Testing A2A Agents Example 🧪")
print("Learn how to test A2A agents using various strategies\n")
# Run unit tests
if args.run_tests and args.test_type in ["unit", "all"]:
print("\n=== Running Unit Tests ===")
unittest.TextTestRunner().run(unittest.makeSuite(TestCalculatorAgent))
else:
print("\n=== Unit Test Examples ===")
print("Unit tests for the Calculator Agent methods:")
print("- test_add: Test addition functionality")
print("- test_subtract: Test subtraction functionality")
print("- test_multiply: Test multiplication functionality")
print("- test_divide: Test division functionality")
print("- test_handle_task_add: Test handling tasks with addition")
print("- test_handle_task_invalid: Test handling invalid inputs")
# Run mock client tests
if args.run_tests and args.test_type in ["mock", "all"]:
print("\n=== Running Mock Client Tests ===")
unittest.TextTestRunner().run(unittest.makeSuite(TestCalculatorAgentWithMockClient))
else:
print("\n=== Mock Client Test Examples ===")
print("Tests using mock A2A clients:")
print("- test_client_ask_add: Test client addition request")
print("- test_client_ask_error: Test client error handling")
# Run integration tests
if args.run_tests and args.test_type in ["integration", "all"]:
print("\n=== Running Integration Tests ===")
# Set up the test server
server_process, port = setup_test_server()
try:
# Run integration tests
results = run_integration_tests(port)
# Display results
print("\nIntegration Test Results:")
for test_name, result in results:
print(f"{test_name}: {result}")
finally:
# Clean up the server process
print("\nStopping test server...")
server_process.terminate()
server_process.join(timeout=2)
print("Test server stopped")
else:
print("\n=== Integration Test Examples ===")
print("Tests against a live A2A server:")
print("- Get agent card: Verify agent information")
print("- Addition: Test adding two numbers")
print("- Subtraction: Test subtracting two numbers")
print("- Multiplication: Test multiplying two numbers")
print("- Division: Test dividing two numbers")
print("- Division by zero: Test error handling for division by zero")
# Show usage patterns
print("\n=== Test Strategy Best Practices ===")
print("1. Unit Testing:")
print(" - Test individual agent methods in isolation")
print(" - Use unittest or pytest frameworks")
print(" - Create a test fixture (setUp) with a clean environment")
print("\n2. Mock Testing:")
print(" - Use unittest.mock to create mock clients")
print(" - Test client-agent interactions without a live server")
print(" - Simulate various response scenarios")
print("\n3. Integration Testing:")
print(" - Test against a live A2A server")
print(" - Use multiprocessing to manage test servers")
print(" - Verify end-to-end workflows")
print("\n=== Example Directory Structure ===")
print("""
project/
├── my_agent/
│ ├── __init__.py
│ ├── agent.py
│ └── skills.py
└── tests/
├── __init__.py
├── test_agent_methods.py
├── test_client_interactions.py
└── test_integration.py
""")
print("\n=== Example pytest Command ===")
print("Run all tests with: pytest tests/")
print("Run specific test file: pytest tests/test_agent_methods.py")
print("Run with verbose output: pytest -v tests/")
print("Run with coverage report: pytest --cov=my_agent tests/")
print("\n=== What's Next? ===")
print("1. Try creating tests for your own A2A agents")
print("2. Set up continuous integration to run tests automatically")
print("3. Try the 'cli_tools.py' example to learn about command-line tools")
print("\n🧪 Test exploration complete! 🧪")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
print("\n✅ Program interrupted by user")
sys.exit(0)