#015 Testowanie miar DAX: jak przestałam klikać po slicerach i zaczęłam pracować mądrzej

Znasz to uczucie? Oddajesz raport, wszystko wygląda świetnie, a dwa tygodnie później przychodzi wiadomość: „Paulino, a czemu jak kliknę tę jedną kategorię, to wychodzi dziwna liczba?”. Klikasz u siebie — faktycznie, dziwna. Miara działa idealnie w dziesięciu miejscach i sypie się w jedenastym, którego nikt nie sprawdził.

Właśnie dlatego testowanie miar DAX to nie fanaberia dla perfekcjonistów, tylko czysta oszczędność czasu.

W tym wpisie idziemy dwoma krokami. Najpierw pokażę Ci na banalnie prostej mierze, na czym polegają cztery typy testów — bez AI, bez instalowania czegokolwiek, do odtworzenia u siebie w pięć minut. A potem to samo na prawdziwej, zakręconej mierze z mojego modelu, gdzie do pomocy wzięłam już AI podłączone bezpośrednio do danych. Work smart, not hard.


Po co w ogóle testować miary?

Miara jest poprawna tylko wtedy, gdy zwraca dobry wynik w każdym kontekście filtra — nie w tym jednym, który akurat sprawdziłaś.

To kluczowa myśl. Bo my zwykle testujemy tak: wrzucamy miarę na kartę KPI, patrzymy na liczbę, mówimy „zgadza się” i idziemy dalej. Ale ta liczba to jeden punkt. Użytkownik za chwilę:

  • kliknie slicer z kategorią,
  • zaznaczy dwa produkty naraz zamiast jednego,
  • wejdzie w drill-down,
  • wybierze miesiąc, w którym tego produktu w ogóle nie było w sprzedaży.

Każda z tych sytuacji to inny kontekst filtra, w którym Twoja miara musi się zachować sensownie.

Ręczne wyklikiwanie ma trzy wady: jest nudne (więc robione pobieżnie), jest nie do powtórzenia po każdej zmianie, i łatwo zapomnieć o kombinacji, która kiedyś już się wysypała.

Porządne testowanie miar DAX rozwiązuje dwie ostatnie: raz napisany test uruchamiasz po każdej zmianie, a raz złapany błąd nigdy nie wraca niezauważony. AI bierze na siebie tę pierwszą — szukanie w danych konkretnych przypadków.


Testowanie miar DAX: cztery typy testów

Kliknij przycisk i pobierz materiały z modelem, aby przećwiczyć kwestie z wpisu i opanować testowanie miar DAX za pomocą AI w praktyce.

Zanim usiądziesz do pisania testów, musisz wiedzieć, czego szukasz. Miara może się pomylić tylko na dwa sposoby — i każdy typ testu pilnuje innej komórki tej tabelki:

Miara policzyłaMiara nie policzyła
Powinna była policzyć✅ pilnuje test pozytywny❌ false negative → prevent FN
Nie powinna była❌ false positive → prevent FP✅ pilnuje test negatywny

I teraz clou, które gubi większość ludzi:

  • Pozytywny i prevent-false-negative oba oczekują liczby. Różnica jest w trudności przypadku: pozytywny to sytuacja oczywista, prevent-FN to sytuacja podchwytliwa, w której sensownie wyglądająca zła wersja miary zwróciłaby 0.
  • Negatywny i prevent-false-positive oba oczekują zera. Też różnią się trudnością: negatywny to „nie ma danych, więc nie ma wyniku”, prevent-FP to „dane są, wyglądają kusząco, ale nie należą do tej selekcji”.

Czyli: testy 1 i 2 sprawdzają, czy miara w ogóle działa. Testy 3 i 4 sprawdzają, czy nie ściąga danych spoza tego, co użytkownik wybrał — i to one łapią większość prawdziwych błędów.

Brzmi abstrakcyjnie? To zobaczmy na liczbach.


Testowanie miar DAX na najprostszym możliwym przykładzie

Weźmy miarę, którą każdy rozumie od razu: sprzedaż produktów Premium.

Sprzedaż Premium =
CALCULATE ( [Total Revenue], KEEPFILTERS ( 'Dim_Product'[Tier] = "Premium" ) )

Po polsku: pokaż przychód, ale tylko z tego, co jest Premium — w ramach tego, co użytkownik już wybrał.

Do porównania weźmiemy dwie realistycznie złe wersje. Nie wymyślone potworki, tylko takie, które pisze się odruchowo:

-- BŁĄD A: bez KEEPFILTERS. Filtr Tier NADPISUJE wybór użytkownika, zamiast się z nim przeciąć.
CALCULATE ( [Total Revenue], 'Dim_Product'[Tier] = "Premium" )

-- BŁĄD B: popularny skrót. Działa tylko wtedy, gdy w kontekście jest dokładnie jeden Tier.
IF ( SELECTEDVALUE ( 'Dim_Product'[Tier] ) = "Premium", [Total Revenue], 0 )

Uruchomiłam wszystkie trzy wersje na swoich danych, w czterech kontekstach filtra. Wyniki:

TestCo klika użytkownikPoprawnieBŁĄD ABŁĄD B
1. Pozytywnyprodukt Elite Laptop (Premium)1 714 220,521 714 220,521 714 220,52
2. NegatywnyWorkstation PC (Premium, 0 transakcji)000
3. Prevent FPslicer Tier = Standard0🔴 7 357 975,300
4. Prevent FNkategoria Computers (mix Premium i Standard)1 714 220,521 714 220,52🔴 0

Popatrz na kolumny z błędami. Testy 1 i 2 przepuszczają obie zepsute wersje. Gdybyś sprawdziła tylko je — a tak właśnie testuje większość z nas — oddałabyś raport z czystym sumieniem.

1. Pozytywny — „czy w ogóle liczy?”

Wybieram produkt, który jest Premium i ma sprzedaż. Najprostsza możliwa sytuacja, oczekuję konkretnej liczby.

Ten test wykrywa tylko katastrofę: literówkę w nazwie kolumny, złą relację, pomylony SUM. Potrzebny, ale sam w sobie niewiele wart.

2. Negatywny — „czy nie zmyśla, gdy nie ma z czego?”

Wybieram Workstation PC — jest Premium, ale nie ma ani jednej transakcji. Oczekuję 0.

Wykrywa miarę, która gubi filtr produktu i podstawia cudzą sprzedaż.

3. Prevent false positive — „czy nie ściąga danych spoza wyboru?”

Tu jest cały trik. Użytkownik przesuwa slicer na Tier = Standard. Poprawna odpowiedź to 0 — wśród produktów Standard nie ma nic Premium. Sprzeczność, pustka.

Ale BŁĄD A zwraca 7 357 975,30 zł. Dlaczego? Bo CALCULATE bez KEEPFILTERS nadpisuje filtr na kolumnie Tier:

Wybór użytkownika:   Tier = Standard
Filtr z CALCULATE:   Tier = Premium
Wynik:               Tier = Premium      ← wybór użytkownika wyparował

Miara policzyła sprzedaż wszystkich produktów Premium w całym modelu. Użytkownik patrzy na slicer ustawiony na „Standard” i widzi obok siedem milionów sprzedaży Premium.

To jest dokładnie ten moment — „czemu jak kliknę, to wygląda dziwnie”. I zwróć uwagę: liczba nie jest absurdalna. Jest duża, prawdopodobna, ładnie sformatowana. Nikt jej nie zakwestionuje.

Z KEEPFILTERS filtry by się przecięły: Standard ∩ Premium = zbiór pusty → 0.

4. Prevent false negative — „czy nie chowa danych, które powinny być?”

Użytkownik wybiera kategorię Computers, w której są i produkty Premium, i Standard. Poprawna odpowiedź to 1 714 220,52 — część Premium tej kategorii.

BŁĄD B zwraca 0. Bo SELECTEDVALUE zwraca wartość tylko wtedy, gdy w kontekście jest dokładnie jedna wartość kolumny. W kategorii Computers są dwa Tiery, więc SELECTEDVALUE daje pusto, warunek nie przechodzi i miara mówi „zero”.

To najpodstępniejszy z czterech błędów, bo zero nie wygląda na błąd. Wygląda na informację: „tutaj nic nie ma”. Nikt nie zgłasza reklamacji na zero.

Jedna rzecz, która mnie samą zaskoczyła

Pierwszą wersję testu nr 3 zrobiłam wyborem produktu Standard Laptop zamiast slicerem na Tier. I błąd się nie ujawnił — wyszło 0 w obu wersjach.

Dlaczego? Bo nadpisanie dotyczy tylko tej samej kolumny. Filtr był na Product_Name, a CALCULATE nadpisywał Tier — nie kolidowały. Musiałam celowo przesunąć test na slicer Tier, żeby błąd wyszedł.

Zapamiętaj to, bo to najważniejsza lekcja z całego wpisu: dobór przypadku testowego jest ważniejszy niż kod testu. Zły przypadek daje PASS przy zepsutej mierze.


A teraz prawdziwa miara — i tu wchodzi AI

Przy „Sprzedaży Premium” przypadki testowe wymyśliłam w głowie w dwie minuty. Problem w tym, że prawdziwe miary tak nie wyglądają.

Wzięłam ze swojego modelu rentowności miarę [Opportunity Gap (Dynamic)]. Liczy, ile pieniędzy zostawiamy na stole na danym produkcie: porównuje faktyczną marżę z potencjałem, jaki dałby target marży lidera kategorii.

Nie musisz rozumieć tej logiki w szczegółach. Wystarczy, że zobaczysz trzy miejsca, w których może pęknąć:

  1. HASONEVALUE('Dim_Product'[Product_Name]) — miara ma sens tylko dla jednego wybranego produktu. Przy dwóch musi zwrócić 0.
  2. Product_Name <> _CurrentProduct — lider kategorii nie może być sam sobie benchmarkiem.
  3. ALL('Dim_Product') — lidera szukamy w całej kategorii, nie tylko wśród tego, co akurat widać na ekranie.

Każde z tych miejsc to osobne ryzyko regresji. Różnica wobec poprzedniego przykładu jest taka, że przypadków testowych nie wymyślę już w głowie — trzeba je znaleźć w danych.

I tu testowanie miar DAX przestaje być kwestią dobrej woli, a zaczyna być kwestią czasu. Przejrzenie dwudziestu wierszy wymiaru i zestawienie ich z faktami to pół godziny klikania. To jest dokładnie ta nudna, powtarzalna robota, którą warto oddać AI.


Krok 1: podłączam AI bezpośrednio do modelu

MCP (Model Context Protocol) to standard, dzięki któremu AI może rozmawiać z zewnętrznym narzędziem. Dla Power BI istnieje Power BI Modeling MCP Server — lokalny serwer, który pozwala modelowi językowemu czytać metadane modelu, podglądać definicje miar i wykonywać zapytania DAX na żywych danych.

To ostatnie jest kluczowe. Bez dostępu do żywych danych testowanie miar DAX z pomocą AI nie ma sensu — dostajesz kod, który wygląda mądrze i nie sprawdza niczego, bo model zgaduje na podstawie nazw kolumn.

Otwarty Power BI Desktop wystawia lokalną instancję Analysis Services na losowym porcie. Claude ją znalazł i się podłączył:

Instancja: PBIDesktop, port 63713
Plik: Strategic_Profitability_Command_Center
Compatibility level: 1702
Tabele: 11 | Miary: 60 | Relacje: 3

Dlaczego compatibility level 1702 ma znaczenie? Bo dopiero od tego poziomu działają funkcje użytkownika w DAX (UDF) — słowo kluczowe FUNCTION w bloku DEFINE. Bez nich zestaw testów byłby jedną wielką kopiuj-wklejką. Z nimi każdy test to osobna, nazwana funkcja.


Krok 2: prompt, który faktycznie zadziałał

To najważniejsza część wpisu. Generyczne „przetestuj tę miarę” nie da nic sensownego — AI wybierze losowe przypadki, które nie dotykają logiki filtrów.

Dobry prompt do testowania miar DAX musi zawierać cztery rzeczy:

  1. regułę biznesową własnymi słowami,
  2. wymiary, które mają się zmieniać (u mnie: 1 vs 2 wybrane produkty),
  3. cztery typy testów wymienione wprost,
  4. kontrakt wyjściowy — jak ma wyglądać wynik.

Mój prompt wyglądał tak:

Podłącz się do otwartego modelu Power BI i przeanalizuj miarę [Opportunity Gap (Dynamic)]. Znajdź w danych transakcyjnych 4 przypadki testowe (pozytywny, negatywny, prevent false positive, prevent false negative) dla 1 i 2 wybranych Product_Name, a następnie wygeneruj kompletne zapytanie DAX z funkcjami UDF do uruchomienia w DAX Query View.

I to wystarczyło. Claude sam przeczytał definicję miary, rozłożył ją na części, przeszukał Fact_Sales i Dim_Product, a potem dobrał konkretne produkty pod każdy typ testu.


Krok 3: co AI znalazło w danych

To są przypadki, których sama szukałabym pół godziny.

Prevent false positive → Security Suite Total. Ten produkt sam jest liderem targetu w swojej kategorii (65%). Po wykluczeniu samego siebie liderem zostaje Antivirus Essential z 25% — potencjał spada poniżej faktycznej marży, więc poprawny wynik to 0.

Gdyby ktoś przy refaktorze usunął warunek wykluczający bieżący produkt, miara pokazałaby 589 854,55 zł nieistniejącego potencjału. Test pozytywny by tego nie złapał.

Prevent false negative → Standard Laptop. Liderem targetu w kategorii Computers jest Workstation PC z targetem 40% — produkt, który nie ma ani jednej transakcji i jest odfiltrowany przez wybór produktu.

Tylko ALL('Dim_Product') pozwala go znaleźć. Bez tego miara zwróciłaby 0 zamiast poprawnych 76 217,97 zł. Cicha, niewidoczna strata.

TestSelekcjaOczekiwane
A1 pozytywnyBasic Membrane Keyboard309 934,21
A2 negatywnyWorkstation PC (0 transakcji)0
A3 prevent FPSecurity Suite Total0
A4 prevent FNStandard Laptop76 217,97

Dorzuciłam jeszcze czwórkę testów dla dwóch zaznaczonych produktów. Tu warto być szczerym: ta miara z definicji zwraca 0 przy więcej niż jednym produkcie, więc trzy z tych testów sprowadzają się do „czy zabezpieczenie HASONEVALUE zadziałało”. Naprawdę nowy jest jeden: w wierszach macierzy przy filtrze strony z dwoma produktami suma musi wynieść 426 222,88 zł, a na poziomie sumy — zero. Przy mierze działającej na poziomie totali byłoby tu więcej do złapania.


Krok 4: kod z funkcjami UDF

Zamiast ośmiu osobnych zapytań — jedna wspólna funkcja wynikowa i osiem funkcji testowych. Tak wygląda jedna z nich:

FUNCTION Test_OppGap_1P_NoFalsePositive = () =>
    VAR Actual =
        COALESCE (
            CALCULATE (
                [Opportunity Gap (Dynamic)],
                TREATAS ( { "Security Suite Total" }, 'Dim_Product'[Product_Name] )
            ), 0
        )
    RETURN
        TestResult (
            "A3. 1 produkt - prevent false positive",
            Actual, 0,
            "Po wykluczeniu siebie liderem jest Antivirus Essential 25%, wynik 0.",
            "Warunek Product_Name <> _CurrentProduct nie dziala (dalby 589 854,55)."
        )

Funkcja TestResult jest wspólna dla wszystkich testów: porównuje wartość faktyczną z oczekiwaną i zwraca jeden wiersz z nazwą testu, statusem PASS/FAIL i opisem. Na końcu spinam wszystko UNION-em:

EVALUATE
UNION (
    Test_OppGap_1P_Positive (),
    Test_OppGap_1P_Negative (),
    Test_OppGap_1P_NoFalsePositive (),
    Test_OppGap_1P_NoFalseNegative (),
    -- ... i cztery testy dla dwóch produktów
)

Dwa triki warte zapamiętania:

  • TREATAS odtwarza kontekst filtra tak, jak zrobiłby to slicer w raporcie. To jest ta „wirtualna ręka”, która klika za Ciebie.
  • COALESCE(..., 0) zamienia pustą wartość na zero, żeby porównanie z oczekiwaną liczbą miało sens.

Wklejam do DAX Query View, klikam Run:

Success (453,7 ms) · 5 columns, 8 rows — 8/8 PASS
testowanie miar DAX

Osiem kontekstów filtra sprawdzonych w pół sekundy. Bez jednego kliknięcia w slicer.


Najważniejsza zasada: wartości oczekiwane weryfikuje człowiek

Tu łatwo się przejechać, więc zaznaczę mocno.

Gdyby AI wyliczyło oczekiwane wyniki testowaną miarą, test nie sprawdzałby niczego — miara zawsze zgadzałaby się sama ze sobą. To tzw. test cyrkularny i jest bezwartościowy.

Dlatego oczekiwane liczby policzyłam niezależnie, bezpośrednio z Fact_Sales i Dim_Product — surowe sumy przychodu i kosztu plus target z wymiaru produktu. Dopiero potem porównałam je z tym, co zwraca miara.

Rola AI to znalezienie przypadków i napisanie powtarzalnego kodu. Definicja tego, co jest poprawne, zostaje po stronie człowieka.


Które miary warto testować, a które odpuścić

Nie każda miara zasługuje na zestaw testów — i tu wiele osób traci czas. Testowanie miar DAX ma sens tam, gdzie miara robi coś z kontekstem filtra. Gdzie tylko sumuje kolumnę, nie ma czego testować.

Odpuść:

  • proste agregacje (SUM, COUNTROWS, DISTINCTCOUNT na jednej kolumnie),
  • miary, które są tylko formatowaniem innej miary,
  • proste różnice i udziały typu [A] - [B].

Testuj koniecznie:

  • miary manipulujące kontekstem filtra — wszystko z ALL, ALLSELECTED, ALLEXCEPT, REMOVEFILTERS, KEEPFILTERS. Zmiana jednego słowa zmienia tu wynik w połowie raportu,
  • miary z zabezpieczeniami typu HASONEVALUE / SELECTEDVALUE / ISINSCOPE — ich całym sensem jest zachowywać się inaczej na różnych poziomach. Karta KPI pokazuje poziom sumy, tabela pokazuje wiersze — to dwa różne zachowania i dwa różne ryzyka,
  • time intelligence — DATEADD, SAMEPERIODLASTYEAR, ruchome średnie. Zwłaszcza na brzegach: pierwszy okres w danych, luty przestępny, miesiące bez sprzedaży,
  • rankingi i TOPN — remisy potrafią zachować się kompletnie nieintuicyjnie,
  • wszystko, co porównuje element do grupy — udział w kategorii, benchmark do lidera, odchylenie od średniej. Tu decyduje to, co dokładnie jest tą grupą — a to zależy od filtrów,
  • miary dekomponujące odchylenie (price / volume / mix). Jeśli jeden składnik liczysz jako resztę po odjęciu pozostałych, to on pochłonie każdy błąd i nigdy się nie wyda. Takie składniki trzeba testować osobno, na ręcznie policzonym przypadku.

Prosta heurystyka: jeśli musiałaś się chwilę zastanowić, pisząc tę miarę — napisz do niej testy. Jeśli napisałaś ją odruchowo — odpuść.

A jeśli nie umiesz wymyślić prawdopodobnej złej wersji swojej miary, to znak, że jest za prosta, żeby ją testować. Bo testy 3 i 4 konstruuje się zawsze tak samo: wyobrażasz sobie najbardziej prawdopodobny błąd i szukasz sytuacji, w której zła wersja rozjedzie się z poprawną.


Co o tym sądzi AI patrząc na to jak ekspert Power BI

Poprosiłam Claude’a o szczerą ocenę tego podejścia — bez owijania w bawełnę. Wybrane fragmenty:

Największa wartość nie leży tam, gdzie się jej spodziewasz. Ludzie myślą, że zysk to „AI pisze kod za mnie”. Nieprawda — kod to piętnaście minut. Prawdziwy zysk to znalezienie przypadku brzegowego, o którym byś nie pomyślała. Workstation PC jako lider kategorii bez jednej transakcji — to nie jest oczywiste, dopóki ktoś nie przejrzy wszystkich dwudziestu wierszy wymiaru i nie zestawi ich z faktami.

Testy są tak dobre, jak dane, na których stoją. Przypadki opierają się na konkretnych produktach i liczbach. Jeśli dane się zmienią, testy zaczną świecić na czerwono bez żadnej zmiany w mierze. Docelowo powinny chodzić na stabilnym modelu testowym, nie na produkcji. To nie wada metody — to koszt wejścia, który ma każde testowanie, z AI czy bez.

8/8 PASS nie znaczy „miara jest poprawna”. Znaczy „miara jest poprawna w tych ośmiu kontekstach”. To realna, ale ograniczona gwarancja. Nie sprzedawaj jej sobie jako czegoś więcej.


Podsumowanie: work smart, not hard

Całe testowanie miar DAX sprowadza się do czterech kroków:

  1. Zacznij od prostej miary, żeby wyczuć cztery typy testów. Nie potrzebujesz do tego AI — wystarczy KEEPFILTERS i trochę wyobraźni o tym, jak Twoja miara mogłaby być zepsuta.
  2. Przy trudnych miarach podłącz AI do modelu przez Power BI Modeling MCP — żeby widziało dane, nie tylko nazwy kolumn.
  3. Napisz prompt z regułą biznesową, wymiarami, czterema typami testów i kontraktem wyjściowym.
  4. Zweryfikuj oczekiwane wartości samodzielnie i zapisz zapytanie razem z projektem.

Po weryfikacji testy przestają być „czymś od AI” i stają się zwykłym kodem DAX, który leży obok Twojego pliku i chodzi w pół sekundy.

A najlepsza część? Następnym razem, gdy zmienisz miarę o 23:00 przed prezentacją, nie będziesz klikać po slicerach i modlić się, że nic nie pękło. Klikniesz Run.

Zostaw komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *