pytest-databricks-lakeflow
Write ordinary pytest tests that run a Lakeflow Job, or a single task from one, defined in your Databricks Asset Bundle. Each test gets its own throwaway copy of the job, runs it with the parameters you choose, and asserts on the outcome: success or failure, task outputs, logs, task values, and the tables the job wrote.
import pytest
from pytest_databricks_lakeflow import Lakeflow
def test_job_succeeds(lakeflow: Lakeflow) -> None:
with lakeflow.job("my_example_job", params={"param1": "hello", "param2": "world"}) as job:
run = job.run()
assert run.succeeded
assert run.task("transform").output == "hello world"
assert "Success!" in run.task("my_wheel_task").logs
def test_job_surfaces_permission_error(lakeflow: Lakeflow) -> None:
with lakeflow.job("my_example_job", params={"param1": "forbidden"}) as job:
run = job.run()
assert run.failed
assert run.termination_code == "RUN_EXECUTION_ERROR"
assert run.task("transform").raised(PermissionError)
def test_subset_of_job(lakeflow: Lakeflow) -> None:
with lakeflow.job("my_example_job") as job:
run = job.run(only=["+transform"])
assert run.succeeded
assert "load" not in run.tasks
@pytest.mark.parametrize(("greeting", "expected"), [("hello", "hello world"), ("goodbye", "goodbye world")])
def test_single_task(lakeflow: Lakeflow, lakeflow_schema: str, greeting: str, expected: str) -> None:
with lakeflow.task(
"my_example_job.transform",
params={"param1": greeting, "param2": "world", "schema": lakeflow_schema},
upstream_values={"extract": {"row_count": 42, "source_path": "/Volumes/x/y/z"}},
capture_values=["rows_written"],
) as task:
run = task.run()
assert run.succeeded
assert run.task("transform").output == expected
assert run.task("transform").values["rows_written"] == 42
def test_run_twice_in_one_job(lakeflow: Lakeflow, lakeflow_schema: str) -> None:
with lakeflow.job("my_example_job", params={"schema": lakeflow_schema}) as job:
first = job.run()
second = job.run(params={"mode": "incremental"})
assert first.succeeded and second.succeeded
assert second.duration < first.duration
def test_values_captured_even_on_failure(lakeflow: Lakeflow) -> None:
with lakeflow.job("my_example_job", params={"param1": "explode"}, capture_values={"transform": ["stage"]}) as job:
run = job.run()
assert run.failed
assert run.task("transform").values["stage"] == "validation"
examples/bundle is a complete bundle that these tests run against.
Install
pip install pytest-databricks-lakeflow
You also need:
- Python 3.11 or later, pytest 8 or later and
databricks-sdk0.54 or later. These are installed as dependencies. - The Databricks CLI v1.7.0 or later on
PATH. The plugin shells out to it forbundle deploy,summary,validate,syncanddestroy. - Credentials that both the CLI and the SDK can use: a profile in
~/.databrickscfgor the standardDATABRICKS_*environment variables. - Serverless jobs in the workspace for the helper tasks, or see
lakeflow_helpers_computebelow.
For parallel tests, install the extra: pip install "pytest-databricks-lakeflow[xdist]".
Set up
Add a target for tests to databricks.yml. Development mode pauses schedules and triggers and lets runs overlap, and the separate root_path keeps the test deployment away from everyone else's:
targets:
test:
mode: development
workspace:
root_path: ~/.bundle/${bundle.name}/test
presets:
trigger_pause_status: PAUSED
tags:
purpose: pytest
Configure the plugin in pytest.ini, pyproject.toml or tox.ini:
[pytest]
lakeflow_target = test
lakeflow_catalog = dev_sandbox
lakeflow_timeout_minutes = 20
The job code under test must read its catalog and schema from job parameters, not from bundle variables. Bundle variables are fixed at deploy time, so passing a different value per test only works through parameters.
How it works
- The first test that uses
lakeflowdeploys the bundle once withdatabricks bundle deploy -t test, reads the deployed job ids fromdatabricks bundle summary, and loads each job's settings withjobs.get. Those settings have resolved workspace paths for notebooks and uploaded wheels. It also uploads two small helper notebooks to/Users/<you>/.pytest-databricks-lakeflow/. lakeflow.job(key)copies the settings of the bundle jobkey(the resource key, as used bydatabricks bundle run). It removes the schedule, triggers, notifications,run_asand the bundle deployment marker, allows one run at a time, names the job[pytest] <test node id>and tags itpytest,pytest_run_idandpytest_nodeid.- Entering the
withblock creates the job, andrun()runs it and waits. You can callrun()more than once. - Leaving the block deletes the job. If the block exited with an exception, other than
pytest.skiporpytest.xfail, the job is kept so you can open the run in the UI. The job and run URLs are logged and added to the test report.
Your tests never run the deployed jobs directly.
lakeflow.job() and lakeflow.task() also work without with. The job is then created by the first run() and deleted at the end of the test, or kept if the test failed.
Single tasks and task values
lakeflow.task("job_key.task_key", ...) builds a job that holds only that task. For each upstream task named in upstream_values it adds a stub task with the same key, which sets those task values. Both dbutils.jobs.taskValues.get(taskKey="extract", ...) and {{tasks.extract.values.row_count}} in the task's settings then resolve against the stub.
Task values cannot be read through the Jobs API, so capture_values adds a probe task that runs after the target, even when it fails, and reports the values. lakeflow.job(key, capture_values={"transform": ["stage"]}) does the same for a whole job.
When a value is missing, run.task("t").values["k"] raises a KeyError that says why: the task never set it, the task did not run, or the probe failed.
Stub values must be JSON-serialisable, at most 49,152 characters of JSON each, with keys of at most 512 characters. The plugin checks this before creating the job. If the target task refers to task values of an upstream task you did not stub, you get an error before anything is created.
API
Fixtures
lakeflow: aLakeflowfor the current test.lakeflow_schema: creates<lakeflow_catalog>.pytest_<run id>_<random>and yields its full name. It is dropped after the test, even if the test failed, unless you pass--lakeflow-keep.
Tests that use either fixture get the lakeflow marker automatically, so pytest -m "not lakeflow" runs only your fast tests. No deploy happens in a session that runs no lakeflow test.
Lakeflow
job(key, params=None, capture_values=None, name=None) -> EphemeralJob.paramsare job parameters for every run.capture_valuesmaps task keys to lists of value keys.namereplaces the node id in the job name.task("job_key.task_key", params=None, upstream_values=None, capture_values=None) -> EphemeralJob.upstream_valuesmaps upstream task keys to the values their stubs set.capture_valuesis a list of value keys.template(key) -> JobSettings: a fresh copy of the deployed settings.ws: theWorkspaceClient, for example to check that a table exists.
Parameters that the job does not declare raise an error, because the API would accept them silently. Set lakeflow_allow_undeclared_params = true to pass them anyway. Names starting with __pytest_ are reserved.
EphemeralJob
run(params=None, only=None, timeout=None) -> JobRun.paramsare merged over the job's params for this run.onlyselects tasks, and+task,task+and+task+add upstream and downstream tasks.timeoutis atimedeltathat defaults tolakeflow_timeout_minutes. On timeout the run is cancelled andLakeflowTimeoutis raised.job_id,urlandruns.
JobRun
succeeded: true only if every task intasksendedSUCCESS, apart from tasksEXCLUDEDbecause theirrun_ifor if/else condition was not met.failedisnot succeeded.tasks: your tasks that were part of the run, by key.skipped_tasks: tasks left out byonly.injected_tasks: the plugin's stubs and probes.task(key): raises aKeyErrorthat lists the available keys.result_state,termination_code,termination_message,duration,run_idandrun_urlare those of the parent run. Do not use the parent'sresult_stateas the verdict: a job with a probe that runs after a failure endsSUCCESS_WITH_FAILURES.
TaskRun
succeeded,result_state,termination_code,termination_message,duration,run_urlandattempt.output: the value passed todbutils.notebook.exit().logsandlog_lines: stdout and stderr of Python script and wheel tasks.error: aTaskErrorwithtype(for example"PermissionError"),messageandtrace.raised(PermissionError)compares class names.valuesandmissing_values: captured task values.
Hooks
Implement these in a conftest.py. Like pytest's own test hooks, they apply to the tests in that conftest's directory and below:
pytest_lakeflow_modify_settings(settings, request): called just before each job is created. Changesettingsin place.pytest_lakeflow_run_finished(run, request): called after each run.
Configuration
| Ini option | Command line | Default | Meaning |
|---|---|---|---|
lakeflow_bundle_root |
nearest directory with databricks.yml |
Bundle root, relative to the rootdir. | |
lakeflow_target |
--lakeflow-target |
test |
Bundle target. |
lakeflow_profile |
standard SDK resolution | Profile for both the CLI and the SDK. | |
lakeflow_sync |
--lakeflow-sync |
deploy |
deploy, sync or none. See below. |
lakeflow_timeout_minutes |
--lakeflow-timeout |
30 |
Default timeout for one run. |
lakeflow_catalog |
Catalog for lakeflow_schema. |
||
lakeflow_helpers_path |
/Users/<you>/.pytest-databricks-lakeflow |
Workspace folder for the helper notebooks. | |
lakeflow_helpers_compute |
serverless | A job_cluster_key from the job for stub and probe tasks, for workspaces without serverless. |
|
lakeflow_helpers_environment_version |
4 |
Serverless environment version for stub and probe tasks. | |
lakeflow_disable_auto_optimization |
true |
Stop serverless from retrying failed tasks, so a failure fails once and fast. | |
lakeflow_performance_optimized |
false |
Run every ephemeral job as PERFORMANCE_OPTIMIZED. |
|
lakeflow_allow_undeclared_params |
false |
Allow parameters the job does not declare. | |
lakeflow_sweep_hours |
24 |
Age after which --lakeflow-sweep deletes leftover jobs. |
|
--lakeflow-keep |
off | Keep every job and schema. | |
--lakeflow-keep-failed, --no-lakeflow-keep-failed |
on | Keep the job when its test fails. | |
--lakeflow-sweep |
off | At session start, delete your own jobs tagged pytest from other runs that are older than lakeflow_sweep_hours. |
|
--lakeflow-destroy |
off | Run databricks bundle destroy at the end of the session. |
The run id that tags every job comes from LAKEFLOW_RUN_ID, then GITHUB_RUN_ID, then BUILD_BUILDID, and is otherwise random.
Sync strategies
deploy(the default) deploys once per session and reads templates from the deployed jobs. This is the only strategy that builds and uploads wheels.syncrunsdatabricks bundle syncand reads templates fromdatabricks bundle validate. Jobs that use local wheels or JARs are refused, becausesyncdoes not upload them.nonedoes nothing and reads templates fromvalidate. Local wheel paths are pointed at the copies that an earlierdeployuploaded, after checking that they exist.
With sync and none, a job that uses ${resources...} references is refused, because validate does not resolve them.
Running in parallel
With pytest-xdist, one worker deploys and loads the templates while the others wait, then all of them reuse the result. Each worker uses its own WorkspaceClient.
With databricks-cli (U2M) authentication, several processes refreshing the token at once can fail with exit status 45. The plugin retries, but OAuth machine-to-machine credentials or a token are more reliable in CI.
Timing
Measured in one Azure workspace:
- A trivial serverless notebook test takes about 20 to 35 seconds end to end, of which about 2 seconds is API overhead.
- A job with
performance_target: STANDARDadds 3 to 4 minutes to every run. The plugin warns about it, andlakeflow_performance_optimized = trueoverrides it. - A classic job cluster takes 2 to 10 minutes to start.
- Five jobs at once took no longer than one.
Limitations
- Logs.
TaskRun.logsis only filled for Python script, wheel and JAR tasks, and holds only the last 5 MB. Notebook tasks have no logs; useoutputinstead. Notebook output keeps only the first 5 MB. The Databricks documentation says that on serverless compute the logs of one task can include output from other tasks in the same run. In our tests each task's logs held only its own output, but this is not guaranteed. - Task values are not available through the Jobs API, so they are read by probe tasks. Each probe costs 7 to 30 seconds of serverless compute, even when its task did not run.
- Job code must read its catalog and schema from job parameters, as described above.
- Single-task jobs keep only the target task. On a shared job cluster, a task can import libraries that other tasks declared. On its own it loses them, so declare every library on the task that needs it.
- Serverless auto-retry is turned off in ephemeral jobs by default, so a test sees one attempt. Set
lakeflow_disable_auto_optimization = falseto keep the platform's behaviour. When a task is retried,TaskRundescribes the last attempt. - Standard (shared) access mode classic clusters were not tested. There, task values set from wheel or script tasks may be silently lost.
Metadata
Release files for pytest-databricks-lakeflow 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pytest_databricks_lakeflow-0.1.0.tar.gz | 65.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_databricks_lakeflow-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 100.5 kB
Release files / pytest_databricks_lakeflow-0.1.0.tar.gz
| Download URL | pytest_databricks_lakeflow-0.1.0.tar.gz |
|---|---|
| Size | 65.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
04e54fb7f924385ba4337fc880096bb91cd261da8c33a3fced176c7e8de1d5b1
|
|
BLAKE2b-256 checksum How to use checksums |
cb85842b35746d2698344092d13f7462b235a3c1812c51b88202ac5229c9b392
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / pytest_databricks_lakeflow-0.1.0-py3-none-any.whl
| Download URL | pytest_databricks_lakeflow-0.1.0-py3-none-any.whl |
|---|---|
| Size | 34.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fb25610213781273c23ea5d41b4767a37d2d91ba77ec994aea8552fd93d1c2db
|
|
BLAKE2b-256 checksum How to use checksums |
5ea02b40e6fc060136c0175947d53161a2e3ccaa909e8492990e13221ca57299
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logSecurity for pytest-databricks-lakeflow 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pytest_databricks_lakeflow-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.