Unit testing framework for Power BI measures
Project description
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/*
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pbi_unit_test-0.1.1.tar.gz.
File metadata
- Download URL: pbi_unit_test-0.1.1.tar.gz
- Upload date:
- Size: 393.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ff5e57e8fb8f109148d05e072bdabfe72a1bb8053c9f7c92623e24c7fa53d7e
|
|
| MD5 |
1df1239295175d7a73ba78f4d5b9fde0
|
|
| BLAKE2b-256 |
4a640d645d0380f818b44f875b4a8701212e2b79cf6275a656353e24a7379118
|
File details
Details for the file pbi_unit_test-0.1.1-py3-none-any.whl.
File metadata
- Download URL: pbi_unit_test-0.1.1-py3-none-any.whl
- Upload date:
- Size: 390.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f882e64c803a8136d1681bebad60c1aa15e4a80ac870c5b8345fbfe074be5c68
|
|
| MD5 |
e425e26f5b63949112ee68d93166482a
|
|
| BLAKE2b-256 |
049712dda6242493c9034ae87be4707b49952be63c1609218b14a4f4a5248a52
|