slimtest 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. slimtest-0.1.0/.github/CODEOWNERS +9 -0
  2. slimtest-0.1.0/.github/workflows/ci.yml +45 -0
  3. slimtest-0.1.0/.github/workflows/release-testpypi.yml +58 -0
  4. slimtest-0.1.0/.github/workflows/release.yml +73 -0
  5. slimtest-0.1.0/.gitignore +60 -0
  6. slimtest-0.1.0/CHANGELOG.md +38 -0
  7. slimtest-0.1.0/LICENSE +21 -0
  8. slimtest-0.1.0/PKG-INFO +375 -0
  9. slimtest-0.1.0/README.md +344 -0
  10. slimtest-0.1.0/examples/sample_dbt_project/.gitignore +6 -0
  11. slimtest-0.1.0/examples/sample_dbt_project/README.md +88 -0
  12. slimtest-0.1.0/examples/sample_dbt_project/dbt_project.yml +26 -0
  13. slimtest-0.1.0/examples/sample_dbt_project/models/marts/order_statuses.sql +52 -0
  14. slimtest-0.1.0/examples/sample_dbt_project/models/marts/order_statuses.yml +145 -0
  15. slimtest-0.1.0/examples/sample_dbt_project/profiles.yml +7 -0
  16. slimtest-0.1.0/examples/sample_dbt_project/seeds/customers.csv +4 -0
  17. slimtest-0.1.0/examples/sample_dbt_project/seeds/order_events.csv +13 -0
  18. slimtest-0.1.0/examples/sample_dbt_project/seeds/orders.csv +4 -0
  19. slimtest-0.1.0/examples/sample_dbt_project/seeds/products.csv +4 -0
  20. slimtest-0.1.0/examples/sample_dbt_project/tests/slimtest_factories/customers.yml +14 -0
  21. slimtest-0.1.0/examples/sample_dbt_project/tests/slimtest_factories/order_events.yml +15 -0
  22. slimtest-0.1.0/examples/sample_dbt_project/tests/slimtest_factories/orders.yml +17 -0
  23. slimtest-0.1.0/examples/sample_dbt_project/tests/slimtest_factories/products.yml +10 -0
  24. slimtest-0.1.0/pyproject.toml +127 -0
  25. slimtest-0.1.0/src/slimtest/__init__.py +13 -0
  26. slimtest-0.1.0/src/slimtest/cli.py +178 -0
  27. slimtest-0.1.0/src/slimtest/compile.py +268 -0
  28. slimtest-0.1.0/src/slimtest/config.py +57 -0
  29. slimtest-0.1.0/src/slimtest/dbt_project.py +109 -0
  30. slimtest-0.1.0/src/slimtest/dbt_runner.py +124 -0
  31. slimtest-0.1.0/src/slimtest/deep_merge.py +34 -0
  32. slimtest-0.1.0/src/slimtest/expand.py +141 -0
  33. slimtest-0.1.0/src/slimtest/factory.py +191 -0
  34. slimtest-0.1.0/src/slimtest/manifest.py +211 -0
  35. slimtest-0.1.0/src/slimtest/model_parser.py +197 -0
  36. slimtest-0.1.0/src/slimtest/parametrize.py +119 -0
  37. slimtest-0.1.0/src/slimtest/result_parser.py +127 -0
  38. slimtest-0.1.0/src/slimtest/runner.py +140 -0
  39. slimtest-0.1.0/src/slimtest/scenario.py +60 -0
  40. slimtest-0.1.0/src/slimtest/schema.py +182 -0
  41. slimtest-0.1.0/src/slimtest/selector.py +52 -0
  42. slimtest-0.1.0/tests/__init__.py +0 -0
  43. slimtest-0.1.0/tests/conftest.py +41 -0
  44. slimtest-0.1.0/tests/test_cli.py +212 -0
  45. slimtest-0.1.0/tests/test_compile.py +671 -0
  46. slimtest-0.1.0/tests/test_config.py +48 -0
  47. slimtest-0.1.0/tests/test_dbt_project.py +106 -0
  48. slimtest-0.1.0/tests/test_dbt_runner.py +156 -0
  49. slimtest-0.1.0/tests/test_deep_merge.py +99 -0
  50. slimtest-0.1.0/tests/test_expand.py +332 -0
  51. slimtest-0.1.0/tests/test_factory.py +186 -0
  52. slimtest-0.1.0/tests/test_manifest.py +222 -0
  53. slimtest-0.1.0/tests/test_model_parser.py +217 -0
  54. slimtest-0.1.0/tests/test_parametrize.py +189 -0
  55. slimtest-0.1.0/tests/test_result_parser.py +156 -0
  56. slimtest-0.1.0/tests/test_runner.py +167 -0
  57. slimtest-0.1.0/tests/test_scenario.py +68 -0
  58. slimtest-0.1.0/tests/test_schema.py +180 -0
  59. slimtest-0.1.0/tests/test_selector.py +104 -0
  60. slimtest-0.1.0/uv.lock +688 -0
@@ -0,0 +1,9 @@
1
+ # All files require @Hisaaki-Kato's review by default.
2
+ # CI/release workflows are especially security-sensitive; called out
3
+ # explicitly so the ownership stays intentional even if the catch-all
4
+ # rule is ever relaxed for community contributions.
5
+ * @Hisaaki-Kato
6
+ /.github/ @Hisaaki-Kato
7
+ /.github/workflows/ @Hisaaki-Kato
8
+ /pyproject.toml @Hisaaki-Kato
9
+ /LICENSE @Hisaaki-Kato
@@ -0,0 +1,45 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ name: pytest / ruff / mypy (Python ${{ matrix.python-version }})
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ python-version: ["3.11", "3.12", "3.13"]
21
+ steps:
22
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4
26
+ with:
27
+ enable-cache: true
28
+
29
+ - name: Set up Python ${{ matrix.python-version }}
30
+ run: uv python install ${{ matrix.python-version }}
31
+
32
+ - name: Sync dependencies
33
+ run: uv sync --python ${{ matrix.python-version }}
34
+
35
+ - name: Lint (ruff check)
36
+ run: uv run --python ${{ matrix.python-version }} ruff check .
37
+
38
+ - name: Format (ruff format --check)
39
+ run: uv run --python ${{ matrix.python-version }} ruff format --check .
40
+
41
+ - name: Type-check (mypy)
42
+ run: uv run --python ${{ matrix.python-version }} mypy src tests
43
+
44
+ - name: Test (pytest)
45
+ run: uv run --python ${{ matrix.python-version }} pytest -q
@@ -0,0 +1,58 @@
1
+ name: Release (TestPyPI dry run)
2
+
3
+ # Manually-triggered dry run against https://test.pypi.org. Use before
4
+ # a real `v*` tag: exercises the full pipeline (build + quality gate +
5
+ # Trusted Publisher upload) without burning a version number on the
6
+ # real PyPI.
7
+ #
8
+ # Setup on TestPyPI: register a Trusted Publisher at
9
+ # https://test.pypi.org/manage/account/publishing/
10
+ # with
11
+ # Owner: Hisaaki-Kato
12
+ # Repository: slimtest
13
+ # Workflow: release-testpypi.yml
14
+ # Environment: testpypi
15
+ #
16
+ # In GitHub, create the `testpypi` environment mirroring `pypi`
17
+ # (Settings -> Environments) with your user as a required reviewer,
18
+ # so the workflow pauses for a manual approval before uploading.
19
+
20
+ on:
21
+ workflow_dispatch:
22
+
23
+ jobs:
24
+ build-and-publish:
25
+ name: Build and publish to TestPyPI
26
+ runs-on: ubuntu-latest
27
+ environment:
28
+ name: testpypi
29
+ url: https://test.pypi.org/p/slimtest
30
+ permissions:
31
+ # PyPI Trusted Publishers OIDC exchange.
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
35
+
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4
38
+
39
+ - name: Set up Python
40
+ run: uv python install 3.12
41
+
42
+ - name: Sync dependencies
43
+ run: uv sync
44
+
45
+ - name: Run full quality gate
46
+ run: |
47
+ uv run ruff check .
48
+ uv run ruff format --check .
49
+ uv run mypy src tests
50
+ uv run pytest -q
51
+
52
+ - name: Build sdist and wheel
53
+ run: uv build
54
+
55
+ - name: Publish to TestPyPI
56
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
57
+ with:
58
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,73 @@
1
+ name: Release
2
+
3
+ # Publish to PyPI when a `v*` tag is pushed.
4
+ #
5
+ # Uses PyPI Trusted Publishers (OIDC): no API token required in secrets.
6
+ # Setup: once the workflow file lands on `main`, register the publisher
7
+ # at https://pypi.org/manage/account/publishing/ with
8
+ # repo: Hisaaki-Kato/slimtest
9
+ # workflow: release.yml
10
+ # env: (leave blank)
11
+
12
+ on:
13
+ push:
14
+ tags:
15
+ - "v*"
16
+
17
+ jobs:
18
+ build-and-publish:
19
+ name: Build and publish to PyPI
20
+ runs-on: ubuntu-latest
21
+ environment:
22
+ name: pypi
23
+ url: https://pypi.org/p/slimtest
24
+ permissions:
25
+ # Required for PyPI Trusted Publishers OIDC exchange.
26
+ id-token: write
27
+ # Required for softprops/action-gh-release to create the GH Release.
28
+ contents: write
29
+ steps:
30
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
31
+
32
+ - name: Install uv
33
+ uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4
34
+
35
+ - name: Set up Python
36
+ run: uv python install 3.12
37
+
38
+ - name: Sync dependencies
39
+ run: uv sync
40
+
41
+ - name: Verify tag matches pyproject.toml version
42
+ # Guard against publishing when the pushed `v*` tag disagrees
43
+ # with the version hard-coded in pyproject.toml -- otherwise a
44
+ # `v0.2.0` tag would happily publish `0.1.0` (or vice versa),
45
+ # leaving GitHub tag / Release and PyPI pointing at different
46
+ # artefacts.
47
+ run: |
48
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
49
+ PYPROJECT_VERSION=$(uv run python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
50
+ if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
51
+ echo "::error::tag '${GITHUB_REF_NAME}' says $TAG_VERSION but pyproject.toml says $PYPROJECT_VERSION"
52
+ exit 1
53
+ fi
54
+ echo "✓ tag and pyproject.toml agree on version $PYPROJECT_VERSION"
55
+
56
+ - name: Run full quality gate
57
+ run: |
58
+ uv run ruff check .
59
+ uv run ruff format --check .
60
+ uv run mypy src tests
61
+ uv run pytest -q
62
+
63
+ - name: Build sdist and wheel
64
+ run: uv build
65
+
66
+ - name: Publish to PyPI
67
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
68
+
69
+ - name: Create GitHub Release
70
+ uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
71
+ with:
72
+ generate_release_notes: true
73
+ files: dist/*
@@ -0,0 +1,60 @@
1
+ # --- Python ---
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+
8
+ # --- Distribution / build ---
9
+ build/
10
+ dist/
11
+ *.egg
12
+ *.egg-info/
13
+ .eggs/
14
+ wheels/
15
+ share/python-wheels/
16
+ pip-wheel-metadata/
17
+
18
+ # --- Virtual envs ---
19
+ .venv/
20
+ venv/
21
+ env/
22
+ ENV/
23
+
24
+ # --- uv ---
25
+ .uv/
26
+
27
+ # --- Tool caches ---
28
+ .pytest_cache/
29
+ .mypy_cache/
30
+ .ruff_cache/
31
+ .tox/
32
+ .nox/
33
+ .cache/
34
+
35
+ # --- Coverage ---
36
+ .coverage
37
+ .coverage.*
38
+ htmlcov/
39
+ coverage.xml
40
+ *.cover
41
+ .hypothesis/
42
+
43
+ # --- Type-check / linter side files ---
44
+ .dmypy.json
45
+ dmypy.json
46
+
47
+ # --- IDE / editor ---
48
+ .idea/
49
+ .vscode/
50
+ *.swp
51
+ *.swo
52
+ *~
53
+
54
+ # --- OS ---
55
+ .DS_Store
56
+ Thumbs.db
57
+
58
+ # --- slimtest runtime output (when running against a project at the root) ---
59
+ target/
60
+ logs/
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-07-07
8
+
9
+ Initial public release.
10
+
11
+ ### Added
12
+
13
+ - CLI: `slimtest compile` and `slimtest unittest` with `--project-dir` and `--select` options.
14
+ - Factory + trait + override DSL under `tests/slimtest_factories/*.yml`, resolved via deep-merge (`base` → `trait` → `override`).
15
+ - Test blocks under `models[*].meta.slimtest.unit_tests` in dbt model YAML.
16
+ - `given:` accepts the more compact `{upstream: [rows]}` dict form (instead of dbt's list-of-pairs).
17
+ - `model:` field inferred from the enclosing `models[*].name`.
18
+ - Automatic `ref()` vs `source()` disambiguation via `target/manifest.json`.
19
+ - Upstream auto-fill: any upstream that a model depends on but the test omits is stitched in from the same-named factory's `base` row (opt-out via `slimtest.yml`).
20
+ - `parametrize:` block to expand one test into N via `$column` substitution, with case-id naming.
21
+ - `scenarios:` block under `meta.slimtest` for shared `given:` bundles referenced by `scenario: <name>`.
22
+ - Partial `expect:` — only list the columns the test asserts about (dbt ignores unmentioned columns).
23
+ - Source map: failures from `dbt test` are mapped back to `<model.yml>:<line>` in the CLI output.
24
+ - Test name prefix `slimtest__<model>__<name>` guarantees no collision with hand-written dbt `unit_tests`.
25
+ - `--select` accepts a model name, the user-written test name, the prefixed test name, `test_type:unit`, or a comma-separated union.
26
+ - `--verbose` / `-v` flag: INFO-level notices (e.g. auto-injected upstream rows) are hidden by default and printed under verbose.
27
+ - `slimtest.yml` project config with `factories_path`, `generated_yml_path`, `auto_fill_upstreams`.
28
+ - Works with any dbt adapter; validated end-to-end against dbt-duckdb.
29
+
30
+ ### Documentation
31
+
32
+ - Project README with why / install / quick start / concepts / coverage / commands / development.
33
+ - Runnable end-to-end example at `examples/sample_dbt_project/` (DuckDB, 5 test definitions → 8 dbt unit tests).
34
+ - MIT `LICENSE`.
35
+ - Non-affiliation disclaimer and dbt trademark notice in README.
36
+
37
+ [Unreleased]: https://github.com/Hisaaki-Kato/slimtest/compare/v0.1.0...HEAD
38
+ [0.1.0]: https://github.com/Hisaaki-Kato/slimtest/releases/tag/v0.1.0
slimtest-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hisaaki-Kato
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,375 @@
1
+ Metadata-Version: 2.4
2
+ Name: slimtest
3
+ Version: 0.1.0
4
+ Summary: Factory + trait DSL on top of dbt unit tests.
5
+ Project-URL: Homepage, https://github.com/Hisaaki-Kato/slimtest
6
+ Project-URL: Repository, https://github.com/Hisaaki-Kato/slimtest
7
+ Project-URL: Issues, https://github.com/Hisaaki-Kato/slimtest/issues
8
+ Project-URL: Changelog, https://github.com/Hisaaki-Kato/slimtest/blob/main/CHANGELOG.md
9
+ Author: Hisaaki Kato
10
+ Maintainer: Hisaaki Kato
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: dbt,factory,testing,unit-test,yaml
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Database
25
+ Classifier: Topic :: Software Development :: Testing :: Unit
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: pydantic<3,>=2.6
28
+ Requires-Dist: ruamel-yaml<1,>=0.18
29
+ Requires-Dist: typer<1,>=0.12
30
+ Description-Content-Type: text/markdown
31
+
32
+ # slimtest
33
+
34
+ > Factory + trait DSL for dbt™'s `unit_tests`. Removes column-level repetition from your test YAML so the tests stay readable as the upstream schema grows.
35
+
36
+ `slimtest` is **not** a replacement for `dbt test` — it's a thin pre-processor that lets you write `unit_tests` in a far more compact form, then compiles them into the standard YAML dbt already knows how to run.
37
+
38
+ ```
39
+ your factory + trait YAML
40
+ │
41
+ │ $ slimtest compile
42
+ ▼
43
+ target/slimtest/<model>.generated.yml ── standard dbt unit_tests
44
+ │
45
+ │ $ dbt test --select <generated names> (slimtest unittest does this for you)
46
+ ▼
47
+ results (with failures mapped back to your source line)
48
+ ```
49
+
50
+ ## Quick start
51
+ ### Install
52
+
53
+ Recommended — install as a tool managed by `uv`:
54
+
55
+ ```bash
56
+ uv tool install slimtest
57
+ ```
58
+
59
+ Or as a dev dependency of your dbt project:
60
+
61
+ ```bash
62
+ uv add --dev slimtest
63
+ # or, with pip:
64
+ pip install slimtest
65
+ ```
66
+
67
+ (Until the package is published, replace `slimtest` with the path to a local clone: `uv tool install /path/to/slimtest`.)
68
+
69
+ ### Dependencies
70
+
71
+ | Layer | Required |
72
+ | ------------ | -------------------------------------------------------------- |
73
+ | Python | 3.11+ |
74
+ | Runtime libs | `pydantic >=2.6,<3`, `ruamel.yaml >=0.18,<1`, `typer >=0.12,<1` |
75
+ | dbt | `dbt-core >=1.10` + an adapter — provided by *your* dbt project, **not** bundled with `slimtest` |
76
+
77
+ `slimtest` shells out to whatever `dbt` is on `PATH`. Install the dbt adapter that matches your project (`dbt-duckdb`, `dbt-bigquery`, …).
78
+
79
+ ### Run Slimtest
80
+
81
+ Assuming you already have a dbt project:
82
+
83
+ 1. Tell dbt to read the generated YAML — add `target/slimtest` to `model-paths` in `dbt_project.yml`:
84
+
85
+ ```yaml
86
+ model-paths: ["models", "target/slimtest"]
87
+ ```
88
+
89
+ 2. Add a factory at `tests/slimtest_factories/customers.yml`:
90
+
91
+ ```yaml
92
+ factories:
93
+ customers:
94
+ base:
95
+ customer_id: 1
96
+ country: JP
97
+ tier: standard
98
+ traits:
99
+ premium: {tier: premium}
100
+ ```
101
+
102
+ 3. Write the test under `meta.slimtest` in your `model.yml`:
103
+
104
+ ```yaml
105
+ models:
106
+ - name: dim_customers
107
+ meta:
108
+ slimtest:
109
+ unit_tests:
110
+ - name: premium_tier_propagates
111
+ given:
112
+ customers:
113
+ - {factory: customers, trait: premium, override: {customer_id: 42}}
114
+ expect:
115
+ - {customer_id: 42, tier: premium}
116
+ ```
117
+
118
+ 4. Run it from your dbt project root:
119
+
120
+ ```bash
121
+ dbt seed && dbt run # upstreams must exist before unit tests can introspect their schema
122
+ slimtest unittest
123
+ ```
124
+
125
+ Failures look like this — note the source location pointing back to the YAML you wrote:
126
+
127
+ ```
128
+ [slimtest] 0/1 passed; 1 failed, 0 errored, 0 skipped.
129
+ FAIL: premium_tier_propagates (models/dim_customers.yml:5)
130
+ actual differs from expected:
131
+ @@ ,customer_id,tier
132
+ → ,42 ,standard→premium
133
+ ```
134
+
135
+ For a more complete example with four upstreams and four scenarios, see [`examples/sample_dbt_project/`](examples/sample_dbt_project/).
136
+
137
+
138
+ ## Why
139
+
140
+ dbt's built-in `unit_tests` (dbt 1.8+) ask you to spell out every upstream row in full. With wide denormalized tables, or models that join several upstreams, the YAML gets verbose fast — the pain is the **column × row** repetition inside each test, not the number of tests.
141
+
142
+ `slimtest` lets you say "this is what a row of `customers` normally looks like; in this test, override the tier", instead of typing the same 15 columns into every row.
143
+
144
+ ### Tiny taste
145
+
146
+ ```yaml
147
+ # Before: dbt-native unit_tests. One of four upstream blocks shown.
148
+ unit_tests:
149
+ - name: test_full_lifecycle
150
+ model: order_statuses
151
+ given:
152
+ - input: ref('order_events')
153
+ rows:
154
+ - {event_id: 1, order_id: 5001, event_type: placed, occurred_at: '2024-01-01 09:00:00', cancelled_at: null}
155
+ - {event_id: 2, order_id: 5001, event_type: paid, occurred_at: '2024-01-01 09:30:00', cancelled_at: null}
156
+ - {event_id: 3, order_id: 5001, event_type: shipped, occurred_at: '2024-01-02 14:00:00', cancelled_at: null}
157
+ - {event_id: 4, order_id: 5001, event_type: delivered, occurred_at: '2024-01-04 11:00:00', cancelled_at: null}
158
+ - input: ref('orders')
159
+ rows:
160
+ - {order_id: 5001, customer_id: 8001, product_id: 3001, total_amount: 12000, currency: JPY, channel: web, placed_at: '2024-01-01 09:00:00', cancelled_at: null}
161
+ - input: ref('customers')
162
+ rows:
163
+ - {customer_id: 8001, country: JP, tier: standard, signup_source: organic}
164
+ - input: ref('products')
165
+ rows:
166
+ - {product_id: 3001, category: apparel, price: 12000}
167
+ expect:
168
+ rows:
169
+ - {status: pending, valid_from: '2024-01-01 09:00:00', valid_to: '2024-01-01 09:30:00'}
170
+ - {status: processing, valid_from: '2024-01-01 09:30:00', valid_to: '2024-01-02 14:00:00'}
171
+ - {status: shipping, valid_from: '2024-01-02 14:00:00', valid_to: '2024-01-04 11:00:00'}
172
+ - {status: completed, valid_from: '2024-01-04 11:00:00', valid_to: null}
173
+ ```
174
+
175
+ ```yaml
176
+ # After: slimtest. Same assertion, every row carries only what changes.
177
+ models:
178
+ - name: order_statuses
179
+ meta:
180
+ slimtest:
181
+ unit_tests:
182
+ - name: test_full_lifecycle
183
+ given:
184
+ order_events:
185
+ - {factory: order_events, trait: placed, override: {event_id: 1, occurred_at: '2024-01-01 09:00:00'}}
186
+ - {factory: order_events, trait: paid, override: {event_id: 2, occurred_at: '2024-01-01 09:30:00'}}
187
+ - {factory: order_events, trait: shipped, override: {event_id: 3, occurred_at: '2024-01-02 14:00:00'}}
188
+ - {factory: order_events, trait: delivered, override: {event_id: 4, occurred_at: '2024-01-04 11:00:00'}}
189
+ # orders / customers / products are auto-filled from each factory's base row
190
+ expect:
191
+ - {status: pending, valid_from: '2024-01-01 09:00:00', valid_to: '2024-01-01 09:30:00'}
192
+ - {status: processing, valid_from: '2024-01-01 09:30:00', valid_to: '2024-01-02 14:00:00'}
193
+ - {status: shipping, valid_from: '2024-01-02 14:00:00', valid_to: '2024-01-04 11:00:00'}
194
+ - {status: completed, valid_from: '2024-01-04 11:00:00', valid_to: null}
195
+ ```
196
+
197
+ …plus a one-time `tests/slimtest_factories/order_events.yml`:
198
+
199
+ ```yaml
200
+ factories:
201
+ order_events:
202
+ base:
203
+ event_id: 10000
204
+ order_id: 5001
205
+ occurred_at: '2024-01-01 09:00:00'
206
+ cancelled_at: null
207
+ traits:
208
+ placed: {event_type: placed}
209
+ paid: {event_type: paid}
210
+ shipped: {event_type: shipped}
211
+ delivered: {event_type: delivered}
212
+ cancelled: {event_type: cancelled}
213
+ returned: {event_type: returned}
214
+ ```
215
+
216
+ The full runnable version of this lives in [`examples/sample_dbt_project/`](examples/sample_dbt_project/).
217
+
218
+ ## Core concepts
219
+
220
+ ### Factory
221
+
222
+ One factory represents "one row of upstream model X". By convention the factory **name matches the upstream name** so `slimtest` can auto-fill missing upstreams (see below). Factories live in `tests/slimtest_factories/*.yml`.
223
+
224
+ ### `base` and `traits`
225
+
226
+ - **`base`**: the "default row" — common defaults shared by every variant.
227
+ - **`traits`**: named diffs that overlay `base` to represent state / variant (`premium` user, `placed` event, `us` customer, …).
228
+
229
+ In a test, you reference a row as `{factory: <name>, trait: <name>, override: {...}}`. Layers are **deep-merged** in this order: `base → trait → override`.
230
+
231
+ ### `override`
232
+
233
+ Per-row, per-test diff applied on top of `base + trait`. This is where test-specific IDs and per-case values go.
234
+
235
+ ### Upstream auto-fill
236
+
237
+ If a test's `given:` omits an upstream and a factory with the same name exists, `slimtest` injects one `base` row of that factory automatically. A happy-path test that only cares about events doesn't have to repeat the boilerplate of orders / customers / products / etc.
238
+
239
+ ### Scenario
240
+
241
+ A reusable bundle of `given:` rows declared under `meta.slimtest.scenarios.<name>`. Tests reference it with `scenario: <name>` and `slimtest` merges its `given:` into the test's (test wins per upstream key). Used to factor out "all the supporting rows for one order shape" so tests only carry their differing bits.
242
+
243
+ ```yaml
244
+ meta:
245
+ slimtest:
246
+ scenarios:
247
+ premium_jp_books:
248
+ given:
249
+ orders: [{factory: orders, override: {order_id: 5002}}]
250
+ customers: [{factory: customers, trait: premium, override: {customer_id: 8002}}]
251
+ products: [{factory: products, trait: books}]
252
+ unit_tests:
253
+ - name: cancellation_after_payment
254
+ scenario: premium_jp_books # orders/customers/products provided by the scenario
255
+ given:
256
+ order_events: [...]
257
+ expect: [...]
258
+ ```
259
+
260
+ ### Parametrize
261
+
262
+ A table-driven sugar that expands one test into N by substituting `$<column>` placeholders. Reduces "for each event type, the status maps to X" style tests from N copies to 1 block.
263
+
264
+ ```yaml
265
+ unit_tests:
266
+ - name: event_maps_to_status
267
+ parametrize:
268
+ columns: [event_trait, expected_status]
269
+ cases:
270
+ - [placed, pending]
271
+ - [paid, processing]
272
+ - [shipped, shipping]
273
+ - [delivered, completed]
274
+ given:
275
+ order_events:
276
+ - {factory: order_events, trait: $event_trait, override: {event_id: 1}}
277
+ expect:
278
+ - {status: $expected_status}
279
+ ```
280
+
281
+ Each case becomes a separate dbt unit test named `slimtest__<model>__event_maps_to_status__<case_id>`. `case_id` defaults to the first column's value (so the example yields `__placed`, `__paid`, `__shipped`, `__delivered`); set an explicit `id:` column to override.
282
+
283
+ ### Source map
284
+
285
+ Failures from `dbt test` are mapped back to the source `model.yml` line where you wrote the test, so you don't have to grep through generated YAML.
286
+
287
+ ### Test name prefix
288
+
289
+ Every generated test is named `slimtest__<model>__<original_name>`, guaranteeing no collision with hand-written `unit_tests` in the same project.
290
+
291
+ ## Coverage
292
+
293
+ `slimtest` is a wrapper around dbt's **`unit_tests`** feature. It does not replace `dbt test`; it generates the YAML that `dbt test` consumes.
294
+
295
+ | Test kind | Handled by | `slimtest` covers? |
296
+ | ---------------------------------------------- | ------------- | --------------------------------- |
297
+ | Unit tests (dbt 1.8+ `unit_tests:`) | `slimtest` | ✅ |
298
+ | Existing top-level `unit_tests:` (dbt-native) | dbt | passthrough (untouched) |
299
+ | Data tests (`unique`, `not_null`, etc.) | dbt | ❌ — run `dbt test` directly |
300
+ | Singular tests (`tests/*.sql`) | dbt | ❌ — run `dbt test` directly |
301
+ | Snapshot / seed / macro tests | (none) | ❌ — dbt itself does not support these |
302
+
303
+ ### Currently supported
304
+
305
+ - factory + trait + override with deep-merge semantics
306
+ - `given:` dict form (`{upstream: [rows]}`) instead of dbt's list-of-pairs form
307
+ - `model:` field inferred from the enclosing `models[*].name`
308
+ - `scenario:` — shared `given:` bundle declared under `meta.slimtest.scenarios`
309
+ - `parametrize:` — `$column` substitution that expands one test into N
310
+ - `ref()` vs `source()` automatic disambiguation via `manifest.json`
311
+ - upstream auto-fill via same-named factories
312
+ - partial `expect:` — only list the columns you care about; dbt ignores unmentioned columns
313
+ - source map: failure → `models/foo.yml:42`
314
+ - `--select` accepts a model name, the user-written test name, the prefixed test name, `test_type:unit`, or a comma-separated union
315
+ - works with any dbt adapter (validated on DuckDB; BigQuery and Postgres are mechanically the same)
316
+ - `slimtest.yml` config file with `factories_path`, `generated_yml_path`, `auto_fill_upstreams`
317
+
318
+ ### Not yet supported
319
+
320
+ - `slimtest validate` subcommand (factory / model.yml lint)
321
+ - `slimtest factories list`
322
+ - factory composition (one row built from multiple factories)
323
+ - factory inheritance (one factory extends another)
324
+ - stale generated-yml detection (no source-hash invariant yet)
325
+ - ephemeral upstreams that require `format: sql` fixtures
326
+ - watch mode / IDE integration / LSP completion
327
+ - dbt's `dbtRunner` in-process API (we shell out to `dbt`, so each `unittest` pays the dbt cold-start cost)
328
+
329
+ ## Commands
330
+
331
+ ```
332
+ slimtest compile [--project-dir DIR] [--select SEL]
333
+ Expand slimtest extension YAML into target/slimtest/*.generated.yml.
334
+ Does NOT invoke dbt. Useful for debugging the expansion.
335
+
336
+ slimtest unittest [--project-dir DIR] [--select SEL]
337
+ 1. dbt parse (refresh target/manifest.json)
338
+ 2. slimtest compile
339
+ 3. dbt test --select <generated test names>
340
+ 4. Map failures back to source location.
341
+
342
+ slimtest --version
343
+ slimtest --help
344
+ ```
345
+
346
+ The `--select` selector accepts:
347
+
348
+ - a model name (`orders`)
349
+ - a user-written test name (`premium_tier_propagates`)
350
+ - a prefixed name (`slimtest__orders__premium_tier_propagates`)
351
+ - `test_type:unit` (no-op; everything `slimtest` emits is a unit test)
352
+ - comma-separated tokens (`orders,customers`)
353
+
354
+ ## Development
355
+
356
+ ```bash
357
+ uv sync # install runtime + dev deps into .venv
358
+ uv run pytest # run the test suite
359
+ uv run ruff check . # lint
360
+ uv run ruff format . # format
361
+ uv run mypy src tests # type check
362
+ ```
363
+
364
+ Conventions:
365
+
366
+ - `ruff` with line-length 88 (E501 is a hard error)
367
+ - `mypy --strict` on `src/`; tests are checked with `disallow_untyped_defs` off
368
+ - `pydantic` v2 models for every user-facing schema; runtime data is plain `dict[str, Any]`
369
+ - dbt is mocked in pytest so the test suite needs no warehouse and no installed adapter
370
+
371
+ ## License
372
+
373
+ MIT. See [`LICENSE`](LICENSE).
374
+
375
+ `slimtest` is an independent open-source project and is not affiliated with dbt Labs. `dbt` is a trademark of dbt Labs, LLC.