The tests have three levels:
- Unit tests (
tests/unit) mock the Apify API and need no credentials. - Integration tests (
tests/integration) call the real Apify API without deploying Actors. - E2E tests (
tests/e2e) build and run Actors on the Apify platform.
Use the lowest level that can test what you need. Integration tests run faster than E2E tests and you can debug them locally, so write an E2E test only when the test needs an Actor built and running on the platform.
prepare_test_envand its autouse wrapper_isolate_test_environment(all levels): reset the global state (Actor initialization, service locator, storages) and pointAPIFY_LOCAL_STORAGE_DIRto a temporary directory before each test.apify_client_async(integration and E2E): a session-scopedApifyClientAsyncconfigured with the test token and API URL.
Unit tests check the SDK's logic in isolation. They mock every external dependency, such as the Apify API and other platform services, so they're fast and need no environment variables or credentials.
uv run poe unit-testsapify_client_async_patcher: patchesApifyClientAsyncmethods to return fixed values or call replacement functions, and records the calls.httpserver: a local HTTP server (frompytest-httpserver) for testing HTTP interactions without real network calls.
If a test fails with SystemExit: 0 raised from _actor.py when async with Actor: exits, the virtual environment is missing the optional extras. Without Scrapy installed, the Actor exits the process on teardown. Run uv sync --all-extras or uv run poe install-dev.
Integration tests make real requests to the Apify API, but they don't build or deploy Actors.
export APIFY_TEST_USER_API_TOKEN=<your-token>
uv run poe integration-testsTo run them against a different environment, also set APIFY_INTEGRATION_TESTS_API_URL.
| File | What it tests |
|---|---|
test_dataset.py |
Dataset operations |
test_key_value_store.py |
Key-value store operations |
test_request_queue.py |
Request queue operations |
test_storages.py |
Behavior across storages: aliases, unnamed default storages, explicit initialization |
request_queue_apify: creates an Apify request queue on the platform, once for each access mode (singleandshared), and drops it after the test.
- Right after you add a request to a shared RQ, its head can still come back empty. Poll negative assertions, such as
is_finished(), the same way as positive ones. - In the response of
update_request,wasAlreadyHandleddescribes the request's state before the update. - RQ metadata counts (
handledRequestCount,pendingRequestCount) lag a few seconds behind the operations that change them.
E2E tests build Actors that use the SDK and run them on the Apify platform. Building and deploying the Actors makes them the slowest level.
export APIFY_TEST_USER_API_TOKEN=<your-token>
uv run poe e2e-testsTo run them against a different environment, also set APIFY_INTEGRATION_TESTS_API_URL.
make_actor: creates a temporary Actor on the platform and builds it, retrying a failed build once. The Actor is deleted after the test.run_actor: starts an Actor run and waits up to 10 minutes for it to finish.
Each test Actor gets the files from tests/e2e/actor_source_base, plus a wheel of the SDK built from the current code. The Dockerfile installs the wheel and the other dependencies from requirements.txt and runs the src package. The test fixture sets the base image to the Python version of the test run.
You can create an Actor straight from a Python function, which means the linter also checks the Actor's code.
async def test_something(
make_actor: MakeActorFunction,
run_actor: RunActorFunction,
) -> None:
async def main() -> None:
async with Actor:
print('Hello!')
actor = await make_actor(label='something', main_func=main)
run_result = await run_actor(actor)
assert run_result.status == 'SUCCEEDED'make_actor writes the function to src/main.py, after import asyncio and from apify import Actor. Put any other imports inside the function body.
For a single-file Actor, pass its code as main_py:
async def test_something(
make_actor: MakeActorFunction,
run_actor: RunActorFunction,
) -> None:
expected_output = f'ACTOR_OUTPUT_{crypto_random_object_id(5)}'
main_py_source = f"""
import asyncio
from apify import Actor
async def main():
async with Actor:
await Actor.set_value('OUTPUT', '{expected_output}')
"""
actor = await make_actor(label='something', main_py=main_py_source)
await run_actor(actor)
output_record = await actor.last_run().key_value_store().get_record('OUTPUT')
assert output_record is not None
assert output_record['value'] == expected_outputFor an Actor with several files, pass source_files:
actor_source_files = {
'src/utils.py': """
from datetime import datetime, timezone
def get_current_datetime():
return datetime.now(timezone.utc)
""",
'src/main.py': """
import asyncio
from apify import Actor
from .utils import get_current_datetime
async def main():
async with Actor:
print('Hello! It is ' + str(get_current_datetime()))
""",
}
actor = await make_actor(label='something', source_files=actor_source_files)Test Actors don't run under pytest, so a failed assertion inside one shows only a bare AssertionError, without the values involved. Always give the assertion a message:
assert is_finished is False, f'is_finished={is_finished}'