-
Notifications
You must be signed in to change notification settings - Fork 56
Users/robrandao/data driven tests #244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cfb8847
4d93a46
be2fb1c
0d5539a
bf4c006
ebe622d
45678e9
c2b84cb
2816e68
2214827
c90c8e4
36f5c3b
dfdd959
d4828fb
91b3c44
1eefe44
a06da9a
d36ec7a
c096048
5451ddf
c2cdd40
7d91ef9
87610a5
ddfdd0b
dd0a87d
6a4e8ef
7793790
3ebbd44
a6681bf
1a7264b
9ba4a43
fab4368
a111155
a2a1092
ae8a286
9a5a6a8
aad011a
be0032a
e3f4324
a077eed
49161c3
69d68d4
3dc3efa
054a3a2
8a5f22e
4b7c673
01f8f5a
9cccc1f
8f87aff
e742e00
a6e305a
c3877a3
fb5b76d
5eb9bfb
81b5d6b
e451459
f241df8
cb3c6aa
b8f3304
42b4ea1
2f79824
cfded79
b6618b2
1e8f19e
d9eb5fa
f02b875
57b87eb
6925093
6d55b29
6866f5d
1db2bf5
e44a2da
1cf561b
e651f92
ed7f2f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| parent: ./defaults.yaml | ||
| description: A basic data driven test example | ||
| defaults: | ||
| input: | ||
| sleep: | ||
| assertion: | ||
| test: | ||
| - type: input | ||
| activity: | ||
| type: message | ||
| text: "Hello, World!" | ||
|
|
||
| - type: sleep | ||
| duration: 1 | ||
|
|
||
| - type: assertion | ||
| quantifier: ONE | ||
| selector: | ||
| index: -1 | ||
| quantifier: ONE | ||
| activity: | ||
| type: message | ||
| activity: | ||
| type: message | ||
| text: "Hello, World!" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| defaults: | ||
| all: | ||
| channel_id: "test_channel" | ||
| service_url: "http://localhost:3978" | ||
| locale: "en-US" | ||
| conversation_id: "test_conversation" | ||
| input: | ||
| user_id: "test_user" | ||
| output: | ||
| user_id: "test_bot" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Microsoft 365 Agents SDK for Python Integration Testing Framework | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import json | ||
|
|
||
| from microsoft_agents.activity import Activity | ||
|
|
||
|
|
||
| def load_activity(channel: str, name: str) -> Activity: | ||
|
|
||
| with open( | ||
| "./dev/integration/src/tests/integration/foundational/activities/{}/{}.json".format( | ||
| channel, name | ||
| ), | ||
| "r", | ||
| ) as f: | ||
| activity = json.load(f) | ||
|
|
||
| return Activity.model_validate(activity) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,141 @@ | ||||||||||||||||||
| import json | ||||||||||||||||||
| import pytest | ||||||||||||||||||
| import asyncio | ||||||||||||||||||
|
|
||||||||||||||||||
| from microsoft_agents.activity import ( | ||||||||||||||||||
| ActivityTypes, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| from src.core import integration, IntegrationFixtures, AiohttpEnvironment | ||||||||||||||||||
| from src.samples import QuickstartSample | ||||||||||||||||||
|
|
||||||||||||||||||
| from ._common import load_activity | ||||||||||||||||||
|
|
||||||||||||||||||
| DIRECTLINE = "directline" | ||||||||||||||||||
|
|
||||||||||||||||||
| @integration() | ||||||||||||||||||
| class TestFoundation(IntegrationFixtures): | ||||||||||||||||||
|
|
||||||||||||||||||
| def load_activity(self, activity_name) -> Activity: | ||||||||||||||||||
|
||||||||||||||||||
| return load_activity(DIRECTLINE, activity_name) | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_activity__sends_hello_world__returns_hello_world(self, agent_client): | ||||||||||||||||||
| activity = load_activity(DIRECTLINE, "hello_world.json") | ||||||||||||||||||
| result = await agent_client.send_activity(activity) | ||||||||||||||||||
| assert result is not None | ||||||||||||||||||
| last = result[-1] | ||||||||||||||||||
| assert last.type == ActivityTypes.message | ||||||||||||||||||
| assert last.text.lower() == "you said: {activity.text}".lower() | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_invoke__send_basic_invoke_activity__receive_invoke_response(self, agent_client): | ||||||||||||||||||
| activity = load_activity(DIRECTLINE, "basic_invoke.json") | ||||||||||||||||||
| result = await agent_client.send_activity(activity) | ||||||||||||||||||
| assert result | ||||||||||||||||||
| data = json.loads(result) | ||||||||||||||||||
| message = data.get("message", {}) | ||||||||||||||||||
| assert "Invoke received." in message | ||||||||||||||||||
| assert "data" in data | ||||||||||||||||||
| assert data["parameters"] and len(data["parameters"]) > 0 | ||||||||||||||||||
| assert "hi" in data["value"] | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_activity__sends_message_activity_to_ac_submit__return_valid_response(self, agent_client): | ||||||||||||||||||
| activity = load_activity(DIRECTLINE, "ac_submit.json") | ||||||||||||||||||
| result = await agent_client.send_activity(activity) | ||||||||||||||||||
| assert result is not None | ||||||||||||||||||
| last = result[-1] | ||||||||||||||||||
| assert last.type == ActivityTypes.message | ||||||||||||||||||
| assert "doStuff" in last.text | ||||||||||||||||||
| assert "Action.Submit" in last.text | ||||||||||||||||||
| assert "hello" in last.text | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_invoke_sends_invoke_activity_to_ac_execute__returns_valid_adaptive_card_invoke_response(self, agent_client): | ||||||||||||||||||
| activity = load_activity(DIRECTLINE, "ac_execute.json") | ||||||||||||||||||
| result = await agent_client.send_invoke(activity) | ||||||||||||||||||
|
|
||||||||||||||||||
| result = json.loads(result) | ||||||||||||||||||
|
|
||||||||||||||||||
| assert result.status == 200 | ||||||||||||||||||
| assert result.value | ||||||||||||||||||
|
|
||||||||||||||||||
| assert "application/vnd.microsoft.card.adaptive" in result.type | ||||||||||||||||||
|
|
||||||||||||||||||
| activity_data = json.loads(activity.value) | ||||||||||||||||||
| assert activity_data.get("action") | ||||||||||||||||||
| user_text = activity_data.get("usertext") | ||||||||||||||||||
| assert user_text in result.value | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_activity_sends_text__returns_poem(self, agent_client): | ||||||||||||||||||
| pass | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_expected_replies_activity__sends_text__returns_poem(self, agent_client): | ||||||||||||||||||
| activity = self.load_activity("expected_replies.json") | ||||||||||||||||||
| result = await agent_client.send_expected_replies(activity) | ||||||||||||||||||
| last = result[-1] | ||||||||||||||||||
| assert last.type == ActivityTypes.message | ||||||||||||||||||
| assert "Apollo" in last.text | ||||||||||||||||||
| assert "\n" in last.text | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_invoke__query_link__returns_text(self, agent_client): | ||||||||||||||||||
| activity = self.load_activity("query_link.json") | ||||||||||||||||||
| result = await agent_client.send_invoke(activity) | ||||||||||||||||||
| pass # TODO | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_invoke__select_item__receive_item(self, agent_client): | ||||||||||||||||||
| activity = self.load_activity("select_item.json") | ||||||||||||||||||
| result = await agent_client.send_invoke(activity) | ||||||||||||||||||
| pass # TODO | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_activity__conversation_update__returns_welcome_message(self, agent_client): | ||||||||||||||||||
| activity = self.load_activity("conversation_update.json") | ||||||||||||||||||
| result = await agent_client.send_activity(activity) | ||||||||||||||||||
| last = result[-1] | ||||||||||||||||||
| assert "Hello and Welcome!" in last.text | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_activity__send_heart_message_reaction__returns_message_reaction_heart(self, agent_client): | ||||||||||||||||||
| activity = self.load_activity("message_reaction_heart.json") | ||||||||||||||||||
| result = await agent_client.send_activity(activity) | ||||||||||||||||||
| last = result[-1] | ||||||||||||||||||
| assert last.type == ActivityTypes.message | ||||||||||||||||||
| assert "Message Reaction Added: heart" in last.text | ||||||||||||||||||
|
|
||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||
| async def test__send_activity__remove_heart_message_reaction__returns_message_reaction_heart(self, agent_client): | ||||||||||||||||||
| activity = self.load_activity | ||||||||||||||||||
|
||||||||||||||||||
| activity = self.load_activity | |
| activity = self.load_activity("message_reaction_heart.json") |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assertion message on lines 132-135 is malformed. The assertion logic appears to be checking if a degree symbol exists in the content, but the error message itself is embedded within the OR conditions using the in operator, which will always evaluate to False. The message should be separate from the condition being asserted.
| assert \ | |
| "�" in adaptive_card.content or \ | |
| "\\u00B0" in adaptive_card.content or \ | |
| f"Missing temperature inside adaptive card: {adaptive_card.content}" in adaptive_card.content | |
| assert ( | |
| "�" in adaptive_card.content or | |
| "\\u00B0" in adaptive_card.content | |
| ), f"Missing temperature inside adaptive card: {adaptive_card.content}" |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incomplete test method. The test ends abruptly at line 141 with assert keyword but no condition or expression to assert, resulting in a syntax error.
| assert | |
| pytest.skip("Test not yet implemented") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # import pytest | ||
| # import asyncio | ||
|
|
||
| # from src.core import IntegrationFixtures, AiohttpEnvironment | ||
| # from src.samples import QuickstartSample | ||
|
|
||
| # class TestQuickstart(Integration): | ||
| # _sample_cls = QuickstartSample | ||
| # _environment_cls = AiohttpEnvironment | ||
|
|
||
| # @pytest.mark.asyncio | ||
| # async def test_welcome_message(self, agent_client, response_client): | ||
| # res = await agent_client.send_expect_replies("hi") | ||
| # await asyncio.sleep(1) # Wait for processing | ||
| # responses = await response_client.pop() | ||
|
|
||
| # assert len(responses) == 0 | ||
|
|
||
| # first_non_typing = next((r for r in res if r.type != "typing"), None) | ||
| # assert first_non_typing is not None | ||
| # assert first_non_typing.text == "you said: hi" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| aioresponses |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,8 @@ | ||||||||||||||
| from microsoft_agents.testing import DataDrivenTester | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+1
to
+3
|
||||||||||||||
| from microsoft_agents.testing import DataDrivenTester | |
| # Minimal DataDrivenTester base class definition | |
| class DataDrivenTester: | |
| def _run_data_driven_test(self, filename): | |
| # Placeholder implementation | |
| pass |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Undefined method _run_data_driven_test. The method is called on line 8 but is not defined in the shown code or in the DataDrivenTester base class (which doesn't appear to exist based on the imports shown).
| def _run_data_driven_test(self, filename): | |
| # Stub implementation: print the filename | |
| print(f"Running data driven test with {filename}") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
|
|
||
| from .activity_assertion import ActivityAssertion | ||
| from .assertions import ( | ||
| assert_activity, | ||
| assert_field, | ||
| ) | ||
| from .check_activity import check_activity | ||
| from .check_field import check_field | ||
| from .type_defs import FieldAssertionType, SelectorQuantifier, AssertionQuantifier | ||
|
|
||
| __all__ = [ | ||
| "assert_activity", | ||
| "assert_field", | ||
| "check_activity", | ||
| "check_field", | ||
| "FieldAssertionType", | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||||||||||
| from typing import Optional | ||||||||||||||
|
||||||||||||||
|
|
||||||||||||||
| from microsoft_agents.activity import Activity | ||||||||||||||
|
|
||||||||||||||
| from .select_activity import select_activities | ||||||||||||||
| from .check_activity import check_activity_verbose | ||||||||||||||
| from .type_defs import AssertionQuantifier, AssertionErrorData | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class ActivityAssertion: | ||||||||||||||
|
|
||||||||||||||
| def __init__(self, config: dict) -> None: | ||||||||||||||
| """Initializes the ActivityAssertion with the given configuration. | ||||||||||||||
|
|
||||||||||||||
| :param config: The configuration dictionary containing quantifier, selector, and assertion. | ||||||||||||||
| """ | ||||||||||||||
| quantifier_name = config.get("quantifier", AssertionQuantifier.ALL) | ||||||||||||||
| self._quantifier = AssertionQuantifier(quantifier_name) | ||||||||||||||
|
|
||||||||||||||
| self._selector = config.get("selector", {}) | ||||||||||||||
| self._assertion = config.get("assertion", {}) | ||||||||||||||
|
|
||||||||||||||
| @staticmethod | ||||||||||||||
| def _combine_assertion_errors(errors: list[AssertionErrorData]) -> str: | ||||||||||||||
| """Combines multiple assertion errors into a single string representation. | ||||||||||||||
|
|
||||||||||||||
| :param errors: The list of assertion errors to be combined. | ||||||||||||||
| :return: A string representation of the combined assertion errors. | ||||||||||||||
| """ | ||||||||||||||
| return "\n".join(str(error) for error in errors) | ||||||||||||||
|
|
||||||||||||||
| def check(self, activities: list[Activity]) -> tuple[bool, Optional[str]]: | ||||||||||||||
| """Asserts that the given activities match the assertion criteria. | ||||||||||||||
|
|
||||||||||||||
| :param activities: The list of activities to be tested. | ||||||||||||||
| :return: A tuple containing a boolean indicating if the assertion passed and an optional error message. | ||||||||||||||
| """ | ||||||||||||||
|
|
||||||||||||||
| activities = select_activities(activities, self._selector) | ||||||||||||||
|
|
||||||||||||||
| count = 0 | ||||||||||||||
| for activity in activities: | ||||||||||||||
| res, assertion_error_data = check_activity_verbose( | ||||||||||||||
| activity, self._assertion | ||||||||||||||
| ) | ||||||||||||||
| if self._quantifier == AssertionQuantifier.ALL and not res: | ||||||||||||||
| return ( | ||||||||||||||
| False, | ||||||||||||||
| f"Activity did not match the assertion: {activity}\nError: {assertion_error_data}", | ||||||||||||||
| ) | ||||||||||||||
| if self._quantifier == AssertionQuantifier.NONE and res: | ||||||||||||||
| return ( | ||||||||||||||
| False, | ||||||||||||||
| f"Activity matched the assertion when none were expected: {activity}", | ||||||||||||||
| ) | ||||||||||||||
| count += 1 | ||||||||||||||
|
||||||||||||||
| count += 1 | |
| if res: | |
| count += 1 |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable count. The variable is incremented on line 56 but is only checked for the ONE quantifier case on line 59. For the ALL and NONE quantifiers, the count is never used, making the increment on line 56 unnecessary in those cases.
| count += 1 | |
| if self._quantifier == AssertionQuantifier.ONE: | |
| count += 1 |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing implementation for AssertionQuantifier.ANY quantifier. The code handles ALL, NONE, and ONE quantifiers, but doesn't implement logic for the ANY quantifier which is defined in the AssertionQuantifier enum (line 42 in type_defs.py).
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,52 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Licensed under the MIT License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from microsoft_agents.activity import Activity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .type_defs import FieldAssertionType, AssertionQuantifier | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .type_defs import FieldAssertionType, AssertionQuantifier | |
| from .type_defs import FieldAssertionType |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable selector is not used.
| for activity in activities: | |
| def matches_selector(activity, selector): | |
| # Assume activity is either an object with attributes or a dict | |
| for key, value in selector.items(): | |
| # Try attribute first, then dict key | |
| if hasattr(activity, key): | |
| if getattr(activity, key) != value: | |
| return False | |
| elif isinstance(activity, dict): | |
| if activity.get(key) != value: | |
| return False | |
| else: | |
| return False | |
| return True | |
| filtered_activities = [ | |
| activity for activity in activities if matches_selector(activity, selector) | |
| ] if selector else activities | |
| for activity in filtered_activities: |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The quantifier variable is retrieved on lines 45-47 but never used in the function. The function iterates over all activities and checks them against assertion (which should be from assertion_config), but doesn't apply any quantifier logic. This appears to be incomplete implementation.
| for activity in activities: | |
| res, assertion_error_data = check_activity_verbose(activity, assertion) | |
| assert res, str(assertion_error_data) | |
| assertion = assertion_config.get("assertion", {}) | |
| match_results = [] | |
| error_messages = [] | |
| for activity in activities: | |
| res, assertion_error_data = check_activity_verbose(activity, assertion) | |
| match_results.append(res) | |
| if not res: | |
| error_messages.append(str(assertion_error_data)) | |
| # Default quantifier is "all" | |
| if quantifier is None or quantifier == "all": | |
| if not all(match_results): | |
| raise AssertionError( | |
| f"Not all activities matched assertion. Errors: {error_messages}" | |
| ) | |
| elif quantifier == "any": | |
| if not any(match_results): | |
| raise AssertionError( | |
| f"No activities matched assertion. Errors: {error_messages}" | |
| ) | |
| elif quantifier == "none": | |
| if any(match_results): | |
| raise AssertionError( | |
| f"Some activities matched assertion when none were expected." | |
| ) | |
| elif isinstance(quantifier, int): | |
| count = match_results.count(True) | |
| if count != quantifier: | |
| raise AssertionError( | |
| f"Expected exactly {quantifier} activities to match assertion, but got {count}. Errors: {error_messages}" | |
| ) | |
| else: | |
| raise ValueError(f"Unknown quantifier: {quantifier}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Syntax Error (in Python 3).