Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pbi_unit_test-0.1.1.tar.gz (393.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pbi_unit_test-0.1.1-py3-none-any.whl (390.6 kB view details)

Uploaded Python 3

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

Hashes for pbi_unit_test-0.1.1.tar.gz
Algorithm Hash digest
SHA256 5ff5e57e8fb8f109148d05e072bdabfe72a1bb8053c9f7c92623e24c7fa53d7e
MD5 1df1239295175d7a73ba78f4d5b9fde0
BLAKE2b-256 4a640d645d0380f818b44f875b4a8701212e2b79cf6275a656353e24a7379118

See more details on using hashes here.

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

Hashes for pbi_unit_test-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f882e64c803a8136d1681bebad60c1aa15e4a80ac870c5b8345fbfe074be5c68
MD5 e425e26f5b63949112ee68d93166482a
BLAKE2b-256 049712dda6242493c9034ae87be4707b49952be63c1609218b14a4f4a5248a52

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page