Skip to content

Commit b906305

Browse files
vdusekclaude
andcommitted
test: fix unit test quality issues found during audit
Fix copy-paste bug in request queue tests (was testing KVS instead of RQ), replace fragile index-based log assertions with content-based filtering, strengthen weak delegation tests to verify arguments, and improve test isolation with monkeypatch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0f1ff9a commit b906305

File tree

10 files changed

+97
-73
lines changed

10 files changed

+97
-73
lines changed

tests/unit/actor/test_actor_helpers.py

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import asyncio
34
import warnings
45
from datetime import timedelta
56
from typing import TYPE_CHECKING
@@ -130,19 +131,21 @@ async def test_metamorph_fails_locally(caplog: pytest.LogCaptureFixture) -> None
130131
async with Actor:
131132
await Actor.metamorph('random-id')
132133

133-
assert len(caplog.records) == 1
134-
assert caplog.records[0].levelname == 'ERROR'
135-
assert 'Actor.metamorph() is only supported when running on the Apify platform.' in caplog.records[0].message
134+
matching = [r for r in caplog.records if 'Actor.metamorph()' in r.message]
135+
assert len(matching) == 1
136+
assert matching[0].levelname == 'ERROR'
137+
assert 'only supported when running on the Apify platform' in matching[0].message
136138

137139

138140
async def test_reboot_fails_locally(caplog: pytest.LogCaptureFixture) -> None:
139141
caplog.set_level('WARNING')
140142
async with Actor:
141143
await Actor.reboot()
142144

143-
assert len(caplog.records) == 1
144-
assert caplog.records[0].levelname == 'ERROR'
145-
assert 'Actor.reboot() is only supported when running on the Apify platform.' in caplog.records[0].message
145+
matching = [r for r in caplog.records if 'Actor.reboot()' in r.message]
146+
assert len(matching) == 1
147+
assert matching[0].levelname == 'ERROR'
148+
assert 'only supported when running on the Apify platform' in matching[0].message
146149

147150

148151
async def test_add_webhook_fails_locally(caplog: pytest.LogCaptureFixture) -> None:
@@ -152,9 +155,10 @@ async def test_add_webhook_fails_locally(caplog: pytest.LogCaptureFixture) -> No
152155
Webhook(event_types=[WebhookEventType.ACTOR_BUILD_ABORTED], request_url='https://example.com')
153156
)
154157

155-
assert len(caplog.records) == 1
156-
assert caplog.records[0].levelname == 'ERROR'
157-
assert 'Actor.add_webhook() is only supported when running on the Apify platform.' in caplog.records[0].message
158+
matching = [r for r in caplog.records if 'Actor.add_webhook()' in r.message]
159+
assert len(matching) == 1
160+
assert matching[0].levelname == 'ERROR'
161+
assert 'only supported when running on the Apify platform' in matching[0].message
158162

159163

160164
async def test_set_status_message_locally(caplog: pytest.LogCaptureFixture) -> None:
@@ -189,8 +193,9 @@ async def test_push_data_with_empty_data() -> None:
189193
assert result is None
190194

191195

192-
async def test_off_removes_event_listener() -> None:
196+
async def test_off_removes_event_listener(monkeypatch: pytest.MonkeyPatch) -> None:
193197
"""Test that Actor.off() removes an event listener."""
198+
monkeypatch.setenv(ApifyEnvVars.PERSIST_STATE_INTERVAL_MILLIS, '50')
194199
called = False
195200

196201
async def listener(_data: object) -> None:
@@ -200,6 +205,10 @@ async def listener(_data: object) -> None:
200205
async with Actor:
201206
Actor.on(Event.PERSIST_STATE, listener)
202207
Actor.off(Event.PERSIST_STATE, listener)
208+
# Wait long enough for at least one PERSIST_STATE event to fire
209+
await asyncio.sleep(0.2)
210+
# Verify the listener was NOT called because it was removed
211+
assert called is False
203212

204213

205214
async def test_start_actor_with_webhooks(
@@ -214,7 +223,11 @@ async def test_start_actor_with_webhooks(
214223
webhooks=[Webhook(event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED], request_url='https://example.com')],
215224
)
216225

217-
assert len(apify_client_async_patcher.calls['actor']['start']) == 1
226+
calls = apify_client_async_patcher.calls['actor']['start']
227+
assert len(calls) == 1
228+
_, kwargs = calls[0][0], calls[0][1]
229+
assert 'webhooks' in kwargs
230+
assert kwargs['webhooks'] is not None
218231

219232

220233
async def test_start_actor_with_timedelta_timeout(
@@ -226,7 +239,10 @@ async def test_start_actor_with_timedelta_timeout(
226239
async with Actor:
227240
await Actor.start('some-actor-id', timeout=timedelta(seconds=120))
228241

229-
assert len(apify_client_async_patcher.calls['actor']['start']) == 1
242+
calls = apify_client_async_patcher.calls['actor']['start']
243+
assert len(calls) == 1
244+
_, kwargs = calls[0][0], calls[0][1]
245+
assert kwargs.get('timeout_secs') == 120
230246

231247

232248
async def test_start_actor_with_invalid_timeout(
@@ -252,7 +268,11 @@ async def test_call_actor_with_webhooks(
252268
webhooks=[Webhook(event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED], request_url='https://example.com')],
253269
)
254270

255-
assert len(apify_client_async_patcher.calls['actor']['call']) == 1
271+
calls = apify_client_async_patcher.calls['actor']['call']
272+
assert len(calls) == 1
273+
_, kwargs = calls[0][0], calls[0][1]
274+
assert 'webhooks' in kwargs
275+
assert kwargs['webhooks'] is not None
256276

257277

258278
async def test_call_actor_with_timedelta_timeout(
@@ -264,7 +284,10 @@ async def test_call_actor_with_timedelta_timeout(
264284
async with Actor:
265285
await Actor.call('some-actor-id', timeout=timedelta(seconds=120))
266286

267-
assert len(apify_client_async_patcher.calls['actor']['call']) == 1
287+
calls = apify_client_async_patcher.calls['actor']['call']
288+
assert len(calls) == 1
289+
_, kwargs = calls[0][0], calls[0][1]
290+
assert kwargs.get('timeout_secs') == 120
268291

269292

270293
async def test_call_actor_with_remaining_time_deprecation(
@@ -305,7 +328,11 @@ async def test_call_task_with_webhooks(
305328
webhooks=[Webhook(event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED], request_url='https://example.com')],
306329
)
307330

308-
assert len(apify_client_async_patcher.calls['task']['call']) == 1
331+
calls = apify_client_async_patcher.calls['task']['call']
332+
assert len(calls) == 1
333+
_, kwargs = calls[0][0], calls[0][1]
334+
assert 'webhooks' in kwargs
335+
assert kwargs['webhooks'] is not None
309336

310337

311338
async def test_call_task_with_timedelta_timeout(
@@ -317,7 +344,10 @@ async def test_call_task_with_timedelta_timeout(
317344
async with Actor:
318345
await Actor.call_task('some-task-id', timeout=timedelta(seconds=120))
319346

320-
assert len(apify_client_async_patcher.calls['task']['call']) == 1
347+
calls = apify_client_async_patcher.calls['task']['call']
348+
assert len(calls) == 1
349+
_, kwargs = calls[0][0], calls[0][1]
350+
assert kwargs.get('timeout_secs') == 120
321351

322352

323353
async def test_call_task_with_invalid_timeout(

tests/unit/actor/test_actor_lifecycle.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,15 @@ async def test_actor_fail_prevents_further_execution(caplog: pytest.LogCaptureFi
306306
await actor.fail(exit_code=2, exception=Exception('abc'), status_message='cde')
307307
raise RuntimeError('This should not trigger')
308308
except SystemExit:
309-
assert caplog.records[-3].levelno == logging.ERROR
310-
assert caplog.records[-3].msg == 'Actor failed with an exception'
311-
assert caplog.records[-3].exc_text == 'Exception: abc'
312-
assert caplog.records[-2].levelno == logging.INFO
313-
assert caplog.records[-2].msg == 'Exiting Actor'
314-
assert caplog.records[-1].levelno == logging.INFO
315-
assert caplog.records[-1].msg == '[Terminal status message]: cde'
309+
fail_records = [r for r in caplog.records if r.msg == 'Actor failed with an exception']
310+
assert len(fail_records) == 1
311+
assert fail_records[0].levelno == logging.ERROR
312+
assert fail_records[0].exc_text == 'Exception: abc'
313+
314+
exit_records = [r for r in caplog.records if r.msg == 'Exiting Actor']
315+
assert len(exit_records) == 1
316+
assert exit_records[0].levelno == logging.INFO
317+
318+
status_records = [r for r in caplog.records if r.msg == '[Terminal status message]: cde']
319+
assert len(status_records) == 1
320+
assert status_records[0].levelno == logging.INFO

tests/unit/actor/test_actor_log.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,7 @@ async def test_actor_logs_messages_correctly(caplog: pytest.LogCaptureFixture) -
3838

3939
records = caplog.records
4040

41-
# Expected number of log records
42-
assert len(records) == 13
43-
4441
expected_logs = [
45-
(logging.INFO, 'Initializing Actor'),
46-
(logging.DEBUG, 'Configuration initialized'),
47-
(logging.DEBUG, 'Storage client initialized'),
48-
(logging.DEBUG, 'Event manager initialized'),
49-
(logging.DEBUG, 'Charging manager initialized'),
5042
(logging.DEBUG, 'Debug message'),
5143
(logging.INFO, 'Info message'),
5244
(logging.WARNING, 'Warning message'),
@@ -58,9 +50,10 @@ async def test_actor_logs_messages_correctly(caplog: pytest.LogCaptureFixture) -
5850
]
5951

6052
for level, message, *exception in expected_logs:
61-
record = records.pop(0)
53+
matching = [r for r in records if r.message == message]
54+
assert len(matching) == 1, f'Expected exactly one log record with message {message!r}, found {len(matching)}'
55+
record = matching[0]
6256
assert record.levelno == level
63-
assert record.message == message
6457
if exception:
6558
assert record.exc_info is not None
6659
assert record.exc_info[0] is type(exception[0])

tests/unit/actor/test_actor_request_queue.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ async def test_open_returns_same_references() -> None:
1717
assert rq1 is rq2
1818

1919
rq_name = 'non-default'
20-
rq_by_name_1 = await Actor.open_key_value_store(name=rq_name)
21-
rq_by_name_2 = await Actor.open_key_value_store(name=rq_name)
20+
rq_by_name_1 = await Actor.open_request_queue(name=rq_name)
21+
rq_by_name_2 = await Actor.open_request_queue(name=rq_name)
2222
assert rq_by_name_1 is rq_by_name_2
2323

2424
rq_1_metadata = await rq_by_name_1.get_metadata()
25-
rq_by_id_1 = await Actor.open_key_value_store(id=rq_1_metadata.id)
26-
rq_by_id_2 = await Actor.open_key_value_store(id=rq_1_metadata.id)
25+
rq_by_id_1 = await Actor.open_request_queue(id=rq_1_metadata.id)
26+
rq_by_id_2 = await Actor.open_request_queue(id=rq_1_metadata.id)
2727
assert rq_by_id_1 is rq_by_name_1
2828
assert rq_by_id_2 is rq_by_id_1

tests/unit/actor/test_configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ async def test_setting_config_after_actor_raises_exception() -> None:
7676

7777

7878
async def test_actor_using_input_configuration() -> None:
79-
"""Test that setting configuration in service locator after actor was created raises an exception."""
79+
"""Test that configuration passed to Actor is stored in the service locator."""
8080
apify_config = ApifyConfiguration()
8181
async with Actor(configuration=apify_config):
8282
pass

tests/unit/storage_clients/test_alias_resolver.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,14 @@ async def test_concurrent_alias_creation_uses_lock() -> None:
6565
assert not lock.locked()
6666

6767

68-
async def test_alias_map_cached_locally() -> None:
69-
"""Test that alias map is returned from memory when already loaded."""
68+
async def test_get_alias_map_returns_in_memory_map() -> None:
69+
"""Test that _get_alias_map returns the in-memory map when not at home."""
7070
AliasResolver._alias_map = {'existing_key': 'existing_id'}
7171
config = Configuration(is_at_home=False, token='test-token')
7272

7373
result = await AliasResolver._get_alias_map(config)
7474
assert result == {'existing_key': 'existing_id'}
75+
# Also verify that an empty map is returned without fetching from KVS when not at home
76+
AliasResolver._alias_map = {}
77+
result = await AliasResolver._get_alias_map(config)
78+
assert result == {}

tests/unit/storage_clients/test_smart_apify_storage_client.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,12 @@ def test_cache_key_at_home(monkeypatch: pytest.MonkeyPatch) -> None:
7979
service_locator._configuration = None
8080

8181
config = Configuration(is_at_home=True, token='test-token')
82-
client = SmartApifyStorageClient()
82+
cloud_client = MagicMock(spec=ApifyStorageClient)
83+
cloud_client.get_storage_client_cache_key.return_value = 'cloud-key'
84+
client = SmartApifyStorageClient(cloud_storage_client=cloud_client)
8385
key = client.get_storage_client_cache_key(config)
84-
assert key is not None
86+
assert key == 'cloud-key'
87+
cloud_client.get_storage_client_cache_key.assert_called_once_with(config)
8588

8689

8790
def test_cache_key_local(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -90,6 +93,9 @@ def test_cache_key_local(monkeypatch: pytest.MonkeyPatch) -> None:
9093
service_locator._configuration = None
9194

9295
config = Configuration()
93-
client = SmartApifyStorageClient()
96+
local_client = MagicMock(spec=FileSystemStorageClient)
97+
local_client.get_storage_client_cache_key.return_value = 'local-key'
98+
client = SmartApifyStorageClient(local_storage_client=local_client)
9499
key = client.get_storage_client_cache_key(config)
95-
assert key is not None
100+
assert key == 'local-key'
101+
local_client.get_storage_client_cache_key.assert_called_once_with(config)

tests/unit/test_crypto.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def test_crypto_random_object_id_length_and_charset() -> None:
112112
assert len(crypto_random_object_id(5)) == 5
113113
long_random_object_id = crypto_random_object_id(1000)
114114
for char in long_random_object_id:
115-
assert char in 'abcdefghijklmnopqrstuvwxyzABCEDFGHIJKLMNOPQRSTUVWXYZ0123456789'
115+
assert char in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
116116

117117

118118
@pytest.mark.parametrize(('test_input', 'expected'), [(0, '0'), (10, 'a'), (999999999, '15FTGf')])

tests/unit/test_log.py

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,25 @@
11
from __future__ import annotations
22

33
import logging
4-
import os
4+
from typing import TYPE_CHECKING
55

66
from apify.log import _configure_logging
77

8+
if TYPE_CHECKING:
9+
import pytest
810

9-
def test_configure_logging_debug_level() -> None:
11+
12+
def test_configure_logging_debug_level(monkeypatch: pytest.MonkeyPatch) -> None:
1013
"""Test that apify_client logger is set to DEBUG when global level is DEBUG."""
11-
original = os.environ.get('CRAWLEE_LOG_LEVEL')
12-
try:
13-
os.environ['CRAWLEE_LOG_LEVEL'] = 'DEBUG'
14-
_configure_logging()
15-
apify_client_logger = logging.getLogger('apify_client')
16-
assert apify_client_logger.level == logging.DEBUG
17-
finally:
18-
if original is None:
19-
os.environ.pop('CRAWLEE_LOG_LEVEL', None)
20-
else:
21-
os.environ['CRAWLEE_LOG_LEVEL'] = original
14+
monkeypatch.setenv('CRAWLEE_LOG_LEVEL', 'DEBUG')
15+
_configure_logging()
16+
apify_client_logger = logging.getLogger('apify_client')
17+
assert apify_client_logger.level == logging.DEBUG
2218

2319

24-
def test_configure_logging_info_level() -> None:
20+
def test_configure_logging_info_level(monkeypatch: pytest.MonkeyPatch) -> None:
2521
"""Test that apify_client logger is set to INFO when global level > DEBUG."""
26-
original = os.environ.get('CRAWLEE_LOG_LEVEL')
27-
try:
28-
os.environ['CRAWLEE_LOG_LEVEL'] = 'INFO'
29-
_configure_logging()
30-
apify_client_logger = logging.getLogger('apify_client')
31-
assert apify_client_logger.level == logging.INFO
32-
finally:
33-
if original is None:
34-
os.environ.pop('CRAWLEE_LOG_LEVEL', None)
35-
else:
36-
os.environ['CRAWLEE_LOG_LEVEL'] = original
22+
monkeypatch.setenv('CRAWLEE_LOG_LEVEL', 'INFO')
23+
_configure_logging()
24+
apify_client_logger = logging.getLogger('apify_client')
25+
assert apify_client_logger.level == logging.INFO

tests/unit/test_proxy_configuration.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,6 @@ async def test_url_reference_not_shared_between_instances() -> None:
255255
proxy_urls.append('http://proxy-example-3.com:8000')
256256
proxy_configuration_2 = ProxyConfiguration(proxy_urls=proxy_urls)
257257

258-
assert proxy_configuration_1 is not None
259-
assert proxy_configuration_2 is not None
260-
261258
assert proxy_configuration_1._proxy_urls is not proxy_configuration_2._proxy_urls
262259

263260
session_id = 'ABCD'

0 commit comments

Comments
 (0)