What you will be able to do
- Choose where to keep functions and their tests for Python, Scala and SQL notebooks
- Name test files so pytest and the workspace Tests pane discover them, and run pytest through Databricks Connect
- Run unittest-based tests inside a notebook and keep them separate with widgets or %run
- Test chains of pipeline tables in isolation with the Lakeflow pipeline testing framework, within its prerequisites and isolation rules
1.Decide where functions and tests live
A test framework can only discover and run tests that live somewhere it can reach. Databricks describes three ways to arrange functions and their unit tests. Each one trades reuse against the number of files you have to maintain, and the right choice depends on the notebook language.
| Layout | Benefit | Challenge | Recommended for |
|---|---|---|---|
| Functions and tests outside notebooks | Callable inside and outside notebooks; test frameworks run best there | Not supported for Scala notebooks; more files to track | Python and R notebooks |
| Functions in one notebook, tests in a separate notebook | Functions easier to reuse across notebooks | More notebooks; functions unusable outside notebooks | Scala notebooks |
| Functions and tests in the same notebook | One notebook to track | Harder to reuse; unusable outside notebooks | None of the three languages above |
| Functions as SQL UDFs in schemas | SQL notebooks call the UDFs and their tests | — | SQL notebooks |
Databricks also recommends writing and running unit tests from a notebook rather than the web terminal. The web terminal has more limitations, including no Spark support.
Checkpoint 1 of 6· Match them up
Match each notebook language to the layout Databricks recommends for it
Tap a term, then the definition that fits it.
Keeping tests outside notebooks is not supported for Scala, so Scala uses a separate test notebook. SQL logic becomes SQL UDFs.
“For Scala notebooks, Databricks recommends including functions in one notebook and their unit tests in a separate notebook.”Source: docs.databricks.com
Sources1
2.pytest naming, the Tests pane and Databricks Connect
If your tests live in files outside notebooks, pytest's naming conventions decide what runs. When you run pytest, it picks up files named test_*.py or *_test.py in the current directory and its subdirectories. Inside those files it looks for functions whose names start with test_.
The workspace applies the same conventions. When you open a valid Python test file, a Tests icon appears in the left sidebar. It recognises test-prefixed functions, and test-prefixed methods inside Test-prefixed classes that have no __init__ method. The pane lets you run all tests, rerun only the failures, or run a single test, and it marks each test as passed or failed. If a test fails, an inline indicator on the failing line opens the full error message. Discovery scope depends on location. In a pipeline, a Declarative Automation Bundle, or a Git folder with an attached cluster, discovery covers every file in the folder. Everywhere else it covers only the current file.
Checkpoint 2 of 6· Check yourself
Which file name would pytest NOT collect as a test file?
Only test_*.py and *_test.py match. transformstest.py has neither the test_ prefix nor the _test suffix.
“Running pytest runs all files whose names follow the form test_*.py or /*_test.py in the current directory and its subdirectories.”Source: docs.databricks.com
pytest can also run on your own machine against Databricks compute through Databricks Connect, for Databricks Runtime 13.3 LTS and above. Databricks Connect and PySpark are mutually exclusive in the same environment. When you run from the terminal, pytest only works with the DEFAULT configuration profile, and that profile names the cluster or serverless compute to use. The documented example tests a function that reads samples.nyctaxi.trips, a real workspace table, and asserts that it returns at least one row. That makes it a test of code and data together, not a test of an isolated function.
Checkpoint 3 of 6· Fill the gap
Which class builds the session in this Databricks Connect helper?
def get_spark() -> SparkSession:
spark = ? .builder.getOrCreate()
return sparkWith Databricks Connect, you get the session from DatabricksSession, imported from databricks.connect. The return annotation still says SparkSession, and that is what makes the first option tempting.
Source: docs.databricks.com3.Testing inside notebooks
Not every test needs pytest. Many testing libraries run directly in a notebook, including Python's built-in unittest package. Failures appear in the cell's output area.
def reverse(s):
return s[::-1]
import unittest
class TestHelpers(unittest.TestCase):
def test_reverse(self):
self.assertEqual(reverse('abc'), 'cba')
r = unittest.main(argv=[''], verbosity=2, exit=False)
assert r.result.wasSuccessful(), 'Test failed; see logs above'There are three documented ways to keep test code from getting in the way of normal code. Widgets let one notebook switch between test runs and normal runs: a Mode dropdown decides whether the cell runs asserts or does the real work. The %run magic moves the tests into a separate notebook that pulls in the shared code. Databricks Git folders let tests live in non-notebook source files. To run tests automatically, schedule the notebook as a job with email notifications. For notebooks in a Git folder, you can run the tests on every commit with Databricks GitHub Actions.
Checkpoint 4 of 6· Check yourself
You want a single notebook to run its asserts when you are testing and do real work otherwise. Which documented feature does this?
A widget such as a Mode dropdown separates test runs from normal runs inside one notebook. %run is for moving tests into a different notebook.
“You can use widgets to distinguish test invocations from normal invocations in a single notebook.”Source: docs.databricks.com
Sources4
4.Integration-style tests across pipeline tables
Databricks' notebook unit-testing article explicitly excludes integration testing. For pipelines, the tool that tests how tables work together is the Lakeflow pipeline unit testing framework, which is in Beta. It provides a test SparkSession that sends table operations to a temporary test schema in the pipeline's default catalog. You mock inputs by table name, run part or all of the pipeline graph on the pipeline's compute, and check the output tables with ordinary pytest asserts. One documented use case is testing chains such as bronze, silver and gold, to confirm that data moves correctly through the graph.
| Requirement | What is needed |
|---|---|
| Pipeline permission | Owner (IS OWNER) |
| Catalog privileges | USE CATALOG and CREATE SCHEMA on the pipeline's default catalog |
| Pipeline mode | Triggered (non-continuous) |
| Runtime | Databricks Runtime 18.1 or above |
| Spark Connect | Not supported |
# Mock data fixture
def mock_users(session):
session.sql("""
CREATE TABLE catalog.schema.wanderbricks_users AS
SELECT * FROM VALUES
(1, 'alice@example.com', 'Alice', 'admin'),
(2, NULL, 'Bob', 'user'),
(3, 'charlie@example.com', 'Charlie', 'user'),
(4, NULL, 'Dana', 'admin')
AS t(user_id, email, name, user_type)
""")
# Test 1: Row count
def test_users_row_count(test_spark):
mock_users(test_spark)
status = test_pipeline.run(test_spark, set(["catalog.schema.users"]))
assert status.is_success
result = test_spark.table("catalog.schema.users")
assert result.count() == 4test_pipeline is TestPipeline.active(), imported from pyspark.pipelines.testing. Pass it a set of table names to run just those outputs. To test a dependent table such as counts, include the table it reads from as well. Call test_pipeline.run(test_spark) with no set to run the whole pipeline. Besides plain tables, the framework can also validate Auto CDC flows, expectations and append flows.
Isolation applies only to operations that reference a table by name. Reading or writing by path, or reading from connectors such as Kafka or Auto Loader, goes around the isolation and hits production. That includes any upstream flow the selected outputs depend on. Do not run GRANT, REVOKE or ownership statements, and do not create catalogs or schemas, inside tests.
Checkpoint 5 of 6· Put it in order
Put the steps of a pipeline table test in order
- 1.Create the mock input table by name with test_spark
- 2.Call test_pipeline.run with the set of output tables to test
- 3.Assert on the result, e.g. row count or assertDataFrameEqual
- 4.Assert status.is_success
- 5.Read the output with test_spark.table
Each documented test mocks its input first, runs the selected outputs, confirms the run succeeded, and only then reads and checks the output table.
“creating mock user data with intentional nulls and running the pipeline in isolation.”Source: docs.databricks.com
Checkpoint 6 of 6· Exam question
A team is refactoring a monolithic PySpark ETL function into small, independently testable steps using `DataFrame.transform`. Which of the following practices align with recommended patterns for writing unit-testable transformation chains with `.transform`? (Select all that apply)(Select 3)
Correct answers: A, B, C — Write each transformation as a standalone function accepting and returning a DataFrame, such as `def add_flag(df): return df.withColumn(...)`, so it can be unit tested in isolation before being composed.; Chain the pipeline as `df.transform(add_flag).transform(dedupe_rows).transform(cast_types)`, keeping each step a named, importable function that a separate test module can exercise on its own.; Call `assertDataFrameEqual` against a small, hand-built expected DataFrame after each individual transformation function, rather than validating correctness only at the end of the full chain.
- A. Defining a transformation as a plain function of DataFrame in, DataFrame out is exactly what `.transform` expects, and it makes the function directly callable and assertable in isolation with a small synthetic input. This is a core pattern for unit-testable pipelines.
- B. Chaining named functions through `.transform` keeps the pipeline readable while preserving each step as an independently importable unit that a test module can call directly. This composition style is what makes granular unit testing practical.
- C. Testing each transformation function against a small expected DataFrame catches regressions at the step where they are introduced, rather than only at the end of a long chain where the root cause is harder to isolate. This is a standard unit-testing practice for `.transform` chains.
- D. Silently swallowing exceptions and returning the unmodified input defeats the purpose of testing: a broken transformation would never surface as a test failure, masking real regressions. This is an anti-pattern, not a recommended practice.
- E. `DataFrame.transform` takes a callable, not a string, and resolving function names via `eval()` introduces an unnecessary code-injection risk while contradicting the actual API contract. This describes incorrect usage of `.transform`.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Pipeline unit tests are isolated, so it is safe to run outputs whose upstream flow reads from Auto Loader or a /Volumes path.Why is that wrong?
Isolation covers only table operations by name. Path-based reads and connectors go around it and act on production systems, including in transitive dependencies.
2.CAN MANAGE on a pipeline is enough to run its unit tests.Why is that wrong?
Only the pipeline Owner can run tests. You also need USE CATALOG and CREATE SCHEMA on the default catalog, so the framework can create its temporary test schema.
3.The workspace Tests pane always discovers tests across the whole folder.Why is that wrong?
Folder-wide discovery happens only in a pipeline, a Declarative Automation Bundle, or a Git folder with an attached cluster. Everywhere else, discovery covers only the current file.
Covered in pytest naming, the Tests pane and Databricks Connect
Practise it for real
Run a unittest suite inside a Databricks notebook, watch it fail, and then move the code under test into a separate notebook with %run.
1.In a Python notebook, paste the reverse function and the TestHelpers unittest class from the notebook-testing section into one cell, then run it.
Why: This shows that a built-in test library runs directly in a notebook cell.
You should see: Verbose unittest output for test_reverse, which passes, and no assertion error.
2.Change the expected value 'cba' to 'abc' and run the cell again.
Why: You want to see how a failure is reported.
You should see: The failure appears in the cell's output area, and the final assert raises 'Test failed; see logs above'.
3.Restore 'cba'. In a new cell, add dbutils.widgets.dropdown("Mode", "Test", ["Test", "Normal"]) and an if/else that asserts reverse('abc') == 'cba' in Test mode and prints reverse('desrever') otherwise. Switch the dropdown to Normal.
Why: Widgets let one notebook separate test runs from normal runs.
You should see: In Test mode it prints 'Tests passed'. In Normal mode it prints 'reversed'.
4.Move reverse into a notebook named shared-code-notebook in the same folder. Create shared-code-notebook-test whose first cell is %run ./shared-code-notebook and whose second cell holds the unittest code.
Why: %run keeps the test code in a separate notebook from the code it tests.
You should see: The test notebook finds reverse through %run, and test_reverse passes.
Stuck? Get a nudge
If the test notebook can't find reverse, make sure both notebooks are in the same workspace folder and that the %run cell runs first.
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.https://docs.databricks.com/aws/en/notebooks/testingOfficial docs
“For Python and R notebooks, Databricks recommends storing functions and their unit tests outside of notebooks.”
↩︎ Decide where functions and tests live“This approach is not supported for Scala notebooks.”
↩︎ Decide where functions and tests live“does not cover other kinds of testing methods, such as integration testing”
↩︎ Integration-style tests across pipeline tables“For Scala notebooks, Databricks recommends including functions in one notebook and their unit tests in a separate notebook.”
↩︎ Checkpoint - 2.
“Databricks follows pytest naming conventions to detect test files, classes and cases.”
↩︎ pytest naming, the Tests pane and Databricks Connect“In other spaces, discovery is limited to the current file.”
↩︎ Exam trap 3 - 3.
“When running Databricks Connect from the terminal, pytest only works with the DEFAULT configuration profile.”
↩︎ pytest naming, the Tests pane and Databricks Connect“Databricks Connect and PySpark are mutually exclusive.”
↩︎ pytest naming, the Tests pane and Databricks Connect - 4.
“Test failures appear in the output area of the cell.”
↩︎ Testing inside notebooks“When you use %run, test code is included in a separate notebook that you call from another notebook.”
↩︎ Testing inside notebooks“You can use widgets to distinguish test invocations from normal invocations in a single notebook.”
↩︎ Checkpoint - 5.https://docs.databricks.com/aws/en/ldp/unit-testingOfficial docs
“Test chains of transformations (for example, bronze, silver, and gold) to validate that data flows correctly through your pipeline graph.”
↩︎ Integration-style tests across pipeline tables“Pipeline must be configured in triggered (non-continuous) mode.”
↩︎ Integration-style tests across pipeline tables“do not read from connectors such as Kafka or Auto Loader. These bypass isolation and act on real production systems.”
↩︎ Exam trap 1“You must be the pipeline Owner (IS OWNER); CAN RUN and CAN MANAGE are not sufficient to run tests.”
↩︎ Exam trap 2“CAN RUN and CAN MANAGE are not sufficient to run tests.”
↩︎ Prediction“creating mock user data with intentional nulls and running the pipeline in isolation.”
↩︎ Checkpoint
Also cited
“Running pytest runs all files whose names follow the form test_*.py or /*_test.py in the current directory and its subdirectories.”
↩︎ Checkpoint