Test file placement affects discoverability and maintainability. Tests placed in a separate directory are often skipped during reviews or become disconnected from the models they cover.
Co-locate Tests with Models
The recommended pattern: place unit tests in a _unit_tests.yml file inside the same directory as the models they test.
models/├── base/│ ├── crm/│ │ ├── base__crm__customers.sql│ │ └── _crm__models.yml│ └── shopify/│ ├── base__shopify__orders.sql│ └── _shopify__models.yml├── intermediate/│ ├── int__customers_enriched.sql│ └── _intermediate__models.yml├── marts/│ ├── core/│ │ ├── mrt__core__customers.sql│ │ ├── _core__models.yml│ │ └── _unit_tests.yml # Unit tests for core marts│ └── finance/│ ├── mrt__finance__orders.sql│ ├── _finance__models.yml│ └── _unit_tests.yml # Unit tests for finance martstests/└── fixtures/ ├── customer_fixture.csv └── large_order_dataset.csvThis follows the same co-location principle as model YAML files. When you open a model’s directory, you see its SQL, its schema definition, and its unit tests all together. No hunting through separate test directories to find what tests exist for a model.
The _unit_tests.yml naming convention mirrors the existing _models.yml pattern — the underscore prefix sorts it to the top of the directory listing, making it immediately visible.
Why Not Put Unit Tests in the Models YAML?
You technically can put unit_tests: blocks in the same YAML file as your model definitions. For projects with a handful of unit tests, this works fine. But as your test suite grows, the model YAML file balloons. A file that mixes model configuration, column documentation, generic tests, and unit tests becomes hard to navigate.
Separating unit tests into their own file keeps each YAML file focused:
_core__models.yml— model config, column descriptions, generic data tests_unit_tests.yml— unit tests with their mocked inputs and expected outputs
This separation also reduces merge conflicts. Unit tests change frequently (as you add edge cases or update fixtures), while model configurations change less often. Separate files mean separate change histories.
External Fixtures
For test data that’s reused across multiple tests or is too large for inline YAML, use external CSV fixture files in tests/fixtures/:
tests/└── fixtures/ ├── customer_fixture.csv └── large_order_dataset.csvReference them in your unit tests with the fixture key:
unit_tests: - name: test_large_order_processing model: mrt__finance__orders given: - input: ref('base__shopify__orders') format: csv fixture: large_order_dataset expect: rows: - {order_id: 1, total: 5000.00}External fixtures make sense when:
- The same dataset is needed by multiple tests across different directories
- The test data has many columns and would make the YAML file unwieldy
- You want non-engineers (like analysts or domain experts) to review or contribute test data in a familiar spreadsheet-like format
The trade-off is indirection. Someone reading the unit test YAML has to open a separate file to see the actual input data. For most tests, inline dict format is preferable because it keeps everything visible in one place.
Naming Conventions
Consistent test names make failures instantly understandable in CI logs. Adopt a pattern:
unit_tests: # Pattern: test_<model_short_name>_<scenario> - name: test_mrt_core_customers_email_validation - name: test_mrt_core_customers_null_handling - name: test_mrt_finance_orders_discount_calculation - name: test_mrt_finance_orders_zero_quantity_edge_caseThe model short name avoids the full double-underscore convention (which would make names excessively long) but retains enough context to identify the model. The scenario suffix describes what the test verifies.
When a test fails in CI, you see:
FAIL test_mrt_finance_orders_discount_calculationThat tells you immediately: it’s the finance orders mart, the discount calculation is broken. No need to look up which model the test belongs to.
Some teams prefer shorter names like test_discount_calc and rely on the file location for context. This works in small projects but breaks down when you have hundreds of tests across dozens of models — test_null_handling tells you nothing if you can’t see which file it came from.
Where Unit Tests Concentrate
In practice, unit tests cluster in the mart layer. Testing strategy by layer explains why: marts contain the complex business logic — customer segmentation, revenue calculations, metric derivations — that warrants the overhead of mocked-input testing.
Base models rarely need unit tests (their logic is mechanical: rename, cast, filter). Intermediate models occasionally need them (for complex joins or aggregation logic). Marts are where the business rules live, and business rules are where bugs hide.
This means your _unit_tests.yml files will mostly appear in models/marts/*/ directories. That’s expected. If you find yourself writing many unit tests for base models, you might have business logic in the wrong layer.
Scaling the Test Suite
As your project grows, a few organizational practices keep things manageable:
Tag tests by business domain. Add config: {tags: ["finance"]} or config: {tags: ["marketing"]} so teams can run only their tests during development.
Group related tests. If a single model has 8+ unit tests, consider whether some scenarios could be combined or whether the model itself is doing too much. A model that needs 8 unit tests might benefit from being split into smaller, more focused models.
Review test files during code review. When a PR modifies a model’s SQL, check whether _unit_tests.yml was also updated. New edge cases in the logic should come with new test scenarios.