8000 added mocks to test QnAMaker.get_answers() · rsliang/botbuilder-python@59b5cbc · GitHub
[go: up one dir, main page]

Skip to content

Commit 59b5cbc

Browse files
committed
added mocks to test QnAMaker.get_answers()
1 parent 158eac7 commit 59b5cbc

File tree

3 files changed

+302
-33
lines changed

3 files changed

+302
-33
lines changed

libraries/botbuilder-ai/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"botbuilder-core>=4.0.0.a6",
1212
]
1313

14+
1415
root = os.path.abspath(os.path.dirname(__file__))
1516

1617
with open(os.path.join(root, "botbuilder", "ai", "about.py")) as f:
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"answers": [
3+
{
4+
"questions": [
5+
"up"
6+
],
7+
"answer": "is a movie",
8+
"score": 100,
9+
"id": 3,
10+
"source": "Editorial",
11+
"metadata": [
12+
{
13+
"name": "movie",
14+
"value": "disney"
15+
}
16+
]
17+
},
18+
{
19+
"questions": [
20+
"up"
21+
],
22+
"answer": "2nd answer",
23+
"score": 100,
24+
"id": 4,
25+
"source": "Editorial",
26+
"metadata": [
27+
{
28+
"name": "movie",
29+
"value": "disney"
30+
}
31+
]
32+
},
33+
{
34+
"questions": [
35+
"up"
36+
],
37+
"answer": "3rd answer",
38+
"score": 100,
39+
"id": 5,
40+
"source": "Editorial",
41+
"metadata": [
42+
{
43+
"name": "movie",
44+
"value": "disney"
45+
}
6D40 46+
]
47+
}
48+
],
49+
"debugInfo": null
50+
}

libraries/botbuilder-ai/tests/qna/test_qna.py

Lines changed: 251 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,260 @@
22
# Licensed under the MIT License.
33

44
import json
5-
import aiounittest
5+
import aiounittest, unittest, requests
6+
from msrest import Deserializer
7+
from os import path
8+
from requests.models import Response
69
from typing import List, Tuple
710
from uuid import uuid4
8-
from botbuilder.schema import Activity, ChannelAccount, ResourceResponse, ConversationAccount
11+
from unittest.mock import Mock, patch, MagicMock
12+
from asyncio import Future
13+
14+
15+
16+
from botbuilder.ai.qna import Metadata, QnAMakerEndpoint, QnAMaker, QnAMakerOptions, QnATelemetryConstants, QueryResult
917
from botbuilder.core import BotAdapter, BotTelemetryClient, NullTelemetryClient, TurnContext
10-
from botbuilder.ai.qna import QnAMakerEndpoint, QnAMaker, QnAMakerOptions, QnATelemetryConstants
11-
12-
13-
# DELETE YO
14-
ACTIVITY = Activity(id='1234',
15-
type='message',
16-
text='up',
17-
from_property=ChannelAccount(id='user', name='User Name'),
18-
recipient=ChannelAccount(id='bot', name='Bot Name'),
19-
conversation=ConversationAccount(id='convo', name='Convo Name'),
20-
channel_id='UnitTest',
21-
service_url='https://example.org'
22-
)
23-
24-
class SimpleAdapter(BotAdapter):
25-
async def send_activities(self, context, activities):
26-
responses = []
27-
for (idx, activity) in enumerate(activities):
28-
responses.append(ResourceResponse(id='5678'))
29-
return responses
30-
31-
async def update_activity(self, context, activity):
32-
assert context is not None
33-
assert activity is not None
34-
35-
async def delete_activity(self, context, reference):
36-
assert context is not None
37-
assert reference is not None
38-
assert reference.activity_id == '1234'
18+
from botbuilder.core.adapters import TestAdapter
19+
from botbuilder.schema import Activity, ActivityTypes, ChannelAccount, ResourceResponse, ConversationAccount
20+
21+
# from botbuilder.ai.qna import (
22+
# Metadata,
23+
# QnAMakerEndpoint,
24+
# QnAMaker,
25+
# QnAMakerOptions,
26+
# QnATelemetryConstants,
27+
# QueryResult
28+
# )
29+
# from botbuilder.core import (BotAdapter,
30+
# BotTelemetryClient,
31+
# NullTelemetryClient,
32+
# TurnContext
33+
# )
34+
# from botbuilder.core.adapters import TestAdapter
35+
# from botbuilder.schema import (Activity,
36+
# ActivityTypes,
37+
# ChannelAccount,
38+
# ResourceResponse,
39+
# ConversationAccount
40+
# )
41+
3942

4043
class QnaApplicationTest(aiounittest.AsyncTestCase):
44+
_knowledge_base_id: str = 'a090f9f3-2f8e-41d1-a581-4f7a49269a0c'
45+
_endpoint_key: str = '4a439d5b-163b-47c3-b1d1-168cc0db5608'
46+
_host: str = 'https://ashleyNlpBot1-qnahost.azurewebsites.net/qnamaker'
47+
48+
tests_endpoint = QnAMakerEndpoint(_knowledge_base_id, _endpoint_key, _host)
49+
50+
def __init__(self, *args, **kwargs):
51+
super().__init__(*args, **kwargs)
52+
# self._mocked_results: QueryResult(
53+
54+
# )
55+
56+
def test_qnamaker_construction(self):
57+
# Arrange
58+
endpoint = self.tests_endpoint
59+
60+
# Act
61+
qna = QnAMaker(endpoint)
62+
endpoint = qna._endpoint
63+
64+
# Assert
65+
self.assertEqual('a090f9f3-2f8e-41d1-a581-4f7a49269a0c', endpoint.knowledge_base_id)
66+
self.assertEqual('4a439d5b-163b-47c3-b1d1-168cc0db5608', endpoint.endpoint_key)
67+
self.assertEqual('https://ashleyNlpBot1-qnahost.azurewebsites.net/qnamaker', endpoint.host)
68+
69+
70+
def test_endpoint_with_empty_kbid(self):
71+
empty_kbid = ''
72+
73+
with self.assertRaises(TypeError):
74+
QnAMakerEndpoint(
75+
empty_kbid,
76+
self._endpoint_key,
77+
self._host
78+
)
79+
80+
def test_endpoint_with_empty_endpoint_key(self):
81+
empty_endpoint_key = ''
82+
83+
with self.assertRaises(TypeError):
84+
QnAMakerEndpoint(
85+
self._knowledge_base_id,
86+
empty_endpoint_key,
87+
self._host
88+
)
89+
90+
def test_endpoint_with_emptyhost(self):
91+
with self.assertRaises(TypeError):
92+
QnAMakerEndpoint(
93+
self._knowledge_base_id,
94+
self._endpoint_key,
95+
''
96+
)
97+
98+
def test_qnamaker_with_none_endpoint(self):
99+
with self.assertRaises(TypeError):
100+
QnAMaker(None)
101+
102+
def test_v2_legacy_endpoint(self):
103+
v2_hostname = 'https://westus.api.cognitive.microsoft.com/qnamaker/v2.0'
104+
105+
v2_legacy_endpoint = QnAMakerEndpoint(
106+
self._knowledge_base_id,
107+
self._endpoint_key,
108+
v2_hostname
109+
)
110+
111+
with self.assertRaises(ValueError):
112+
QnAMaker(v2_legacy_endpoint)
113+
114+
def test_legacy_protocol(self):
115+
v3_hostname = 'https://westus.api.cognitive.microsoft.com/qnamaker/v3.0'
116+
v3_legacy_endpoint = QnAMakerEndpoint(
117+
self._knowledge_base_id,
118+
self._endpoint_key,
119+
v3_hostname
120+
)
121+
legacy_qna = QnAMaker(v3_legacy_endpoint)
122+
is_legacy = True
123+
124+
v4_hostname = 'https://UpdatedNonLegacyQnaHostName.azurewebsites.net/qnamaker'
125+
nonlegacy_endpoint = QnAMakerEndpoint(
126+
self._knowledge_base_id,
127+
self._endpoint_key,
128+
v4_hostname
129+
)
130+
v4_qna = QnAMaker(nonlegacy_endpoint)
131+
132+
self.assertEqual(is_legacy, legacy_qna._is_legacy_protocol)
133+
self.assertNotEqual(is_legacy, v4_qna._is_legacy_protocol)
134+
135+
def test_set_default_options_with_no_options_arg(self):
136+
qna_without_options = QnAMaker(self.tests_endpoint)
137+
138+
options = qna_without_options._options
139+
140+
default_threshold = 0.3
141+
default_top = 1
142+
default_strict_filters = []
143+
144+
self.assertEqual(default_threshold, options.score_threshold)
145+
self.assertEqual(default_top, options.top)
146+
self.assertEqual(default_strict_filters, options.strict_filters)
147+
148+
def test_options_passed_to_ctor(self):
149+
options = QnAMakerOptions(
150+
score_threshold=0.8,
151+
timeout=9000,
152+
top=5,
153+
strict_filters=[Metadata('movie', 'disney')]
154+
)
155+
156+
qna_with_options = QnAMaker(self.tests_endpoint, options)
157+
actual_options = qna_with_options._options
158+
159+
expected_threshold = 0.8
160+
expected_timeout = 9000
161+
expected_top = 5
162+
expected_strict_filters = [Metadata('movie', 'disney')]
163+
164+
self.assertEqual(expected_threshold, actual_options.score_threshold)
165+
self.assertEqual(expected_timeout, actual_options.timeout)
166+
self.assertEqual(expected_top, actual_options.top)
167+
self.assertEqual(expected_strict_filters[0].name, actual_options.strict_filters[0].name)
168+
self.assertEqual(expected_strict_filters[0].value, actual_options.strict_filters[0].value)
169+
170+
171+
async def test_returns_answer_using_options(self):
172+
# Arrange
173+
question: str = 'up'
174+
response_path: str = 'AnswerWithOptions.json'
175+
options = QnAMakerOptions(
176+
score_threshold = 0.8,
177+
top = 5,
178+
strict_filters = [{
179+
'name': 'movie',
180+
'value': 'disney'
181+
}]
182+
10000 )
183+
184+
# Act
185+
result = await QnaApplicationTest._get_service_result(
186+
question,
187+
response_path,
188+
options=options
189+
)
190+
191+
first_answer = result['answers'][0]
192+
has_at_least_1_ans = True
193+
first_metadata = first_answer['metadata'][0]
194+
195+
# Assert
196+
self.assertIsNotNone(result)
197+
self.assertEqual(has_at_least_1_ans, len(result['answers']) >= 1 and len(result['answers']) <= options.top)
198+
self.assertTrue(question in first_answer['questions'])
199+
self.assertTrue(first_answer['answer'])
200+
self.assertEqual('is a movie', first_answer['answer'])
201+
self.assertTrue(first_answer['score'] >= options.score_threshold)
202+
self.assertEqual('movie', first_metadata['name'])
203+
self.assertEqual('disney', first_metadata['value'])
204+
205+
@classmethod
206+
async def _get_service_result(
207+
cls,
208+
utterance: str,
209+
response_file: str,
210+
bot_adapter: BotAdapter = TestAdapter(),
211+
options: QnAMakerOptions = None
212+
) -> [QueryResult]:
213+
response_json = QnaApplicationTest._get_json_for_file(response_file)
214+
215+
qna = QnAMaker(QnaApplicationTest.tests_endpoint)
216+
context = QnaApplicationTest._get_context(utterance, bot_adapter)
217+
response = Mock(spec=Response)
218+
response.status_code = 200
219+
response.headers = {}
220+
response.reason = ''
221+
222+
with patch('requests.post', return_value=response):
223+
with patch('botbuilder.ai.qna.QnAMaker._format_qna_result', return_value=response_json):
224+
result = await qna.get_answers(context, options)
225+
226+
return result
227+
228+
@classmethod
229+
def _get_json_for_file(cls, response_file: str) -> object:
230+
curr_dir = path.dirname(path.abspath(__file__))
231+
response_path = path.join(curr_dir, "test_data", response_file)
232+
233+
with open(response_path, "r", encoding="utf-8-sig") as f:
234+
response_str = f.read()
235+
response_json = json.loads(response_str)
236+
237+
return response_json
238+
239+
@staticmethod
240+
def _get_context(utterance: str, bot_adapter: BotAdapter) -> TurnContext:
241+
test_adapter = TestAdapter()
242+
activity = Activity(
243+
type = ActivityTypes.message,
244+
text = utterance,
245+
conversation = ConversationAccount(),
246+
recipient = ChannelAccount(),
247+
from_property = ChannelAccount(),
248+
)
249+
250+
return TurnContext(test_adapter, activity)
251+
252+
253+
254+
255+
256+
257+
258+
259+
260+
41261

42-
async def test_initial_test(self):
43-
pass

0 commit comments

Comments
 (0)
0