Skip to main content

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)

Source distribution for pbi-unit-test 0.1.1
File Size Uploaded
pbi_unit_test-0.1.1.tar.gz 393.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pbi-unit-test 0.1.1
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page