mirror of
https://github.com/temporalio/skill-temporal-developer.git
synced 2026-09-14 13:52:58 +08:00
b5719bc143
* Add initial skill for testing, which is simply Steve's skill (#1) * Add initial skill for testing, which is simply Steve's skill * Rename skill to 'temporal-dev' and update version Updated skill name and version for Temporal Python. * Use claude to merge Steve's, Max's, and Mason's skills. (#2) * Use claude to merge Steve's, Max's, and Mason's skills. Did a review pass using claude's skill devlopment skills * Add missing things from Steve * trigger tweaks * Add in common gotchas from Johann * add simple feedback mechanism (#3) * Change skill name to kebab-case, for compatibility with Amp and Cline (#7) * Clean up references/core/ai-integration.md * Clean up references/core/common-gotchas.md * Clean up references/core/common-gotchas.md * Clean up references/core/determinism.md * Clean up references/core/determinism.md * Update error-reference.md * Update interactive-workflows.md * Clean up patterns.md * Cut shell scripts * Edit troubleshooting.md * remove interceptors for now * remove dynamic workflows * clarify on heartbeating of async activity completions, and prompt it a bit in relation to signals * Improve references/python/advanced-features.md * Use explicit namespace in connect * remove duplicated content from determinism.md, clean up * Improve references/python/data-handling.md * Prefer start_to_close_timeout * don't explicitely provide defaults for retry policies * error-handling.md cleanup * move idempotency patterns to patterns.md * remove multi-param activities * small edits * Unify sandbox stuff into one file * local activities aren't experimental * Clean up references/python/sync-vs-async.md * Cleanup observability.md, remove duplicated search attributes * Cut otel for now * cut a lot of duplicate stuff from python gotchas, address comments * de-duplicate content * Lots of improvements to testing * cleanup to top level of skill (like CLI install instructions), and to top-level of python * Improve patterns.md * clean up ai-patterns.md * Update readme with installation instructions * remove ts directory * De-couple core from python and TypeScript as much as possible * Remove TypeScript hints * add prompting for feedback at startup - wait for ethan on slack channel * shorten url * Update slack channel * Automated pass over on python cleanup & deduplication * Remove multi-patching from Python, since its obvious, dont waste tokens on it. (#34) * Add TypeScript (#31) Adds initial support for TypeScript to the skill --------- Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com> Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> * Fix typos and reference links (#36) * Fix typos and reference links * 2 more typo fixes * quick edit to readme (#37) * Fix saga compensations to run under cancellation protection (#43) When a workflow is cancelled mid-saga, compensations must run in a cancellation-protected scope, otherwise they are immediately cancelled before they can execute. - Python: wrap compensation loop in asyncio.shield() so it runs even when the workflow receives a CancelledError - TypeScript: wrap compensation loop in CancellationScope.nonCancellable() so it runs even when the root scope is cancelled (per official docs: "Cleanup logic must be in a nonCancellable scope") - TypeScript: also fix compensation registration order — register BEFORE calling the activity (was already correct in Python) Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Update readme for public preview (#45) * a few more readme tweaks (#46) * Add MIT License to the project (#47) * Add Go (supersedes other PR) (#38) * progress on go * Go translation workflow completed. * missed a few spots * Manual edits * Address feedback * Add gotcha about anonymous local activities * Sample code for payload converter * clarify sdk protection mechanisms * Setup CODEOWNERS to AI SDK team (#48) * Align version number in SKILL.md and plugin.json. (#49) --------- Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com> Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
166 lines
4.9 KiB
Markdown
166 lines
4.9 KiB
Markdown
# Python SDK Testing
|
||
|
||
## Overview
|
||
|
||
You test Temporal Python Workflows using the Temporal testing package plus a normal Python test framework like pytest. The Temporal Python SDK provides `WorkflowEnvironment` for testing workflows in a local environment and `ActivityEnvironment` for isolated activity testing.
|
||
|
||
## Workflow Test Environment
|
||
|
||
The core pattern is:
|
||
|
||
1. Start a test WorkflowEnvironment (`WorkflowEnvironment.start_local()`).
|
||
2. Start a Worker in that environment with your Workflow and Activities registered.
|
||
3. Use the environment’s client to execute the Workflow, using a fresh UUID for the task queue name and workflow ID.
|
||
4. Assert on the result or status.
|
||
|
||
`WorkflowEnvironment.start_local` configures a ready-to-go local environment for running and testing workflows:
|
||
|
||
```python
|
||
import uuid
|
||
import pytest
|
||
|
||
from temporalio.testing import WorkflowEnvironment
|
||
from temporalio.worker import Worker
|
||
|
||
from activities import my_activity
|
||
from workflows import MyWorkflow
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_workflow():
|
||
task_queue_name = str(uuid.uuid4())
|
||
async with await WorkflowEnvironment.start_local() as env:
|
||
async with Worker(
|
||
env.client,
|
||
task_queue=task_queue_name,
|
||
workflows=[MyWorkflow],
|
||
activities=[my_activity],
|
||
):
|
||
result = await env.client.execute_workflow(
|
||
MyWorkflow.run,
|
||
"input",
|
||
id=str(uuid.uuid4()),
|
||
task_queue=task_queue_name,
|
||
)
|
||
```
|
||
|
||
Conveniently, the local `env` can be shared among tests, e.g. via a pytest fixture.
|
||
|
||
If your workflows / tests involve long durations (such as using Temporal timers / sleeps), then you can use the time-skipping environment, via `WorkflowEnvironment.start_time_skipping()`.
|
||
Only use time-skipping if you must. It can *not* be shared among tests.
|
||
|
||
## Mocking Activities
|
||
|
||
```python
|
||
import uuid
|
||
import pytest
|
||
|
||
from temporalio import activity
|
||
from temporalio.testing import WorkflowEnvironment
|
||
from temporalio.worker import Worker
|
||
|
||
from workflows import MyWorkflow
|
||
|
||
@activity.defn(name="compose_greeting")
|
||
async def compose_greeting_mocked(input: str) -> str:
|
||
return "mocked result"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_with_mock():
|
||
task_queue_name = str(uuid.uuid4())
|
||
async with await WorkflowEnvironment.start_local() as env:
|
||
async with Worker(
|
||
env.client,
|
||
task_queue=task_queue_name,
|
||
workflows=[MyWorkflow],
|
||
activities=[compose_greeting_mocked],
|
||
):
|
||
result = await env.client.execute_workflow(...)
|
||
```
|
||
|
||
## Testing Signals and Queries
|
||
|
||
```python
|
||
@pytest.mark.asyncio
|
||
async def test_signals():
|
||
async with await WorkflowEnvironment.start_local() as env:
|
||
async with Worker(...):
|
||
handle = await env.client.start_workflow(...) # same arguments as to execute_workflow
|
||
|
||
# Send signal
|
||
await handle.signal(MyWorkflow.my_signal, "data")
|
||
|
||
# Query state
|
||
status = await handle.query(MyWorkflow.get_status)
|
||
assert status == "expected"
|
||
|
||
# Wait for completion
|
||
result = await handle.result()
|
||
```
|
||
|
||
## Testing Failure Cases
|
||
|
||
Below shows an example of how to test failure cases:
|
||
|
||
```python
|
||
# Test failure scenarios
|
||
@pytest.mark.asyncio
|
||
async def test_activity_failure_handling():
|
||
async with await WorkflowEnvironment.start_local() as env:
|
||
# An example activity that always fails
|
||
@activity.defn
|
||
async def failing_activity() -> str:
|
||
raise ApplicationError("Simulated failure", non_retryable=True)
|
||
|
||
async with Worker(...):
|
||
with pytest.raises(WorkflowFailureError):
|
||
await env.client.execute_workflow(...)
|
||
```
|
||
|
||
## Workflow Replay Testing
|
||
|
||
```python
|
||
import json
|
||
import pytest
|
||
import uuid
|
||
from temporalio.client import WorkflowHistory
|
||
from temporalio.worker import Replayer
|
||
|
||
from workflows import MyWorkflow
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_replay():
|
||
with open("example-history.json", "r") as f:
|
||
history_json = json.load(f)
|
||
|
||
replayer = Replayer(workflows=[MyWorkflow])
|
||
|
||
# From JSON file
|
||
await replayer.replay_workflow(
|
||
WorkflowHistory.from_json(workflow_id=str(uuid.uuid4()), history_json)
|
||
)
|
||
```
|
||
|
||
|
||
## Activity Testing
|
||
|
||
```python
|
||
import pytest
|
||
|
||
from temporalio.testing import ActivityEnvironment
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_activity():
|
||
env = ActivityEnvironment()
|
||
result = await env.run(my_activity, "arg1", "arg2")
|
||
assert result == "expected"
|
||
```
|
||
|
||
## Best Practices
|
||
|
||
1. Use the `WorkflowEnvironment.start_local` environment for most testing
|
||
2. Use time-skipping environment for workflows with durable timers / durable sleeps.
|
||
3. Mock external dependencies in activities
|
||
4. Test replay compatibility, especially when changing workflow code
|
||
5. Test signal/query handlers explicitly
|
||
6. Use unique workflow IDs and task queues per test to avoid conflicts. Easiest is a `uuid.uuid4()`
|