Skip to content

Commit 6ce2a97

Browse files
Integration Testing Framework (#228)
* basic integration utility * Integration test suite factory implementation * Implementing core models for integration testing * AutoClient mockup * Adding runner starter code * Adding foundational classes * Drafting AgentClient and ResponseClient implementations * Spec test * Filling in more implementation details * More files * Cleaning up implementations * Adding expect replies sending method * Beginning unit tests * Adding integration decor from sample test cases * Integration from service url tests * _handle_conversation implementation for response_client * AgentClient tests completed * Creating response client tests * Hosting server for response client * Response client tests completed * Beginning refactor of aiohttp runner * Unit test updates * Fixing issues in refactor * Fixed TestIntegrationFromSample unit test * Another commit * Reorganizing files * Completed TestIntegrationFromServiceUrl unit tests * Reformatting with black * Quickstart integration test beginning * Draft of quickstart sample setup * Environment config connection * Renaming messaging endpoint to agent url * Removing unnecessary files * Fixing agent client payload sending * First successful integration test * Beginning foundational test cases * TypingIndicator test * Adding more test cases * More foundational integration test cases * Reorganizing testing tools into package * Polished the testing framework package * Adding verbose logging for results with benchmark tool * General cleanup * Adding README * Revising README * Formatting * Addressing PR comments * Removing unused import * Update dev/microsoft-agents-testing/microsoft_agents/testing/sdk_config.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dev/microsoft-agents-testing/pyproject.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dev/microsoft-agents-testing/microsoft_agents/testing/auth/generate_token.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dev/microsoft-agents-testing/microsoft_agents/testing/integration/core/environment.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Removing unused code * Writing draft README for testing package * Second pass for README.md * Removing link to PyPI --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 4ac1998 commit 6ce2a97

45 files changed

Lines changed: 1558 additions & 6 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dev/README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
1-
This directory contains tools to aid the developers of the Microsoft 365 Agents SDK for Python.
1+
# Development Tools
22

3-
### `benchmark`
3+
Development utilities for the Microsoft Agents for Python project.
44

5-
This folder contains benchmarking utilities built in Python to send concurrent requests
6-
to an agent.
5+
## Contents
6+
7+
- **[`install.sh`](install.sh)** - Installs testing framework in editable mode
8+
- **[`benchmark/`](benchmark/)** - Performance testing and stress testing tools
9+
- **[`microsoft-agents-testing/`](microsoft-agents-testing/)** - Testing framework package
10+
11+
## Quick Setup
12+
13+
```bash
14+
./install.sh
15+
```
16+
17+
## Benchmarking
18+
19+
Performance testing tools with support for concurrent workers and authentication. Requires a running agent instance and Azure Bot Service credentials.
20+
21+
See [benchmark/README.md](benchmark/README.md) for setup and usage details.
22+
23+
## Testing Framework
24+
25+
Provides testing utilities and helpers for Microsoft Agents development. Installed in editable mode for active development.

dev/benchmark/__init__.py

Whitespace-only changes.

dev/benchmark/payload.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
"id": "user-id-0",
1616
"aadObjectId": "00000000-0000-0000-0000-0000000000020"
1717
},
18-
"type": "message"
18+
"type": "message",
19+
"text": "Hello, Bot!"
1920
}

dev/benchmark/src/main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .executor import Executor, CoroutineExecutor, ThreadExecutor
99
from .aggregated_results import AggregatedResults
1010
from .config import BenchmarkConfig
11+
from .output import output_results
1112

1213
LOG_FORMAT = "%(asctime)s: %(message)s"
1314
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO, datefmt="%H:%M:%S")
@@ -20,13 +21,14 @@
2021
"--payload_path", "-p", default="./payload.json", help="Path to the payload file."
2122
)
2223
@click.option("--num_workers", "-n", default=1, help="Number of workers to use.")
24+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging.")
2325
@click.option(
2426
"--async_mode",
2527
"-a",
2628
is_flag=True,
2729
help="Run coroutine workers rather than thread workers.",
2830
)
29-
def main(payload_path: str, num_workers: int, async_mode: bool):
31+
def main(payload_path: str, num_workers: int, verbose: bool, async_mode: bool):
3032
"""Main function to run the benchmark."""
3133

3234
with open(payload_path, "r", encoding="utf-8") as f:
@@ -39,6 +41,8 @@ def main(payload_path: str, num_workers: int, async_mode: bool):
3941
start_time = datetime.now(timezone.utc).timestamp()
4042
results = executor.run(func, num_workers=num_workers)
4143
end_time = datetime.now(timezone.utc).timestamp()
44+
if verbose:
45+
output_results(results)
4246

4347
agg = AggregatedResults(results)
4448
agg.display(start_time, end_time)

dev/benchmark/src/output.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from .executor import ExecutionResult
2+
3+
4+
def output_results(results: list[ExecutionResult]) -> None:
5+
"""Output the results of the benchmark to the console."""
6+
7+
for result in results:
8+
status = "Success" if result.success else "Failure"
9+
print(
10+
f"Execution ID: {result.exe_id}, Duration: {result.duration:.4f} seconds, Status: {status}"
11+
)
12+
print(result.result)

dev/install.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pip install -e ./microsoft-agents-testing/ --config-settings editable_mode=compat
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Microsoft 365 Agents SDK for Python - Testing Framework
2+
3+
A comprehensive testing framework designed specifically for Microsoft 365 Agents SDK, providing essential utilities and abstractions to streamline integration testing, authentication, and end-to-end agent validation.
4+
5+
## Why This Package Exists
6+
7+
Building and testing conversational agents presents unique challenges that standard testing frameworks don't address. This package eliminates these pain points by providing useful abstractions specifically designed for agent testing scenarios.
8+
9+
## Key Features
10+
11+
### 🔐 Authentication Utilities
12+
- **OAuth2 Token Generation**: Generate access tokens using client credentials flow
13+
- **Configuration-Based Auth**: Load credentials from environment variables or config objects
14+
- **MSAL Integration**: Built-in support for Microsoft Authentication Library
15+
16+
```python
17+
from microsoft_agents.testing import generate_token, generate_token_from_config
18+
19+
# Generate token directly
20+
token = generate_token(
21+
app_id="your-app-id",
22+
app_secret="your-secret",
23+
tenant_id="your-tenant"
24+
)
25+
26+
# Or from SDK config
27+
token = generate_token_from_config(sdk_config)
28+
```
29+
30+
### 🧪 Integration Test Framework
31+
- **Pytest Fixtures**: Pre-built fixtures for common test scenarios
32+
- **Environment Abstraction**: Reusable environment setup for different hosting configurations
33+
- **Sample Management**: Base classes for organizing test samples and configurations
34+
- **Application Runners**: Abstract server lifecycle management for integration tests
35+
36+
```python
37+
from microsoft_agents.testing import Integration, Environment, Sample
38+
39+
class MyAgentTests(Integration):
40+
_sample_cls = MyAgentSample
41+
_environment_cls = AiohttpEnvironment
42+
43+
@pytest.mark.asyncio
44+
async def test_conversation_flow(self, agent_client, sample):
45+
# Client and sample are automatically set up via fixtures
46+
response = await agent_client.send_activity("Hello")
47+
assert response is not None
48+
```
49+
50+
### 🤖 Agent Communication Clients
51+
- **AgentClient**: High-level client for sending Activities to agents
52+
- **ResponseClient**: Handle responses from agent services
53+
- **Automatic Token Management**: Clients handle authentication automatically
54+
- **Delivery Mode Support**: Test both standard and `ExpectReplies` delivery patterns
55+
56+
```python
57+
from microsoft_agents.testing import AgentClient
58+
59+
client = AgentClient(
60+
agent_url="http://localhost:3978",
61+
cid="conversation-id",
62+
client_id="your-client-id",
63+
tenant_id="your-tenant-id",
64+
client_secret="your-secret"
65+
)
66+
67+
# Send simple text message
68+
response = await client.send_activity("What's the weather?")
69+
70+
# Send Activity with ExpectReplies
71+
replies = await client.send_expect_replies(
72+
Activity(type=ActivityTypes.message, text="Hello")
73+
)
74+
```
75+
76+
### 🛠️ Testing Utilities
77+
- **Activity Population**: Automatically fill default Activity properties for testing
78+
- **URL Parsing**: Extract host and port from service URLs
79+
- **Configuration Management**: Centralized SDK configuration for tests
80+
81+
```python
82+
from microsoft_agents.testing import populate_activity, get_host_and_port
83+
84+
# Populate test activity with defaults
85+
activity = populate_activity(
86+
Activity(text="Hello"),
87+
defaults={"service_url": "http://localhost", "channel_id": "test"}
88+
)
89+
90+
# Parse service URLs
91+
host, port = get_host_and_port("http://localhost:3978/api/messages")
92+
```
93+
94+
## Who Should Use This Package
95+
96+
- **Agent Developers**: Testing agents built with `microsoft-agents-hosting-core` and related packages
97+
- **QA Engineers**: Writing integration and E2E tests for conversational AI systems
98+
- **DevOps Teams**: Automating agent validation in CI/CD pipelines
99+
- **Sample Authors**: Creating reproducible examples and documentation
100+
101+
## Integration with CI/CD
102+
103+
This package is designed for seamless integration into continuous integration pipelines:
104+
105+
```yaml
106+
# Example: GitHub Actions
107+
- name: Run Agent Integration Tests
108+
run: |
109+
pip install microsoft-agents-testing pytest pytest-asyncio
110+
pytest tests/integration/ -v
111+
env:
112+
CLIENT_ID: ${{ secrets.AGENT_CLIENT_ID }}
113+
CLIENT_SECRET: ${{ secrets.AGENT_CLIENT_SECRET }}
114+
TENANT_ID: ${{ secrets.TENANT_ID }}
115+
```
116+
117+
## Quick Start Example
118+
119+
```python
120+
import pytest
121+
from microsoft_agents.testing import Integration, AiohttpEnvironment, Sample
122+
from microsoft_agents.activity import Activity
123+
124+
class MyAgentSample(Sample):
125+
async def init_app(self):
126+
# Initialize your agent application
127+
self.app = create_my_agent_app(self.env)
128+
129+
@classmethod
130+
async def get_config(cls):
131+
return {"service_url": "http://localhost:3978"}
132+
133+
class TestMyAgent(Integration):
134+
_sample_cls = MyAgentSample
135+
_environment_cls = AiohttpEnvironment
136+
137+
_agent_url = "http://localhost:3978"
138+
_cid = "test-conversation"
139+
140+
@pytest.mark.asyncio
141+
async def test_greeting(self, agent_client):
142+
response = await agent_client.send_activity("Hello")
143+
assert "Hi there" in response
144+
145+
@pytest.mark.asyncio
146+
async def test_conversation(self, agent_client):
147+
replies = await agent_client.send_expect_replies("What can you do?")
148+
assert len(replies) > 0
149+
assert replies[0].type == "message"
150+
```
151+
152+
## Related Packages
153+
154+
This package complements the Microsoft 365 Agents SDK ecosystem:
155+
156+
- `microsoft-agents-activity`: Activity types and protocols
157+
- `microsoft-agents-hosting-core`: Core hosting framework
158+
- `microsoft-agents-hosting-aiohttp`: aiohttp hosting integration
159+
- `microsoft-agents-authentication-msal`: MSAL authentication
160+
161+
## Contributing
162+
163+
This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA). For details, visit [https://cla.opensource.microsoft.com](https://cla.opensource.microsoft.com).
164+
165+
## License
166+
167+
MIT
168+
169+
## Support
170+
171+
For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/microsoft/Agents-for-python).
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from .sdk_config import SDKConfig
2+
3+
from .auth import generate_token, generate_token_from_config
4+
5+
from .utils import populate_activity, get_host_and_port
6+
7+
from .integration import (
8+
Sample,
9+
Environment,
10+
ApplicationRunner,
11+
AgentClient,
12+
ResponseClient,
13+
AiohttpEnvironment,
14+
Integration,
15+
)
16+
17+
__all__ = [
18+
"SDKConfig",
19+
"generate_token",
20+
"generate_token_from_config",
21+
"Sample",
22+
"Environment",
23+
"ApplicationRunner",
24+
"AgentClient",
25+
"ResponseClient",
26+
"AiohttpEnvironment",
27+
"Integration",
28+
"populate_activity",
29+
"get_host_and_port",
30+
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .generate_token import generate_token, generate_token_from_config
2+
3+
__all__ = ["generate_token", "generate_token_from_config"]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import requests
2+
3+
from microsoft_agents.hosting.core import AgentAuthConfiguration
4+
from microsoft_agents.testing.sdk_config import SDKConfig
5+
6+
7+
def generate_token(app_id: str, app_secret: str, tenant_id: str) -> str:
8+
"""Generate a token using the provided app credentials.
9+
10+
:param app_id: Application (client) ID.
11+
:param app_secret: Application client secret.
12+
:param tenant_id: Directory (tenant) ID.
13+
:return: Generated access token as a string.
14+
"""
15+
16+
authority_endpoint = (
17+
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
18+
)
19+
20+
res = requests.post(
21+
authority_endpoint,
22+
headers={
23+
"Content-Type": "application/x-www-form-urlencoded",
24+
},
25+
data={
26+
"grant_type": "client_credentials",
27+
"client_id": app_id,
28+
"client_secret": app_secret,
29+
"scope": f"{app_id}/.default",
30+
},
31+
timeout=10,
32+
)
33+
return res.json().get("access_token")
34+
35+
36+
def generate_token_from_config(sdk_config: SDKConfig) -> str:
37+
"""Generates a token using a provided config object.
38+
39+
:param sdk_config: Configuration dictionary containing connection settings.
40+
:return: Generated access token as a string.
41+
"""
42+
43+
settings: AgentAuthConfiguration = sdk_config.get_connection()
44+
45+
app_id = settings.CLIENT_ID
46+
app_secret = settings.CLIENT_SECRET
47+
tenant_id = settings.TENANT_ID
48+
49+
if not app_id or not app_secret or not tenant_id:
50+
raise ValueError("Incorrect configuration provided for token generation.")
51+
return generate_token(app_id, app_secret, tenant_id)

0 commit comments

Comments
 (0)