Skip to content

Commit e64be38

Browse files
chore: improve unit test speed (#1109)
1 parent 7b19d3d commit e64be38

File tree

53 files changed

+457
-480
lines changed

Some content is hidden

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

53 files changed

+457
-480
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import asyncio
2+
import threading
3+
import time
4+
from queue import Queue
5+
from unittest import TestCase
6+
7+
from .mock_server_thread import MockServerThread
8+
from .received_requests import ReceivedRequests
9+
10+
11+
def setup_mock_web_api_server(test: TestCase):
12+
test.server_started = threading.Event()
13+
test.received_requests = ReceivedRequests(Queue())
14+
test.thread = MockServerThread(test.received_requests.queue, test)
15+
test.thread.start()
16+
test.server_started.wait()
17+
18+
19+
def cleanup_mock_web_api_server(test: TestCase):
20+
test.thread.stop()
21+
test.thread = None
22+
23+
24+
def assert_received_request_count(test: TestCase, path: str, min_count: int, timeout: float = 1):
25+
start_time = time.time()
26+
error = None
27+
while time.time() - start_time < timeout:
28+
try:
29+
received_count = test.received_requests.get(path, 0)
30+
assert (
31+
received_count == min_count
32+
), f"Expected {min_count} '{path}' {'requests' if min_count > 1 else 'request'}, but got {received_count}!"
33+
return
34+
except Exception as e:
35+
error = e
36+
# waiting for some requests to be received
37+
time.sleep(0.05)
38+
39+
if error is not None:
40+
raise error
41+
42+
43+
def assert_auth_test_count(test: TestCase, expected_count: int):
44+
assert_received_request_count(test, "/auth.test", expected_count, 0.5)
45+
46+
47+
#########
48+
# async #
49+
#########
50+
51+
52+
def setup_mock_web_api_server_async(test: TestCase):
53+
test.server_started = threading.Event()
54+
test.received_requests = ReceivedRequests(asyncio.Queue())
55+
test.thread = MockServerThread(test.received_requests.queue, test)
56+
test.thread.start()
57+
test.server_started.wait()
58+
59+
60+
def cleanup_mock_web_api_server_async(test: TestCase):
61+
test.thread.stop_unsafe()
62+
test.thread = None
63+
64+
65+
async def assert_received_request_count_async(test: TestCase, path: str, min_count: int, timeout: float = 1):
66+
start_time = time.time()
67+
error = None
68+
while time.time() - start_time < timeout:
69+
try:
70+
received_count = await test.received_requests.get_async(path, 0)
71+
assert (
72+
received_count == min_count
73+
), f"Expected {min_count} '{path}' {'requests' if min_count > 1 else 'request'}, but got {received_count}!"
74+
return
75+
except Exception as e:
76+
error = e
77+
# waiting for mock_received_requests updates
78+
await asyncio.sleep(0.05)
79+
80+
if error is not None:
81+
raise error
82+
83+
84+
async def assert_auth_test_count_async(test: TestCase, expected_count: int):
85+
await assert_received_request_count_async(test, "/auth.test", expected_count, 0.5)
Lines changed: 5 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import asyncio
21
import json
32
import logging
4-
from queue import Queue
5-
import threading
6-
import time
73
from http import HTTPStatus
8-
from http.server import HTTPServer, SimpleHTTPRequestHandler
9-
from typing import Type, Optional
10-
from unittest import TestCase
11-
from urllib.parse import urlparse, parse_qs, ParseResult
4+
from http.server import SimpleHTTPRequestHandler
5+
from typing import Optional
6+
from urllib.parse import ParseResult, parse_qs, urlparse
127

138
INVALID_AUTH = json.dumps(
149
{
@@ -93,7 +88,6 @@ class MockHandler(SimpleHTTPRequestHandler):
9388
protocol_version = "HTTP/1.1"
9489
default_request_version = "HTTP/1.1"
9590
logger = logging.getLogger(__name__)
96-
received_requests = {}
9791

9892
def is_valid_token(self):
9993
return "Authorization" in self.headers and str(self.headers["Authorization"]).startswith("Bearer xoxb-")
@@ -109,8 +103,8 @@ def set_common_headers(self, content_length: int = 0):
109103
def _handle(self):
110104
parsed_path: ParseResult = urlparse(self.path)
111105
path = parsed_path.path
112-
self.server.queue.put(path)
113-
self.received_requests[path] = self.received_requests.get(path, 0) + 1
106+
# put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited
107+
self.server.queue.put_nowait(path)
114108
try:
115109
if path == "/webhook":
116110
self.send_response(200)
@@ -208,95 +202,3 @@ def _parse_request_body(self, parsed_path: str, content_len: int) -> Optional[di
208202
if parsed_path and parsed_path.query:
209203
request_body = {k: v[0] for k, v in parse_qs(parsed_path.query).items()}
210204
return request_body
211-
212-
213-
class MockServerThread(threading.Thread):
214-
def __init__(self, queue: Queue, test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler):
215-
threading.Thread.__init__(self)
216-
self.handler = handler
217-
self.test = test
218-
self.queue = queue
219-
220-
def run(self):
221-
self.server = HTTPServer(("localhost", 8888), self.handler)
222-
self.server.queue = self.queue
223-
self.test.mock_received_requests = self.handler.received_requests
224-
self.test.server_url = "http://localhost:8888"
225-
self.test.host, self.test.port = self.server.socket.getsockname()
226-
self.test.server_started.set() # threading.Event()
227-
228-
self.test = None
229-
try:
230-
self.server.serve_forever(0.05)
231-
finally:
232-
self.server.server_close()
233-
234-
def stop(self):
235-
self.handler.received_requests = {}
236-
with self.server.queue.mutex:
237-
del self.server.queue
238-
self.server.shutdown()
239-
self.join()
240-
241-
242-
class ReceivedRequests:
243-
def __init__(self, queue: Queue):
244-
self.queue = queue
245-
self.received_requests = {}
246-
247-
def get(self, key: str, default: Optional[int] = None) -> Optional[int]:
248-
while not self.queue.empty():
249-
path = self.queue.get()
250-
self.received_requests[path] = self.received_requests.get(path, 0) + 1
251-
return self.received_requests.get(key, default)
252-
253-
254-
def setup_mock_web_api_server(test: TestCase):
255-
test.server_started = threading.Event()
256-
test.received_requests = ReceivedRequests(Queue())
257-
test.thread = MockServerThread(test.received_requests.queue, test)
258-
test.thread.start()
259-
test.server_started.wait()
260-
261-
262-
def cleanup_mock_web_api_server(test: TestCase):
263-
test.thread.stop()
264-
test.thread = None
265-
266-
267-
def assert_received_request_count(test: TestCase, path: str, min_count: int, timeout: float = 1):
268-
start_time = time.time()
269-
error = None
270-
while time.time() - start_time < timeout:
271-
try:
272-
assert test.received_requests.get(path, 0) == min_count
273-
return
274-
except Exception as e:
275-
error = e
276-
# waiting for some requests to be received
277-
time.sleep(0.05)
278-
279-
if error is not None:
280-
raise error
281-
282-
283-
def assert_auth_test_count(test: TestCase, expected_count: int):
284-
assert_received_request_count(test, "/auth.test", expected_count, 0.5)
285-
286-
287-
async def assert_auth_test_count_async(test: TestCase, expected_count: int):
288-
await asyncio.sleep(0.1)
289-
retry_count = 0
290-
error = None
291-
while retry_count < 3:
292-
try:
293-
test.mock_received_requests.get("/auth.test", 0) == expected_count
294-
break
295-
except Exception as e:
296-
error = e
297-
retry_count += 1
298-
# waiting for mock_received_requests updates
299-
await asyncio.sleep(0.1)
300-
301-
if error is not None:
302-
raise error
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import asyncio
2+
import threading
3+
from http.server import HTTPServer, SimpleHTTPRequestHandler
4+
from queue import Queue
5+
from typing import Type, Union
6+
from unittest import TestCase
7+
8+
from .mock_handler import MockHandler
9+
10+
11+
class MockServerThread(threading.Thread):
12+
def __init__(
13+
self, queue: Union[Queue, asyncio.Queue], test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler
14+
):
15+
threading.Thread.__init__(self)
16+
self.handler = handler
17+
self.test = test
18+
self.queue = queue
19+
20+
def run(self):
21+
self.server = HTTPServer(("localhost", 8888), self.handler)
22+
self.server.queue = self.queue
23+
self.test.server_url = "http://localhost:8888"
24+
self.test.host, self.test.port = self.server.socket.getsockname()
25+
self.test.server_started.set() # threading.Event()
26+
27+
self.test = None
28+
try:
29+
self.server.serve_forever(0.05)
30+
finally:
31+
self.server.server_close()
32+
33+
def stop(self):
34+
with self.server.queue.mutex:
35+
del self.server.queue
36+
self.server.shutdown()
37+
self.join()
38+
39+
def stop_unsafe(self):
40+
del self.server.queue
41+
self.server.shutdown()
42+
self.join()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import asyncio
2+
from queue import Queue
3+
from typing import Optional, Union
4+
5+
6+
class ReceivedRequests:
7+
def __init__(self, queue: Union[Queue, asyncio.Queue]):
8+
self.queue = queue
9+
self.received_requests = {}
10+
11+
def get(self, key: str, default: Optional[int] = None) -> Optional[int]:
12+
while not self.queue.empty():
13+
path = self.queue.get()
14+
self.received_requests[path] = self.received_requests.get(path, 0) + 1
15+
return self.received_requests.get(key, default)
16+
17+
async def get_async(self, key: str, default: Optional[int] = None) -> Optional[int]:
18+
while not self.queue.empty():
19+
path = await self.queue.get()
20+
self.received_requests[path] = self.received_requests.get(path, 0) + 1
21+
return self.received_requests.get(key, default)

tests/scenario_tests_async/test_app.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
1717
from slack_bolt.request.async_request import AsyncBoltRequest
1818
from tests.mock_web_api_server import (
19-
setup_mock_web_api_server,
20-
cleanup_mock_web_api_server,
19+
cleanup_mock_web_api_server_async,
20+
setup_mock_web_api_server_async,
2121
)
2222
from tests.utils import remove_os_env_temporarily, restore_os_env, get_event_loop
2323

@@ -31,11 +31,11 @@ class TestAsyncApp:
3131
def event_loop(self):
3232
old_os_env = remove_os_env_temporarily()
3333
try:
34-
setup_mock_web_api_server(self)
34+
setup_mock_web_api_server_async(self)
3535
loop = get_event_loop()
3636
yield loop
3737
loop.close()
38-
cleanup_mock_web_api_server(self)
38+
cleanup_mock_web_api_server_async(self)
3939
finally:
4040
restore_os_env(old_os_env)
4141

tests/scenario_tests_async/test_app_actor_user_token.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import asyncio
21
import datetime
32
import json
43
import logging
@@ -19,9 +18,10 @@
1918
from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
2019
from slack_bolt.request.async_request import AsyncBoltRequest
2120
from tests.mock_web_api_server import (
22-
setup_mock_web_api_server,
23-
cleanup_mock_web_api_server,
21+
assert_received_request_count_async,
22+
cleanup_mock_web_api_server_async,
2423
assert_auth_test_count_async,
24+
setup_mock_web_api_server_async,
2525
)
2626
from tests.utils import remove_os_env_temporarily, restore_os_env, get_event_loop
2727

@@ -40,11 +40,11 @@ class TestApp:
4040
def event_loop(self):
4141
old_os_env = remove_os_env_temporarily()
4242
try:
43-
setup_mock_web_api_server(self)
43+
setup_mock_web_api_server_async(self)
4444
loop = get_event_loop()
4545
yield loop
4646
loop.close()
47-
cleanup_mock_web_api_server(self)
47+
cleanup_mock_web_api_server_async(self)
4848
finally:
4949
restore_os_env(old_os_env)
5050

@@ -128,9 +128,8 @@ async def handle_events(context: AsyncBoltContext, say: AsyncSay):
128128
request = self.build_request()
129129
response = await app.async_dispatch(request)
130130
assert response.status == 200
131-
await assert_auth_test_count_async(self, 1)
132-
await asyncio.sleep(1) # wait a bit after auto ack()
133-
assert self.mock_received_requests["/chat.postMessage"] == 1
131+
await assert_auth_test_count_async(self, 2)
132+
await assert_received_request_count_async(self, "/chat.postMessage", 1)
134133

135134
@pytest.mark.asyncio
136135
async def test_authorize_result_no_user_token(self):
@@ -164,8 +163,7 @@ async def handle_events(context: AsyncBoltContext, say: AsyncSay):
164163
response = await app.async_dispatch(request)
165164
assert response.status == 200
166165
await assert_auth_test_count_async(self, 1)
167-
await asyncio.sleep(1) # wait a bit after auto ack()
168-
assert self.mock_received_requests["/chat.postMessage"] == 1
166+
await assert_received_request_count_async(self, "/chat.postMessage", 1)
169167

170168

171169
class MemoryInstallationStore(AsyncInstallationStore):

0 commit comments

Comments
 (0)