Power BI Unit Tests
Run DAX unit tests against a locally open Power BI Desktop model — no Power BI Service required.
How it works
The tool connects to the Analysis Services instance that Power BI Desktop runs locally (msmdsrv.exe) via ADOMD.NET. It reads a test-case table from your model, evaluates each measure with its filters, and compares the result against the expected value.
Prerequisites
- Windows (Power BI Desktop is Windows-only)
- Power BI Desktop open with a model loaded
- Python 3.9+
- .NET Framework 4.7.2 (usually already present on Windows)
Installation
From pip
pip install pbi-unit-test
The ADOMD.NET client library is bundled with the package, so no additional setup is required.
Custom ADOMD path (optional)
If you need to use a different version of the ADOMD client library, set the ADOMD_PATH environment variable:
set ADOMD_PATH=C:\path\to\your\adomd\net472
Test table schema
Create a calculated table (or a regular table) in your Power BI model with these columns:
| Column | Type | Description |
|---|---|---|
[Measure] |
Text | Name of the measure to test |
[Filters] |
Text | DAX filter context passed to CALCULATE, e.g. Store[Country] = "US" |
[WithRounding?] |
Boolean | If TRUE, rounds the actual result to 0 decimal places before comparing |
[Expected Value] |
Decimal | The value the measure should produce |
Example calculated table:
DAX
unit_tests_2026_03_22 = DATATABLE(
"Id", INTEGER,
"Measure", STRING,
"Filters", STRING,
"WithRounding?", BOOLEAN,
"Expected Value", DOUBLE,
{
{ 1 ,"Total Sales", "Store[Country] = ""US""", FALSE, 1234567.89 },
{ 2 ,"AOV", "Store[Country] = ""US""", TRUE, 42 }
}
)
Power Query
let
Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WMlTSUQrJL0nMUQhOzEktBvKCS/KLUqOd80vzSooqYxVsFWKUQoNjlHQUnIEK8lISi6IjUxOLYm2NDIxMgcrdEnOKU4G0oZGxiamZuZ6FpVKsTrSSEVDI0T+MePMUgDJQI0OKSkEmmhgpxcYCAA==", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [Id = _t, Measure = _t, Filters = _t, #"WithRounding?" = _t, #"Expected Value" = _t]),
#"Changed Type" = Table.TransformColumnTypes(Source,{{"Id", Int64.Type}, {"Measure", type text}, {"Filters", type text}, {"WithRounding?", type logical}, {"Expected Value", type number}})
in
#"Changed Type"
Usage
Command line
python -m pbi_unit_test <table_pattern> [options]
Arguments
| Argument | Description |
|---|---|
table_pattern |
Table name or glob pattern (e.g. unit_test_*) to run all matching tables |
--output-measure [FILE] |
Generate a live DAX measure. Omit FILE to print to stdout; provide a path to write to a file |
--csv FILE |
CSV file to append results to (default: test_results.csv) |
--force |
Re-run tables already recorded in the CSV, replacing their rows |
The process exits with code 0 if all tests pass, 1 if any test fails or no tables matched the pattern — suitable for CI pipelines.
Use case 1 — Run a single test table
python -m pbi_unit_test unit_tests
Output:
Connected to: MyReport.pbix
=== unit_tests ===
[PASS ✓] Total Sales | filters: Store[Country] = "US"
[FAIL ✗] AOV | filters: Store[Country] = "US"
1/2 tests passed.
Failed tests:
AOV: expected 42, got 38.0
Use case 2 — Run all tables matching a glob pattern
Useful when you have one test table per sprint or date, e.g. unit_tests_2026_03_*.
python -m pbi_unit_test "unit_tests_*"
Each matching table is run in turn and results are appended to the CSV.
Use case 3 — Generate a live DAX measure (print to stdout)
Generates a Test Status measure you can paste directly into Power BI Desktop so you can evaluate tests interactively without re-running Python.
python -m pbi_unit_test unit_tests --output-measure
The generated DAX is printed between two separator lines so it is easy to copy.
Use case 4 — Generate a live DAX measure and save to a file
python -m pbi_unit_test unit_tests --output-measure status_measure.dax
The file can be committed to source control alongside your test table.
Use case 5 — Track results in a custom CSV file
By default results are appended to test_results.csv in the current directory. Use --csv to choose a different path.
python -m pbi_unit_test unit_tests --csv reports/q1_results.csv
CSV columns: table_name, id, measure, filters, expected, actual, passed, error, dax_query, run_at.
Use case 6 — Re-run a table already recorded (--force)
By default the tool skips any table whose name already appears in the CSV, preventing accidental double-runs. Use --force to overwrite the existing rows.
python -m pbi_unit_test unit_tests --force
Use case 7 — CI pipeline
The exit code makes it straightforward to fail a build when tests regress.
python -m pbi_unit_test unit_tests || exit 1
Or in a GitHub Actions step:
- name: Run Power BI unit tests
run: python -m pbi_unit_test unit_tests
Use case 8 — Combined: run all tables, generate measure, log to custom CSV
python -m pbi_unit_test "unit_tests_*" --output-measure status_measure.dax --csv results/history.csv --force
Python API
from pbi_unit_test import run_unit_tests, list_dax_measures
summary = run_unit_tests("unit_tests")
summary.print_report()
# Access individual results
for result in summary.results:
print(result.measure, result.passed)
# Explore measures in the model
print(list_dax_measures())
Project structure
pbi_unit_test/
├── __init__.py # public API
├── __main__.py # CLI entry point
├── connection.py # port detection, ADOMD connection, DAX execution
├── measures.py # measure listing and expression retrieval
├── runner.py # test runner, TestResult, TestSummary
└── adom_client/ # bundled ADOMD.NET client library
└── Microsoft.AnalysisServices.AdomdClient.dll
examples/ # example files
└── status_measure.dax # example status measure DAX
Publishing to PyPI
To publish a new version:
pip install build twine
python -m build
twine upload dist/*
Metadata
Release files for pbi-unit-test 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pbi_unit_test-0.1.1.tar.gz | 393.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pbi_unit_test-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 783.9 kB
Release files / pbi_unit_test-0.1.1.tar.gz
| Download URL | pbi_unit_test-0.1.1.tar.gz |
|---|---|
| Size | 393.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5ff5e57e8fb8f109148d05e072bdabfe72a1bb8053c9f7c92623e24c7fa53d7e
|
|
BLAKE2b-256 checksum How to use checksums |
4a640d645d0380f818b44f875b4a8701212e2b79cf6275a656353e24a7379118
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.3
|
Release files / pbi_unit_test-0.1.1-py3-none-any.whl
| Download URL | pbi_unit_test-0.1.1-py3-none-any.whl |
|---|---|
| Size | 390.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f882e64c803a8136d1681bebad60c1aa15e4a80ac870c5b8345fbfe074be5c68
|
|
BLAKE2b-256 checksum How to use checksums |
049712dda6242493c9034ae87be4707b49952be63c1609218b14a4f4a5248a52
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.3
|