Skip to content

Commit f848cb6

Browse files
committed
feature: adding smarter unit test execution
Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com>
1 parent 7bd09ef commit f848cb6

14 files changed

Lines changed: 262 additions & 24 deletions

File tree

docs/concepts/overview.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ Bad data is worse than no data. The best way to keep bad data out of your system
5252
### [Tests](./tests.md)
5353
SQLMesh "tests" are similar to unit tests in software development, where the unit is a single model. SQLMesh tests validate model *code* &mdash; you specify the input data and expected output, then SQLMesh runs the test and compares the expected and actual output.
5454

55-
SQLMesh automatically runs tests when you apply a `plan`, or you can run them on demand with the [`test` command](../reference/cli.md#test).
55+
SQLMesh automatically runs tests for models included in a `plan` (added, modified, or restated). Plans with no such models, and `--skip-backfill` / `--dry-run` plans, skip unit tests by default. Use `--all-tests` for the full suite, `--skip-tests` to skip, or run tests on demand with the [`test` command](../reference/cli.md#test).
56+
57+
Learn more in the [testing guide](../guides/testing.md).
5658

5759
### [Audits](./audits.md)
5860
In contrast to tests, SQLMesh "audits" validate the results of model code applied to your actual data.

docs/concepts/tests.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Testing
22

3-
Testing allows you to protect your project from regression by continuously verifying that the output of each model matches your expectations. Unlike [audits](audits.md), tests are executed either on demand (for example, as part of a CI/CD job) or every time a new [plan](plans.md) is created.
3+
Testing allows you to protect your project from regression by continuously verifying that the output of each model matches your expectations. Unlike [audits](audits.md), tests are executed either on demand (for example, as part of a CI/CD job or via [`sqlmesh test`](../reference/cli.md#test)) or when a new [plan](plans.md) is created.
4+
5+
By default, `sqlmesh plan` runs unit tests only for models included in the plan (added, modified, or restated). Plans with no such models skip unit tests. Use `--all-tests` to run the full suite, or `--skip-tests` to run none.
6+
7+
!!! important
8+
9+
`--skip-backfill` / `--dry-run` plans skip unit tests by default. Pass `--all-tests` if you still want the full suite to run.
410

511
Similar to unit testing in software development, SQLMesh evaluates the model's logic against predefined inputs and then compares the output to expected outcomes provided as part of each test.
612

docs/faq/faq.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666

6767
However, the commands to create and apply a plan are different. In Terraform, the "plan" command generates a plan and saves it to file. The "apply" command reads a plan file and applies it.
6868

69-
In SQLMesh, the `sqlmesh plan` command generates a plan, runs any unit tests, and prompts the user to apply the plan. There is no "apply" command in SQLMesh.
69+
In SQLMesh, the `sqlmesh plan` command generates a plan, runs unit tests for models included in the plan, and prompts the user to apply the plan. There is no "apply" command in SQLMesh.
7070

7171
## Getting Started
7272

@@ -102,7 +102,7 @@
102102
SQLMesh's default behavior is appropriate for most deployments, but you can override where SQLMesh creates physical tables and views with [schema configuration options](../guides/configuration.md#environment-schemas).
103103

104104
??? question "What's the difference between a `test` and an `audit`?"
105-
A SQLMesh [`test`](../concepts/tests.md) is analogous to a "unit test" in software engineering. It tests *code* based on known inputs and outputs. In SQLMesh, the inputs and outputs are specified in a YAML file, and SQLMesh automatically runs them when `sqlmesh plan` is executed.
105+
A SQLMesh [`test`](../concepts/tests.md) is analogous to a "unit test" in software engineering. It tests *code* based on known inputs and outputs. In SQLMesh, the inputs and outputs are specified in a YAML file, and SQLMesh runs tests for models included in the plan when `sqlmesh plan` is executed (use `--all-tests` for the full suite).
106106

107107
Writing YAML is annoying and error-prone, so SQLMesh's [`create_test` command](../concepts/tests.md#automatic-test-generation) allows you to automatically generate YAML test files based on queries of existing data tables.
108108

@@ -126,7 +126,7 @@
126126
??? question "What's the difference between `sqlmesh plan` and `sqlmesh run`?"
127127
During project development, there are two things in play: the current state of your project files and the existing states of each environment you have.
128128

129-
SQLMesh’s `plan` command is the primary tool for understanding the effects of changes you make to your project. If your project files have changed or are different from the state of an environment, you execute `sqlmesh plan [environment name]` to synchronize the environment's state with your project files. `sqlmesh plan` will generate a summary of the actions needed to implement the changes, automatically run unit tests, and prompt you to `apply` the plan and implement the changes.
129+
SQLMesh’s `plan` command is the primary tool for understanding the effects of changes you make to your project. If your project files have changed or are different from the state of an environment, you execute `sqlmesh plan [environment name]` to synchronize the environment's state with your project files. `sqlmesh plan` will generate a summary of the actions needed to implement the changes, run unit tests for models included in the plan, and prompt you to `apply` the plan and implement the changes.
130130

131131
If your project files have not changed, you execute `sqlmesh run` to run your project's models and audits.
132132

@@ -210,7 +210,7 @@
210210
- Configure your project and set up a project database (using DuckDB locally or a database connection)
211211
- Create, configure, and modify models, audits, tests, and other project components
212212
- Execute `sqlmesh plan [environment name]` to:
213-
- Generate a summary of the differences between your project files and the environment and whether each change is `breaking`. The `plan` includes a list of the actions needed to implement the changes and automatically runs the project's unit `test`s.
213+
- Generate a summary of the differences between your project files and the environment and whether each change is `breaking`. The `plan` includes a list of the actions needed to implement the changes and runs unit `test`s for models included in the plan.
214214
- Optionally apply the plan to implement the actions and run the project's `audit`s.
215215
- Execute `sqlmesh run` on a schedule to ingest and transform new data
216216

docs/guides/models.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,11 @@ Reverting to a previous model version is a quick operation since no additional w
161161
162162
SQLMesh automatically validates your models in order to ensure the quality and accuracy of your data. This is done via the following:
163163
164-
* Running unit tests by default when you execute the `plan` command. This ensures all changes to applied to any environment are logically validated. Refer to [testing](../concepts/tests.md) for more information.
164+
* Running unit tests for models in the plan when you execute the `plan` command (use `--all-tests` for the full suite, or `--skip-tests` to skip). This ensures changes applied to any environment are logically validated. Refer to [testing](../concepts/tests.md) for more information.
165+
166+
!!! important
167+
168+
`--skip-backfill` / `--dry-run` plans skip unit tests by default. Pass `--all-tests` if you still want tests to run.
165169
* Running audits whenever data is loaded to a table (either for backfill or loading on a cadence). This way you know all data present in any table has passed all defined audits. Refer to [auditing](../concepts/audits.md) for more information.
166170
167171
SQLMesh also provides automatic validation via CI/CD by automatically creating a preview environment.

docs/guides/testing.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ OK
1212
```
1313
As the unit tests run, SQLMesh will identify any that fail.
1414

15+
By default, `sqlmesh plan` runs unit tests only for models included in the plan (added, modified, or restated). Plans with no such models skip unit tests. Use `--all-tests` to run the full suite, or `--skip-tests` to run none.
16+
17+
!!! important
18+
19+
`--skip-backfill` / `--dry-run` plans skip unit tests by default. Pass `--all-tests` if you still want the full suite to run.
20+
1521
For more information about tests, refer to [testing](../concepts/tests.md).
1622

1723
### Test changes to a specific model

docs/reference/cli.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ Options:
357357
Default: prod.
358358
--skip-tests Skip tests prior to generating the plan if
359359
they are defined.
360+
--all-tests Run all unit tests instead of only tests for
361+
models included in the plan.
360362
--skip-linter Skip linting prior to generating the plan if
361363
the linter is enabled.
362364
-r, --restate-model TEXT Restate data for specified models and models
@@ -371,7 +373,8 @@ Options:
371373
when comparing to existing snapshots for
372374
matching models in the target environment.
373375
--skip-backfill, --dry-run Skip the backfill step and only create a
374-
virtual update for the plan.
376+
virtual update for the plan. Unit tests are
377+
also skipped unless --all-tests is passed.
375378
--empty-backfill Produce empty backfill. Like --skip-backfill
376379
no models will be backfilled, unlike --skip-
377380
backfill missing intervals will be recorded

docs/reference/notebook.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ options:
9696
#### plan
9797
```
9898
%plan [--start START] [--end END] [--execution-time EXECUTION_TIME]
99-
[--create-from CREATE_FROM] [--skip-tests]
99+
[--create-from CREATE_FROM] [--skip-tests] [--all-tests]
100100
[--restate-model [RESTATE_MODEL ...]] [--no-gaps]
101101
[--skip-backfill, --dry-run] [--forward-only]
102102
[--effective-from EFFECTIVE_FROM] [--no-prompts] [--auto-apply]
@@ -120,6 +120,8 @@ options:
120120
The environment to create the target environment from
121121
if it doesn't exist. Default: prod.
122122
--skip-tests, -t Skip the unit tests defined for the model.
123+
--all-tests Run all unit tests instead of only tests for models
124+
included in the plan.
123125
--restate-model <[RESTATE_MODEL ...]>, -r <[RESTATE_MODEL ...]>
124126
Restate data for specified models (and models
125127
downstream from the one specified). For production
@@ -131,7 +133,9 @@ options:
131133
comparing to existing snapshots for matching models in
132134
the target environment.
133135
--skip-backfill, --dry-run
134-
Skip the backfill step and only create a virtual update for the plan.
136+
Skip the backfill step and only create a virtual
137+
update for the plan. Unit tests are also skipped
138+
unless --all-tests is passed.
135139
--forward-only Create a plan for forward-only changes.
136140
--effective-from EFFECTIVE_FROM
137141
The effective date from which to apply forward-only

sqlmesh/cli/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,12 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None:
423423
help="Skip tests prior to generating the plan if they are defined.",
424424
default=None,
425425
)
426+
@click.option(
427+
"--all-tests",
428+
is_flag=True,
429+
help="Run all unit tests instead of only tests for models included in the plan.",
430+
default=None,
431+
)
426432
@click.option(
427433
"--skip-linter",
428434
is_flag=True,
@@ -446,7 +452,7 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None:
446452
"--skip-backfill",
447453
"--dry-run",
448454
is_flag=True,
449-
help="Skip the backfill step and only create a virtual update for the plan.",
455+
help="Skip the backfill step and only create a virtual update for the plan. Unit tests are also skipped unless --all-tests is passed.",
450456
default=None,
451457
)
452458
@click.option(

sqlmesh/core/context.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
ModelTestMetadata,
116116
generate_test,
117117
run_tests,
118+
filter_tests_by_model_names,
118119
filter_tests_by_patterns,
119120
)
120121
from sqlmesh.core.user import User
@@ -1347,6 +1348,7 @@ def plan(
13471348
execution_time: t.Optional[TimeLike] = None,
13481349
create_from: t.Optional[str] = None,
13491350
skip_tests: t.Optional[bool] = None,
1351+
all_tests: t.Optional[bool] = None,
13501352
restate_models: t.Optional[t.Iterable[str]] = None,
13511353
no_gaps: t.Optional[bool] = None,
13521354
skip_backfill: t.Optional[bool] = None,
@@ -1384,6 +1386,7 @@ def plan(
13841386
create_from: The environment to create the target environment from if it
13851387
doesn't exist. If not specified, the "prod" environment will be used.
13861388
skip_tests: Unit tests are run by default so this will skip them if enabled
1389+
all_tests: Run every loaded unit test instead of only tests for models in the plan
13871390
restate_models: A list of either internal or external models, or tags, that need to be restated
13881391
for the given plan interval. If the target environment is a production environment,
13891392
ALL snapshots that depended on these upstream tables will have their intervals deleted
@@ -1430,6 +1433,7 @@ def plan(
14301433
execution_time=execution_time,
14311434
create_from=create_from,
14321435
skip_tests=skip_tests,
1436+
all_tests=all_tests,
14331437
restate_models=restate_models,
14341438
no_gaps=no_gaps,
14351439
skip_backfill=skip_backfill,
@@ -1484,6 +1488,7 @@ def plan_builder(
14841488
execution_time: t.Optional[TimeLike] = None,
14851489
create_from: t.Optional[str] = None,
14861490
skip_tests: t.Optional[bool] = None,
1491+
all_tests: t.Optional[bool] = None,
14871492
restate_models: t.Optional[t.Iterable[str]] = None,
14881493
no_gaps: t.Optional[bool] = None,
14891494
skip_backfill: t.Optional[bool] = None,
@@ -1518,6 +1523,7 @@ def plan_builder(
15181523
create_from: The environment to create the target environment from if it
15191524
doesn't exist. If not specified, the "prod" environment will be used.
15201525
skip_tests: Unit tests are run by default so this will skip them if enabled
1526+
all_tests: Run every loaded unit test instead of only tests for models in the plan
15211527
restate_models: A list of either internal or external models, or tags, that need to be restated
15221528
for the given plan interval. If the target environment is a production environment,
15231529
ALL snapshots that depended on these upstream tables will have their intervals deleted
@@ -1559,6 +1565,7 @@ def plan_builder(
15591565
"execution_time": execution_time,
15601566
"create_from": create_from,
15611567
"skip_tests": skip_tests,
1568+
"all_tests": all_tests,
15621569
"restate_models": list(restate_models) if restate_models is not None else None,
15631570
"no_gaps": no_gaps,
15641571
"skip_backfill": skip_backfill,
@@ -1588,6 +1595,9 @@ def plan_builder(
15881595
}
15891596

15901597
skip_tests = explain or skip_tests or False
1598+
all_tests = all_tests or False
1599+
if skip_tests and all_tests:
1600+
raise PlanError("Cannot combine --all-tests with --skip-tests.")
15911601
no_gaps = no_gaps or False
15921602
skip_backfill = skip_backfill or False
15931603
empty_backfill = empty_backfill or False
@@ -1596,6 +1606,10 @@ def plan_builder(
15961606
skip_linter = skip_linter or False
15971607
min_intervals = min_intervals or 0
15981608

1609+
# Virtual-only plans (--skip-backfill / --dry-run) skip unit tests unless --all-tests.
1610+
if skip_backfill and not all_tests:
1611+
skip_tests = True
1612+
15991613
environment = environment or self.config.default_target_environment
16001614
environment = Environment.sanitize_name(environment)
16011615
is_dev = environment != c.PROD
@@ -1614,8 +1628,6 @@ def plan_builder(
16141628
if not skip_linter:
16151629
self.lint_models()
16161630

1617-
self._run_plan_tests(skip_tests=skip_tests)
1618-
16191631
environment_ttl = (
16201632
self.environment_ttl if environment not in self.pinned_environments else None
16211633
)
@@ -1698,6 +1710,15 @@ def plan_builder(
16981710
*[s.name for s in context_diff.added],
16991711
}
17001712

1713+
self._run_plan_tests(
1714+
skip_tests=skip_tests,
1715+
all_tests=all_tests,
1716+
model_names={
1717+
*modified_model_names,
1718+
*(expanded_restate_models or set()),
1719+
},
1720+
)
1721+
17011722
if (
17021723
is_dev
17031724
and not include_unmodified
@@ -2314,14 +2335,15 @@ def test(
23142335
verbosity: Verbosity = Verbosity.DEFAULT,
23152336
preserve_fixtures: bool = False,
23162337
stream: t.Optional[t.TextIO] = None,
2338+
model_names: t.Optional[t.Collection[str]] = None,
23172339
) -> ModelTextTestResult:
23182340
"""Discover and run model tests"""
23192341
if verbosity >= Verbosity.VERBOSE:
23202342
import pandas as pd
23212343

23222344
pd.set_option("display.max_columns", None)
23232345

2324-
test_meta = self.select_tests(tests=tests, patterns=match_patterns)
2346+
test_meta = self.select_tests(tests=tests, patterns=match_patterns, model_names=model_names)
23252347

23262348
result = run_tests(
23272349
model_test_metadata=test_meta,
@@ -2781,15 +2803,24 @@ def _run_tests(
27812803
result = self.test(stream=test_output_io, verbosity=verbosity)
27822804
return result, test_output_io.getvalue()
27832805

2784-
def _run_plan_tests(self, skip_tests: bool = False) -> t.Optional[ModelTextTestResult]:
2785-
if not skip_tests:
2786-
result = self.test()
2787-
if not result.wasSuccessful():
2788-
raise PlanError(
2789-
"Cannot generate plan due to failing test(s). Fix test(s) and run again."
2790-
)
2791-
return result
2792-
return None
2806+
def _run_plan_tests(
2807+
self,
2808+
skip_tests: bool = False,
2809+
all_tests: bool = False,
2810+
model_names: t.Optional[t.Collection[str]] = None,
2811+
) -> t.Optional[ModelTextTestResult]:
2812+
if skip_tests:
2813+
return None
2814+
2815+
if not all_tests and model_names is not None and not model_names:
2816+
return None
2817+
2818+
result = self.test(model_names=None if all_tests else model_names)
2819+
if not result.wasSuccessful():
2820+
raise PlanError(
2821+
"Cannot generate plan due to failing test(s). Fix test(s) and run again."
2822+
)
2823+
return result
27932824

27942825
def _warn_if_virtual_catalog_rematerialization(self, plan: "Plan") -> None:
27952826
"""Warn when ClickHouse models appear as new snapshots solely because a virtual catalog
@@ -3465,6 +3496,7 @@ def select_tests(
34653496
self,
34663497
tests: t.Optional[t.List[str]] = None,
34673498
patterns: t.Optional[t.List[str]] = None,
3499+
model_names: t.Optional[t.Collection[str]] = None,
34683500
) -> t.List[ModelTestMetadata]:
34693501
"""Filter pre-loaded test metadata based on tests and patterns."""
34703502

@@ -3488,6 +3520,14 @@ def select_tests(
34883520
if patterns:
34893521
test_meta = filter_tests_by_patterns(test_meta, patterns)
34903522

3523+
if model_names is not None:
3524+
test_meta = filter_tests_by_model_names(
3525+
test_meta,
3526+
set(model_names),
3527+
default_catalog=self.default_catalog,
3528+
dialect=self.default_dialect,
3529+
)
3530+
34913531
return test_meta
34923532

34933533

sqlmesh/core/test/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from sqlmesh.core.test.definition import ModelTest as ModelTest, generate_test as generate_test
44
from sqlmesh.core.test.discovery import (
55
ModelTestMetadata as ModelTestMetadata,
6+
filter_tests_by_model_names as filter_tests_by_model_names,
67
filter_tests_by_patterns as filter_tests_by_patterns,
78
)
89
from sqlmesh.core.test.result import ModelTextTestResult as ModelTextTestResult

0 commit comments

Comments
 (0)