Skip to content

New testing utilities package with mock implementations for integration testing - #503

Draft
Rodrigo Brandão (rodrigobr-msft) wants to merge 8 commits into
mainfrom
users/robrandao/mocks
Draft

New testing utilities package with mock implementations for integration testing#503
Rodrigo Brandão (rodrigobr-msft) wants to merge 8 commits into
mainfrom
users/robrandao/mocks

Conversation

@rodrigobr-msft

Copy link
Copy Markdown
Contributor

This pull request introduces the new microsoft_agents.testing package, providing a comprehensive test utility suite for Microsoft Agents, and makes improvements to typing and protocol definitions in related libraries. The key changes include the addition of a mock user token client for testing OAuth/token flows, improvements to type annotations for better clarity and type safety, and the inclusion of licensing and packaging files for the new testing package.

Testing utilities (new package):

  • Added the microsoft_agents.testing package, including MockUserTokenClient, TestAdapter, and TestFlow to facilitate in-memory testing of user tokens and OAuth flows without external dependencies. This includes a full-featured mock for UserTokenClientBase and supporting data structures. [1][2][3][4][5]
  • Added licensing (LICENSE) and packaging (MANIFEST.in) files, along with copyright headers, to ensure compliance and proper distribution of the new testing utilities. [1][2]

Type annotations and protocol improvements:

  • Updated the use method in both ChannelAdapterProtocol and ChannelAdapter to use Self from typing_extensions for more accurate type annotations, improving subclassing and chaining support. [1][2][3][4]
  • Simplified and clarified type annotations in TokenStatus, removing unnecessary imports and updating type hints to use standard Python union syntax (e.g., str | None). [1][2]

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new microsoft_agents.testing package to enable in-memory/integration-style testing of agent turns (including mocked OAuth/token flows), and updates a few type annotations in existing core/protocol libraries to improve typing ergonomics.

Changes:

  • Added a new microsoft-agents-testing library with TestAdapter, TestFlow, and MockUserTokenClient.
  • Added tests for the new TestFlow / adapter reply-queue behavior.
  • Updated adapter/protocol typing (use() returns Self) and simplified TokenStatus type annotations.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
tests/testing_package/test_test_flow.pyAdds async pytest coverage for TestFlow fluent chaining and adapter reply queuing.
tests/testing_package/init.pyAdds test package marker for the new testing-package test suite.
tests/_common/testing_objects/mocks/mock_user_token_client.pyMinor formatting cleanup for an existing test mock.
libraries/microsoft-agents-testing/setup.pyAdds packaging entrypoint and install requirements for the new testing package.
libraries/microsoft-agents-testing/readme.mdAdds package README for distribution/documentation.
libraries/microsoft-agents-testing/pyproject.tomlAdds PEP 621 project metadata for the new testing package.
libraries/microsoft-agents-testing/microsoft_agents/testing/type_def.pyIntroduces shared typing aliases for the testing helpers.
libraries/microsoft-agents-testing/microsoft_agents/testing/test_flow.pyImplements fluent TestFlow send/assert helpers for scripted agent testing.
libraries/microsoft-agents-testing/microsoft_agents/testing/test_adapter.pyImplements the in-memory adapter that runs the normal pipeline and captures outgoing activities.
libraries/microsoft-agents-testing/microsoft_agents/testing/auth/mock_user_token_client.pyAdds an in-memory UserTokenClientBase implementation for OAuth/token-flow testing.
libraries/microsoft-agents-testing/microsoft_agents/testing/auth/_types.pyAdds dataclasses used as keys/records for the token mock’s internal stores.
libraries/microsoft-agents-testing/microsoft_agents/testing/auth/init.pyExports the auth testing utilities.
libraries/microsoft-agents-testing/microsoft_agents/testing/_defaults.pyAdds default conversation/user/bot constants for deterministic tests.
libraries/microsoft-agents-testing/microsoft_agents/testing/init.pyExports the primary testing surface area (MockUserTokenClient, TestAdapter, TestFlow).
libraries/microsoft-agents-testing/MANIFEST.inAdds packaging include rules (VERSION.txt).
libraries/microsoft-agents-testing/LICENSEAdds license file for distribution compliance.
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.pyUpdates use() to return Self for better chaining/subclass typing.
libraries/microsoft-agents-activity/microsoft_agents/activity/token_status.pySimplifies TokenStatus type annotations (relies on camel-case alias generator).
libraries/microsoft-agents-activity/microsoft_agents/activity/channel_adapter_protocol.pyUpdates protocol use() return type to Self for accurate fluent typing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlibraries/microsoft-agents-testing/pyproject.toml Outdated
Comment threadlibraries/microsoft-agents-testing/setup.py
CopilotAI review requested due to automatic review settings July 28, 2026 15:56

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 28 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

libraries/microsoft-agents-testing/setup.py:10

  • When VERSION.txt is missing and PackageVersion isn't set, package_version falls back to "0.0.0", which makes install_requires pin microsoft-agents-hosting-core==0.0.0 and will typically fail dependency resolution for local/source installs. Also, aiohttp is declared as a hard dependency but this package doesn't import it anywhere (only setup.py references it), which unnecessarily expands the install surface.
 package_version = environ.get("PackageVersion", "0.0.0")

libraries/microsoft-agents-testing/microsoft_agents/testing/auth/mock_user_token_client.py:356

  • exchange_token() uses is to compare the _RAISE_EXCEPTION sentinel. Since this is a string value stored in a dict, identity comparison is not guaranteed; use equality so the sentinel check is reliable.
 if key in self._exchangable_tokens:
token = self._exchangable_tokens[key]
if token is _RAISE_EXCEPTION:
raise Exception("Simulated exception during token exchange.")

libraries/microsoft-agents-testing/microsoft_agents/testing/test_adapter.py:232

  • process_activity() overwrites activity.id unconditionally. This breaks scenarios where tests intentionally supply an ID (and also causes IDs to be generated twice for send_text_to_bot(), since create_activity() already assigns an ID). Only assign an ID when the inbound activity doesn't already have one.
 activity.recipient = self._conversation.agent
activity.conversation = self._conversation.conversation
activity.service_url = self._conversation.service_url
activity.id = self._gen_id()

tests/hosting_msteams/helpers.py:41

  • return [ResourceResponse()] * len(activities) repeats the same ResourceResponse instance for every activity. If any caller mutates a response (or inspects IDs), this can produce incorrect behavior. Return a distinct ResourceResponse per activity instead.
 async def send_activities(self, context, activities):
self.sent_activities.extend(activities)
return [ResourceResponse()] * len(activities)

CopilotAI review requested due to automatic review settings July 28, 2026 16:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 30 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

libraries/microsoft-agents-testing/microsoft_agents/testing/auth/mock_user_token_client.py:356

  • In MockUserTokenClient.exchange_token(), the sentinel check uses identity comparison (is) against a string constant. String interning is not guaranteed, so raise_on_exchange_request() may not reliably trigger the simulated exception.
 if key in self._exchangable_tokens:
token = self._exchangable_tokens[key]
if token is _RAISE_EXCEPTION:
raise Exception("Simulated exception during token exchange.")

libraries/microsoft-agents-testing/microsoft_agents/testing/test_adapter.py:232

  • TestAdapter.process_activity() always overwrites activity.id with a new generated ID. This discards IDs already set by callers (including activities created by create_activity()) and also advances the adapter's deterministic counter unnecessarily.
 activity.recipient = self._conversation.agent
activity.conversation = self._conversation.conversation
activity.service_url = self._conversation.service_url
activity.id = self._gen_id()

CopilotAI review requested due to automatic review settings July 28, 2026 16:17

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 34 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

libraries/microsoft-agents-testing/microsoft_agents/testing/auth/mock_user_token_client.py:356

  • exchange_token checks token is _RAISE_EXCEPTION, but _RAISE_EXCEPTION is a string sentinel. Identity comparison on strings is not reliable and can fail even when the values are equal, so the mock may silently return a token instead of raising the simulated exception. Use == for the sentinel comparison (or switch to a unique object sentinel).
 if key in self._exchangable_tokens:
token = self._exchangable_tokens[key]
if token is _RAISE_EXCEPTION:
raise Exception("Simulated exception during token exchange.")

libraries/microsoft-agents-testing/setup.py:18

  • aiohttp is declared as an install requirement for microsoft-agents-testing, but there are no aiohttp imports/usages in this package (only referenced here in setup.py). This adds an unnecessary runtime dependency footprint for test utilities; consider removing it unless there's a concrete API that requires it.
setup(
version=package_version,
install_requires=[
f"microsoft-agents-hosting-core=={package_version}",
"aiohttp>=3.11.11",
],
)

CopilotAI review requested due to automatic review settings July 28, 2026 16:33
@rodrigobr-msft
Rodrigo Brandão (rodrigobr-msft) marked this pull request as ready for review July 28, 2026 16:33

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 34 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (4)

libraries/microsoft-agents-testing/setup.py:17

  • aiohttp is listed as an install requirement for microsoft-agents-testing, but nothing in this package imports/uses it. Keeping unused runtime dependencies increases install size and can introduce avoidable dependency conflicts.
 install_requires=[
f"microsoft-agents-hosting-core=={package_version}",
],
)

libraries/microsoft-agents-testing/microsoft_agents/testing/test_adapter.py:384

  • get_next_reply_async() appends a Future to _queued_requests but never removes it if the caller times out/cancels (e.g., via asyncio.wait_for). This can leak cancelled futures and can also block immediate dequeuing because _queued_requests stays non-empty even when replies are queued.
 loop = asyncio.get_running_loop()
future: asyncio.Future[Activity] = loop.create_future()
self._queued_requests.append(future)
return await future

libraries/microsoft-agents-testing/microsoft_agents/testing/test_adapter.py:405

  • create_activity() builds an Activity shaped like the current conversation, but it does not set channel_id. Setting it makes the returned Activity self-contained for callers that use create_turn_context(create_activity(...)) without going through process_activity().
 return Activity(
type=ActivityTypes.message,
text=text,
locale=self.locale or _DEFAULTS._LOCALE,
recipient=self._conversation.agent,

tests/hosting_msteams/helpers.py:43

  • [ResourceResponse()] * len(activities) returns the same ResourceResponse instance repeated, so mutating one entry would mutate all of them. Returning distinct instances avoids surprising shared-state in tests.
 async def send_activities(self, context, activities):
self.sent_activities.extend(activities)
return [ResourceResponse()] * len(activities)

CopilotAI review requested due to automatic review settings July 28, 2026 16:39

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 34 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (7)

libraries/microsoft-agents-testing/microsoft_agents/testing/test_adapter.py:232

  • process_activity unconditionally overwrites activity.id, but create_activity() already assigns an ID. This double-increments the adapter's ID counter and discards any caller-supplied inbound activity ID, which can make activity ID assertions flaky. Only generate an ID when one isn't already present.
 activity.recipient = self._conversation.agent
activity.conversation = self._conversation.conversation
activity.service_url = self._conversation.service_url
activity.id = self._gen_id()

libraries/microsoft-agents-activity/microsoft_agents/activity/channel_adapter_protocol.py:8

  • Importing Self directly from typing_extensions makes runtime availability depend on typing_extensions being installed even on Python versions (3.11+) where typing.Self exists. Prefer importing Self from typing with a fallback to typing_extensions for Python 3.10, to avoid an unnecessary hard dependency on typing_extensions for newer runtimes.
from typing import Protocol, Callable, Awaitable, Optional
from typing_extensions import Self

libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py:8

  • Importing Self directly from typing_extensions makes runtime availability depend on typing_extensions being installed even on Python versions (3.11+) where typing.Self exists. Prefer importing Self from typing with a fallback to typing_extensions for Python 3.10, to avoid an unnecessary hard dependency on typing_extensions for newer runtimes.
from __future__ import annotations
from typing_extensions import Self
from abc import ABC, abstractmethod

libraries/microsoft-agents-testing/microsoft_agents/testing/test_flow.py:228

  • assert_reply_contains accepts a description override but currently drops it when delegating to assert_reply, so failures won't show the caller-provided message. Pass description through so the error text is consistent with other assertions.
 async def validate(reply: Activity) -> None:
if expected not in (reply.text or ""):
raise AssertionError(
description
or f"Expected reply text to contain '{expected}', received '{getattr(reply, 'text', None)}'."
)
return self.assert_reply(validate, timeout=timeout)

libraries/microsoft-agents-testing/microsoft_agents/testing/test_adapter.py:282

  • TurnContext.send_activities([]) is allowed (it can legitimately result in an empty output list), but TestAdapter.send_activities currently raises for an empty activities list. This makes tests fail if code calls context.send_activities([]) or similar no-op sends. Return an empty response list instead of raising.
 if not activities:
raise ValueError("Activities list cannot be empty.")

tests/hosting_msteams/helpers.py:43

  • Using list multiplication here returns the same ResourceResponse instance repeated N times. If any code later mutates a response object, all entries will appear to change together. Prefer a list comprehension to create distinct instances.
 async def send_activities(self, context, activities):
self.sent_activities.extend(activities)
return [ResourceResponse()] * len(activities)

libraries/microsoft-agents-testing/microsoft_agents/testing/auth/mock_user_token_client.py:46

  • The attribute name _exchangable_tokens is misspelled (should be _exchangeable_tokens). This is internal but it makes the code harder to read/search and increases the chance of future typos. Consider renaming it (and updating all references in this module).
 _user_tokens: dict[UserTokenKey, str]
_exchangable_tokens: dict[ExchangeableTokenKey, str]
_magic_codes: list[TokenMagicCode]
def __init__(self):
"""Create an empty in-memory token store."""
self._user_tokens = {}
self._exchangable_tokens = {}
self._magic_codes = []

@rodrigobr-msft
Rodrigo Brandão (rodrigobr-msft) marked this pull request as draft July 28, 2026 17:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide microsoft-agents-testing package providing similar functionality as Microsoft.Agents.Builder.Testing from .NET

2 participants

@rodrigobr-msft