Skip to content

Commit 07f21fe

Browse files
vdusekclaude
andcommitted
refactor: split integration tests into integration and e2e test suites
Move Actor-based tests (which build and deploy Actors on the Apify platform) from tests/integration/actor/ to tests/e2e/, and flatten tests/integration/apify_api/ into tests/integration/. This gives a clearer separation: integration tests make real API calls but run locally, while e2e tests require building/deploying Actors on the platform. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cf36cc2 commit 07f21fe

30 files changed

+237
-164
lines changed

.github/workflows/_tests.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,15 @@ jobs:
2929
python_version_for_codecov: "3.14"
3030
operating_system_for_codecov: ubuntu-latest
3131
tests_concurrency: "16"
32+
33+
e2e_tests:
34+
name: E2E tests
35+
uses: apify/workflows/.github/workflows/python_integration_tests.yaml@main
36+
secrets: inherit
37+
with:
38+
python_versions: '["3.10", "3.14"]'
39+
operating_systems: '["ubuntu-latest"]'
40+
python_version_for_codecov: "3.14"
41+
operating_system_for_codecov: ubuntu-latest
42+
run_tests_command: "uv run poe e2e-tests-cov"
43+
tests_concurrency: "16"

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ indent-style = "space"
155155
"**/{tests}/{integration}/*" = [
156156
"PLC0415", # `import` should be at the top-level of a file
157157
]
158+
"**/{tests}/e2e/*" = [
159+
"PLC0415", # `import` should be at the top-level of a file
160+
]
158161
"**/{docs,website}/**" = [
159162
"D", # Everything from the pydocstyle
160163
"INP001", # File {filename} is part of an implicit namespace package, add an __init__.py
@@ -234,6 +237,8 @@ unit-tests = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/unit
234237
unit-tests-cov = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify --cov-report=xml:coverage-unit.xml tests/unit"
235238
integration-tests = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/integration"
236239
integration-tests-cov = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify --cov-report=xml:coverage-integration.xml tests/integration"
240+
e2e-tests = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/e2e"
241+
e2e-tests-cov = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify --cov-report=xml:coverage-e2e.xml tests/e2e"
237242
check-code = ["lint", "type-check", "unit-tests"]
238243

239244
[tool.poe.tasks.install-dev]

tests/e2e/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# E2E tests
2+
3+
These tests build and run Actors using the Python SDK on the Apify platform. They are slower than integration tests (see `tests/integration/`) because they need to build and deploy Actors. Preferably try to write integration tests first, and only write E2E tests when you need to test something that can only be tested on the platform.
4+
5+
## Running
6+
7+
```bash
8+
# Set the API token
9+
export APIFY_TEST_USER_API_TOKEN=<your-token>
10+
11+
# Run the tests
12+
uv run poe e2e-tests
13+
```
14+
15+
If you want to run the tests on a different environment than the main Apify platform, set the `APIFY_INTEGRATION_TESTS_API_URL` environment variable to the right URL.
16+
17+
## How to write tests
18+
19+
There are two fixtures which you can use to write tests:
20+
21+
### `apify_client_async`
22+
23+
This fixture just gives you an instance of `ApifyClientAsync` configured with the right token and API URL, so you don't have to do that yourself.
24+
25+
```python
26+
async def test_something(apify_client_async: ApifyClientAsync) -> None:
27+
assert await apify_client_async.user('me').get() is not None
28+
```
29+
30+
### `make_actor`
31+
32+
This fixture returns a factory function for creating Actors on the Apify platform.
33+
34+
For the Actor source, the fixture takes the files from `tests/e2e/actor_source_base`, builds the Apify SDK wheel from the current codebase, and adds the Actor source you passed to the fixture as an argument. You have to pass exactly one of the `main_func`, `main_py` and `source_files` arguments.
35+
36+
The created Actor will be uploaded to the platform, built there, and after the test finishes, it will be automatically deleted. If the Actor build fails, it will not be deleted, so that you can check why the build failed.
37+
38+
### Creating test Actor straight from a Python function
39+
40+
You can create Actors straight from a Python function. This is great because you can have the test Actor source code checked with the linter.
41+
42+
```python
43+
async def test_something(
44+
make_actor: MakeActorFunction,
45+
run_actor: RunActorFunction,
46+
) -> None:
47+
async def main() -> None:
48+
async with Actor:
49+
print('Hello!')
50+
51+
actor = await make_actor(label='something', main_func=main)
52+
run_result = await run_actor(actor)
53+
54+
assert run_result.status == 'SUCCEEDED'
55+
```
56+
57+
These Actors will have the `src/main.py` file set to the `main` function definition, prepended with `import asyncio` and `from apify import Actor`, for your convenience.
58+
59+
### Creating Actor from source files
60+
61+
You can also pass the source files directly if you need something more complex (e.g. pass some fixed value to the Actor source code or use multiple source files).
62+
63+
To pass the source code of the `src/main.py` file directly, use the `main_py` argument to `make_actor`:
64+
65+
```python
66+
async def test_something(
67+
make_actor: MakeActorFunction,
68+
run_actor: RunActorFunction,
69+
) -> None:
70+
expected_output = f'ACTOR_OUTPUT_{crypto_random_object_id(5)}'
71+
main_py_source = f"""
72+
import asyncio
73+
from datetime import datetime
74+
from apify import Actor
75+
async def main():
76+
async with Actor:
77+
print('Hello! It is ' + datetime.now().time())
78+
await Actor.set_value('OUTPUT', '{expected_output}')
79+
"""
80+
81+
actor = await make_actor(label='something', main_py=main_py_source)
82+
await run_actor(actor)
83+
84+
output_record = await actor.last_run().key_value_store().get_record('OUTPUT')
85+
assert output_record is not None
86+
assert output_record['value'] == expected_output
87+
```
88+
89+
### Asserts
90+
91+
Since test Actors are not executed as standard pytest tests, we don't get introspection of assertion expressions. In case of failure, only a bare `AssertionError` is shown, without the left and right values. This means, we must include explicit assertion messages to aid potential debugging.
92+
93+
```python
94+
async def test_add_and_fetch_requests(
95+
make_actor: MakeActorFunction,
96+
run_actor: RunActorFunction,
97+
) -> None:
98+
"""Test basic functionality of adding and fetching requests."""
99+
100+
async def main() -> None:
101+
async with Actor:
102+
rq = await Actor.open_request_queue()
103+
await rq.add_request(f'https://apify.com/')
104+
assert is_finished is False, f'is_finished={is_finished}'
105+
106+
actor = await make_actor(label='rq-test', main_func=main)
107+
run_result = await run_actor(actor)
108+
109+
assert run_result.status == 'SUCCEEDED'
110+
```

tests/e2e/_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from __future__ import annotations
2+
3+
from crawlee._utils.crypto import crypto_random_object_id
4+
5+
6+
def generate_unique_resource_name(label: str) -> str:
7+
"""Generates a unique resource name, which will contain the given label."""
8+
name_template = 'python-sdk-tests-{}-generated-{}'
9+
template_length = len(name_template.format('', ''))
10+
api_name_limit = 63
11+
generated_random_id_length = 8
12+
label_length_limit = api_name_limit - template_length - generated_random_id_length
13+
14+
label = label.replace('_', '-')
15+
assert len(label) <= label_length_limit, f'Max label length is {label_length_limit}, but got {len(label)}'
16+
17+
return name_template.format(label, crypto_random_object_id(generated_random_id_length))
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)