Skip to content

Commit 8cc9299

Browse files
authored
Merge pull request #48 from sinch/DEVEXP_757-Run_sync_async_tests
DEVEXP 757: [Python E2E] Run tests with Sync and Async clients
2 parents 2bb078e + bd0c5af commit 8cc9299

File tree

3 files changed

+111
-33
lines changed

3 files changed

+111
-33
lines changed

.github/workflows/run-tests.yml

+12-3
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,15 @@ jobs:
3636
run: |
3737
python -m pip install --upgrade pip
3838
pip install -r requirements-dev.txt
39+
3940
- name: Lint with flake8
4041
run: |
4142
flake8 sinch --count --max-complexity=10 --max-line-length=120 --statistics
43+
4244
- name: Test with Pytest
4345
run: |
4446
coverage run --source=. -m pytest
47+
4548
- name: Coverage Test Report
4649
run: |
4750
python -m coverage report --skip-empty
@@ -72,6 +75,12 @@ jobs:
7275
run: .github/scripts/wait-for-mockserver.sh
7376
shell: bash
7477

75-
- name: Run e2e tests
76-
run: behave tests/e2e/**/features
77-
78+
- name: Run e2e tests sync
79+
run: |
80+
export SINCH_CLIENT_MODE=sync
81+
behave tests/e2e/**/features
82+
83+
- name: Run e2e tests async
84+
run: |
85+
export SINCH_CLIENT_MODE=async
86+
behave tests/e2e/**/features
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import os
2+
import logging
3+
import asyncio
4+
from sinch import SinchClient, SinchClientAsync
5+
6+
def get_logger():
7+
"""Creates and returns a logger instance for this module only."""
8+
log = logging.getLogger(__name__)
9+
log.setLevel(logging.INFO)
10+
if not log.hasHandlers():
11+
handler = logging.StreamHandler()
12+
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
13+
log.addHandler(handler)
14+
log.propagate = False
15+
16+
return log
17+
18+
logger = get_logger()
19+
20+
def before_all(context):
21+
"""
22+
Initializes the appropriate Sinch client based on the environment variable SINCH_CLIENT_MODE.
23+
If it's set to 'async', a single event loop is created for all tests.
24+
Otherwise, we use the synchronous client.
25+
"""
26+
client_mode = os.getenv('SINCH_CLIENT_MODE', 'sync')
27+
28+
logger.info(f" Running E2E tests in **{client_mode.upper()}** mode")
29+
30+
client_params = {
31+
'project_id': 'tinyfrog-jump-high-over-lilypadbasin',
32+
'key_id': 'keyId',
33+
'key_secret': 'keySecret',
34+
}
35+
if client_mode == 'async':
36+
# Create and set a single event loop for the entire test run
37+
context.loop = asyncio.new_event_loop()
38+
asyncio.set_event_loop(context.loop)
39+
context.sinch = SinchClientAsync(**client_params)
40+
else:
41+
# Sync client does not need an event loop
42+
context.sinch = SinchClient(**client_params)
43+
context.sinch.configuration.auth_origin = 'http://localhost:3011'
44+
context.sinch.configuration.numbers_origin = 'http://localhost:3013'
45+
46+
def after_all(context):
47+
"""
48+
Closes the Async event loop if it was created during the test
49+
"""
50+
if hasattr(context, 'loop'):
51+
context.loop.close()

tests/e2e/numbers/features/steps/numbers.steps.py

+48-30
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
1+
import inspect
12
from datetime import timezone, datetime
23
from behave import given, when, then
34
from decimal import Decimal
4-
from sinch import SinchClient
55
from sinch.domains.numbers.exceptions import NumberNotFoundException
66
from sinch.domains.numbers.models.available.activate_number_response import ActivateNumberResponse
77
from sinch.domains.numbers.models.available.rent_any_number_response import RentAnyNumberResponse
88
from sinch.domains.numbers.models.numbers import NotFoundError
99

10+
def execute_sync_or_async(context,call):
11+
"""
12+
Ensures proper execution of both synchronous and asynchronous calls.
13+
- If the call is synchronous, it executes directly.
14+
- If the call is a coroutine (async), it runs using asyncio
15+
This abstracts away execution differences, allowing test steps to be written uniformly.
16+
"""
17+
if call is None:
18+
return None
19+
if inspect.iscoroutine(call):
20+
# Reuse the single loop created in before_all
21+
return context.loop.run_until_complete(call)
22+
else:
23+
return call
1024

1125
@given('the Numbers service is available')
1226
def step_service_is_available(context):
13-
sinch = SinchClient(
14-
project_id='tinyfrog-jump-high-over-lilypadbasin',
15-
key_id='keyId',
16-
key_secret='keySecret',
17-
)
18-
sinch.configuration.auth_origin = 'http://localhost:3011'
19-
sinch.configuration.numbers_origin = 'http://localhost:3013'
20-
context.sinch = sinch
27+
"""Ensures the Sinch client is initialized"""
28+
assert hasattr(context, 'sinch') and context.sinch, 'Sinch client was not initialized'
2129

2230
@when('I send a request to search for available phone numbers')
2331
def step_search_available_numbers(context):
2432
response = context.sinch.numbers.available.list(
2533
region_code='US',
2634
number_type='LOCAL'
2735
)
28-
context.response = response
36+
context.response = execute_sync_or_async(context, response)
2937

3038
@then('the response contains "{count}" available phone numbers')
3139
def step_check_available_numbers_count(context, count):
@@ -50,11 +58,10 @@ def step_check_number_properties(context):
5058
def step_check_number_availability(context, phone_number):
5159
try:
5260
response = context.sinch.numbers.available.check_availability(phone_number)
53-
context.response = response
61+
context.response = execute_sync_or_async(context, response)
5462
except NumberNotFoundException as e:
5563
context.error = e
5664

57-
5865
@then('the response displays the phone number "{phone_number}" details')
5966
def step_validate_number_details(context, phone_number):
6067
data = context.response
@@ -69,8 +76,7 @@ def step_check_unavailable_number(context, phone_number):
6976

7077
@when('I send a request to rent a number with some criteria')
7178
def step_rent_any_number(context):
72-
sinch_client: SinchClient = context.sinch
73-
response = sinch_client.numbers.available.rent_any(
79+
response = context.sinch.numbers.available.rent_any(
7480
region_code = 'US',
7581
type_ = 'LOCAL',
7682
capabilities = ['SMS', 'VOICE'],
@@ -86,7 +92,7 @@ def step_rent_any_number(context):
8692
'search_pattern': 'END'
8793
},
8894
)
89-
context.response = response
95+
context.response = execute_sync_or_async(context, response)
9096

9197
@then('the response contains a rented phone number')
9298
def step_validate_rented_number(context):
@@ -100,15 +106,19 @@ def step_validate_rented_number(context):
100106
assert data.money.currency_code == 'EUR'
101107
assert data.money.amount == Decimal('0.80')
102108
assert data.payment_interval_months == 1
103-
assert data.next_charge_date == datetime.fromisoformat('2024-06-06T14:42:42.022227+00:00').astimezone(tz=timezone.utc)
109+
assert data.next_charge_date == datetime.fromisoformat(
110+
'2024-06-06T14:42:42.022227+00:00'
111+
).astimezone(tz=timezone.utc)
104112
assert data.expire_at == None
105113
assert data.callback_url == ''
106114
assert data.sms_configuration.service_plan_id == ''
107115
assert data.sms_configuration.campaign_id == ''
108116
assert data.sms_configuration.scheduled_provisioning.service_plan_id == 'SpaceMonkeySquadron'
109117
assert data.sms_configuration.scheduled_provisioning.campaign_id == ''
110118
assert data.sms_configuration.scheduled_provisioning.status == 'WAITING'
111-
assert data.sms_configuration.scheduled_provisioning.last_updated_time == datetime.fromisoformat('2024-06-06T14:42:42.596223+00:00').astimezone(tz=timezone.utc)
119+
assert data.sms_configuration.scheduled_provisioning.last_updated_time == datetime.fromisoformat(
120+
'2024-06-06T14:42:42.596223+00:00'
121+
).astimezone(tz=timezone.utc)
112122
assert data.sms_configuration.scheduled_provisioning.error_codes == []
113123
assert data.voice_configuration.type == 'RTC'
114124
assert data.voice_configuration.app_id == ''
@@ -119,12 +129,13 @@ def step_validate_rented_number(context):
119129
assert data.voice_configuration.scheduled_voice_provisioning.trunk_id == ''
120130
assert data.voice_configuration.scheduled_voice_provisioning.service_id == ''
121131
assert data.voice_configuration.scheduled_voice_provisioning.status == 'WAITING'
122-
assert data.voice_configuration.scheduled_voice_provisioning.last_updated_time == datetime.fromisoformat('2024-06-06T14:42:42.604092+00:00').astimezone(tz=timezone.utc)
132+
assert data.voice_configuration.scheduled_voice_provisioning.last_updated_time == datetime.fromisoformat(
133+
'2024-06-06T14:42:42.604092+00:00'
134+
).astimezone(tz=timezone.utc)
123135

124136
@when('I send a request to rent the phone number "{phone_number}"')
125137
def step_rent_specific_number(context, phone_number):
126-
sinch_client: SinchClient = context.sinch
127-
response = sinch_client.numbers.available.activate(
138+
response = context.sinch.numbers.available.activate(
128139
phone_number = phone_number,
129140
sms_configuration= {
130141
'service_plan_id': 'SpaceMonkeySquadron',
@@ -133,7 +144,7 @@ def step_rent_specific_number(context, phone_number):
133144
'app_id': 'sunshine-rain-drop-very-beautifulday'
134145
}
135146
)
136-
context.response = response
147+
context.response = execute_sync_or_async(context, response)
137148

138149
@then('the response contains this rented phone number "{phone_number}"')
139150
def step_validate_rented_specific_number(context, phone_number):
@@ -142,9 +153,8 @@ def step_validate_rented_specific_number(context, phone_number):
142153

143154
@when('I send a request to rent the unavailable phone number "{phone_number}"')
144155
def step_rent_unavailable_number(context, phone_number):
145-
sinch_client: SinchClient = context.sinch
146156
try:
147-
response = sinch_client.numbers.available.activate(
157+
response = context.sinch.numbers.available.activate(
148158
phone_number=phone_number,
149159
sms_configuration={
150160
'service_plan_id': 'SpaceMonkeySquadron',
@@ -153,18 +163,18 @@ def step_rent_unavailable_number(context, phone_number):
153163
'app_id': 'sunshine-rain-drop-very-beautifulday'
154164
}
155165
)
156-
context.response = response
166+
context.response = execute_sync_or_async(context, response)
157167
except NumberNotFoundException as e:
158168
context.error = e
159169

160170
@when("I send a request to list the phone numbers")
161171
def step_when_list_phone_numbers(context):
162-
sinch_client: SinchClient = context.sinch
163-
response = sinch_client.numbers.active.list(
172+
response = context.sinch.numbers.active.list(
164173
region_code='US',
165174
number_type='LOCAL'
166175
)
167176
# Get the first page
177+
response = execute_sync_or_async(context, response)
168178
context.response = response.content()
169179

170180

@@ -175,15 +185,23 @@ def step_then_response_contains_x_phone_numbers(context, count):
175185

176186
@when("I send a request to list all the phone numbers")
177187
def step_when_list_all_phone_numbers(context):
178-
sinch_client: SinchClient = context.sinch
179-
response = sinch_client.numbers.active.list(
188+
response = context.sinch.numbers.active.list(
180189
region_code='US',
181190
number_type='LOCAL'
182191
)
183192
active_numbers_list = []
184193

185-
for number in response.iterator():
186-
active_numbers_list.append(number)
194+
response = execute_sync_or_async(context, response)
195+
if inspect.isasyncgen(response.iterator()):
196+
async def collect_async_numbers():
197+
async for number in response.iterator():
198+
active_numbers_list.append(number)
199+
200+
execute_sync_or_async(context, collect_async_numbers())
201+
else:
202+
for number in response.iterator():
203+
active_numbers_list.append(number)
204+
187205
context.active_numbers_list = active_numbers_list
188206

189207
@then('the phone numbers list contains "{count}" phone numbers')

0 commit comments

Comments
 (0)