{"id":110320,"date":"2026-06-02T12:00:00","date_gmt":"2026-06-02T12:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=110320"},"modified":"2026-05-28T14:51:08","modified_gmt":"2026-05-28T14:51:08","slug":"never-ship-a-broken-semantic-model-again-how-to-build-automated-tests-in-power-bi-with-user-defined-functions","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/business-intelligence\/powerbi\/never-ship-a-broken-semantic-model-again-how-to-build-automated-tests-in-power-bi-with-user-defined-functions\/","title":{"rendered":"Never ship a broken semantic model again: how to build automated tests in Power BI with user-defined functions"},"content":{"rendered":"\n<p><strong>User-defined functions (UDFs) in Power BI let you build reusable, automated tests for your semantic models \u2014 so you catch broken measures, duplicate rows, and faulty relationships before they reach your reports. This article walks through how to use UDFs alongside PQL.Assert to standardize testing across your team, and how to automate those tests with Power Automate or a Fabric Notebook.<\/strong><\/p>\n\n\n\n<p><strong>Reuse<\/strong> is a very important term in <a href=\"https:\/\/www.ibm.com\/think\/topics\/dataops\" target=\"_blank\" rel=\"noreferrer noopener\">DataOps<\/a>. It is defined as the practice of leveraging existing components, code, or processes across multiple projects to reduce redundancy and improve consistency.<\/p>\n\n\n\n<p>However, when it comes to <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/bi-sql-server\/power-bi-part-1-introduction\/\" target=\"_blank\" rel=\"noreferrer noopener\">Power BI<\/a>, reusing <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/bi-sql-server\/using-the-dax-calculate-and-values-functions\/\" target=\"_blank\" rel=\"noreferrer noopener\">DAX<\/a> measures across projects was a difficult &#8216;copy and paste&#8217; job. For my teams, we used DAX measures to help with testing our <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/semantic-model-more-than-a-simple-name-change\/\" target=\"_blank\" rel=\"noreferrer noopener\">semantic models<\/a>, but ensuring consistent testing conventions (and standard schemas of the tests) required lots of manual review. <\/p>\n\n\n\n<p>Thankfully, that changed in late 2025 when Microsoft introduced <a href=\"https:\/\/learn.microsoft.com\/en-us\/power-bi\/transform-model\/desktop-user-defined-functions-overview\" target=\"_blank\" rel=\"noreferrer noopener\">User Defined Functions (UDFs) for Power BI<\/a>. In this article, I&#8217;ll demonstrate how to use UDFs for testing, plus how to standardize the way teams test their models.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-is-it-important-to-test-semantic-models\">Why is it important to test semantic models?<\/h2>\n\n\n\n<p>Many folks ask me, <em>\u201cwhy would you test your model?\u201d<\/em> Humans are fallible, so the semantic models we build are fallible as well. It&#8217;s easy to write a DAX measure that doesn\u2019t properly handle filter context and results in inaccurate summations when a slicer filters the model. <\/p>\n\n\n\n<p>It&#8217;s also easy to leave a filter in a step in <a href=\"https:\/\/www.red-gate.com\/simple-talk\/business-intelligence\/13-things-i-wish-i-knew-about-power-query\/\" target=\"_blank\" rel=\"noreferrer noopener\">Power Query<\/a> when debugging your transformations, resulting in rows being accidentally removed from the model (not that I have ever done that!)<\/p>\n\n\n\n<p>We should not only test to avoid repeating past mistakes, but for parts of the model we anticipate would have detrimental effects on reports. For example, columns used in relationships, often-used DAX measures, etc. <\/p>\n\n\n\n<p>From a technical perspective, <strong>DAX and Power Query are code<\/strong>. Yes, they are maybe somewhat abstracted by low-code tools, but <strong>code should be tested nonetheless<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-to-use-user-defined-functions-for-testing-in-power-bi\">How to use user-defined functions for testing in Power BI<\/h2>\n\n\n\n<p><strong>I&#8217;ll run through the process step-by-step. First, the setup. You\u2019ll need to specifically turn on UDFs, since it&#8217;s currently (as of March 2026), a preview feature<\/strong>:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Open Power BI Desktop.<br><br><\/li>\n\n\n\n<li>Go to File > Options and Settings > Options.<br><br><\/li>\n\n\n\n<li>Select Preview Features from the left-hand menu.<br><br><\/li>\n\n\n\n<li>Check the box for User Defined Functions and click OK.<br><br><\/li>\n\n\n\n<li>Restart Power BI Desktop for the change to take effect.<\/li>\n<\/ul>\n<\/div>\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"365\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-8.png\" alt=\"An image showing the enablement of user-defined functions in Power BI Desktop preview features\" class=\"wp-image-110321\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-8.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-8-300x125.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-8-768x320.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>Enabling user-defined functions in Power BI Desktop preview features<\/em><\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-how-to-import-pql-assert\">How to import PQL.Assert<\/h3>\n\n\n\n<p>While UDFs are a new concept to Power BI, the idea behind them exists in many other programming languages to share and reuse code. C# has <a href=\"https:\/\/www.nuget.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">NuGet<\/a>, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/development\/javascript\/the-javascript-landscape-in-broad-brushstrokes\/\" target=\"_blank\" rel=\"noreferrer noopener\">JavaScript<\/a> has npm, and <a href=\"https:\/\/pypi.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">Python has PyPI<\/a>. <\/p>\n\n\n\n<p><strong>Thankfully, the team at <a href=\"https:\/\/sqlbi.com\" target=\"_blank\" rel=\"noreferrer noopener\">SQLBI<\/a> introduced <a href=\"https:\/\/daxlib.org\" target=\"_blank\" rel=\"noreferrer noopener\">DAXLib<\/a>.<\/strong> This site has a catalog of UDFs organized into packages which serve different purposes. There are ones that help build SVGs for visuals and others that help with calculating industry metrics. <\/p>\n\n\n\n<p><strong>The one that assists with building tests, however, is <a href=\"https:\/\/daxlib.org\/package\/PQL.Assert\" target=\"_blank\" rel=\"noreferrer noopener\">PQL.Assert<\/a>. It&#8217;s an open-source library I helped develop.<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"456\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-9.png\" alt=\"An image showing the DAXLib package catalog showing PQL.Assert\" class=\"wp-image-110322\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-9.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-9-300x156.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-9-768x400.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>The DAXLib package catalog showing PQL.Assert<\/em><\/figcaption><\/figure>\n\n\n\n<p>Here&#8217;s how to import PQL.Assert:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Navigate to the <a href=\"https:\/\/daxlib.org\/package\/PQL.Assert\" target=\"_blank\" rel=\"noreferrer noopener\"><em>PQL.Assert package page<\/em><\/a> on DAXLib.<br><br><\/li>\n\n\n\n<li>Click the <strong>Add to Power BI<\/strong> button.<\/li>\n<\/ul>\n<\/div>\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"839\" height=\"418\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-10.png\" alt=\"An image showing clicking the 'Add to Power BI' button\" class=\"wp-image-110323\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-10.png 839w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-10-300x149.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-10-768x383.png 768w\" sizes=\"auto, (max-width: 839px) 100vw, 839px\" \/><figcaption class=\"wp-element-caption\"><em>Clicking the &#8216;Add to Power BI&#8217; button<\/em><\/figcaption><\/figure>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Select the <strong>Copy<\/strong> button to copy the TMDL script to your clipboard.<\/li>\n<\/ul>\n<\/div>\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"840\" height=\"745\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-11.png\" alt=\"An image showing selecting the 'Copy' button to copy the TMDL script.\" class=\"wp-image-110324\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-11.png 840w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-11-300x266.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-11-768x681.png 768w\" sizes=\"auto, (max-width: 840px) 100vw, 840px\" \/><figcaption class=\"wp-element-caption\"><em>Selecting the &#8216;Copy&#8217; button to copy the TMDL script<\/em><\/figcaption><\/figure>\n\n\n\n<p>Now, open your model in Power BI Desktop.<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Navigate to the <strong>TMDL View<\/strong>.<br><br><\/li>\n\n\n\n<li>Create a new tab by clicking the <strong>\u201c+\u201d<\/strong> icon.<br><br><\/li>\n\n\n\n<li>Paste the script (Ctrl+V).<br><br><\/li>\n\n\n\n<li>Then hit the <strong>\u201cApply\u201d<\/strong> button.<\/li>\n<\/ul>\n<\/div>\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"612\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-12.png\" alt=\"\" class=\"wp-image-110325\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-12.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-12-300x210.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-12-768x537.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>Pasting the PQL.Assert TMDL script in the TMDL View<\/em><\/figcaption><\/figure>\n\n\n\n<p>PQL.Assert provides a comprehensive set of assertion functions organized into several categories:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Basic Assertions:<\/strong> Validate simple conditions such as <code>PQL.Assert.ShouldBeTrue<\/code>, <code>PQL.Assert.ShouldEqual<\/code>, and <code>PQL.Assert.ShouldBeNull<\/code>.<br><br><\/li>\n\n\n\n<li><strong>Column Assertions:<\/strong> Validate column-level properties such as <code>PQL.Assert.Col.ShouldBeDistinct<\/code>, <code>PQL.Assert.Col.ShouldNotBeNull<\/code>, and <code>PQL.Assert.Col.ShouldBeInRange<\/code>.<br><br><\/li>\n\n\n\n<li><strong>Table Assertions:<\/strong> Validate table-level properties such as <code>PQL.Assert.Tbl.ShouldHaveRows<\/code>, <code>PQL.Assert.Tbl.ShouldExist<\/code>, and <code>PQL.Assert.Tbl.ShouldHaveColumns<\/code>.<br><br><\/li>\n\n\n\n<li><strong>Relationship Assertions:<\/strong> Validate model relationships with <code>PQL.Assert.Relationship.ShouldExist<\/code>.<\/li>\n<\/ul>\n<\/div>\n\n\n<section id=\"my-first-block-block_8c4173254ac3ae2efa32abd4fd290962\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Enjoying this article? Subscribe to the Simple Talk newsletter<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Get selected articles, event information, podcasts and other industry content delivered straight to your inbox.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/simple-talk\/subscribe\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Subscribe now: Enjoying this article? Subscribe to the Simple Talk newsletter\">Subscribe now<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-to-build-your-first-test-with-power-bi-user-defined-functions\">How to build your first test with Power BI user-defined functions<\/h2>\n\n\n\n<p>Let\u2019s say you depend on an upstream SQL provider to provide you data for part of your model. From time to time they truncate the tables to hydrate them with the most up-to-date data. <\/p>\n\n\n\n<p><strong>However, their routines <em>sometimes<\/em> hydrate data with duplicates.<\/strong> <strong>This led to inaccurate calculations, so you had to put Power Query in place to remove duplicate rows.<\/strong> <\/p>\n\n\n\n<p>Of course, you\u2019d now like to ensure that those columns stay distinct, and avoid getting calls about calculations looking wrong. How would you write that test? Let&#8217;s find out.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-first-open-your-model-in-power-bi-desktop-and-go-to-dax-query-view\">First, open your model in Power BI Desktop and go to DAX Query View:<\/h4>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Create a new tab.<\/strong> Click the \u201c+\u201d icon in DAX Query View to add a new query tab.<br><br><\/li>\n\n\n\n<li><strong>Name the tab<\/strong> following the naming convention <code>[name].[environment].test(s)<\/code>. For example, name it <code>DataQuality.ANY.Tests<\/code> so it applies to all environments.<br><br><\/li>\n\n\n\n<li><strong>Enter the template for defining your test.<\/strong> Start with a <code>DEFINE<\/code> block and a <code>FUNCTION<\/code> declaration:<\/li>\n<\/ul>\n<\/div>\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">DEFINE\n    FUNCTION DataQuality.ANY.Tests = () =&gt;\n    PQL.Assert.Col.ShouldBeDistinct(\"Verify AlignmentID is distinct\", 'AlignmentDim'[AlignmentID])\n\nEVALUATE DataQuality.ANY.Tests()<\/pre><\/div>\n\n\n\n<p><strong>Enter PQL.Assert<\/strong> and the <a href=\"https:\/\/www.red-gate.com\/hub\/product-learning\/sql-prompt\/sql-intellisense-and-autocomplete-in-ssms-and-sql-prompt\" target=\"_blank\" rel=\"noreferrer noopener\">IntelliSense<\/a> will display a wide array of functions for testing. You can see all of them listed <a href=\"https:\/\/daxlib.org\/package\/PQL.Assert\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>. But, for now, we will use <code>PQL.Assert.Col.ShouldBeDistinct<\/code> (in turn, ensuring that all column values are unique.)<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"535\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-13.png\" alt=\"An image of IntelliSense showing PQL.Assert functions in DAX Query View.\" class=\"wp-image-110326\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-13.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-13-300x183.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-13-768x470.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>IntelliSense showing PQL.Assert functions in DAX Query View<\/em><\/figcaption><\/figure>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Define the test name<\/strong> in the first parameter. I like to give it a brief assertion, such as \u201cVerify AlignmentID is distinct.\u201d<br><br><\/li>\n\n\n\n<li><strong>Identify the column.<\/strong> The beauty of UDFs is that they allow you to pass in columns or tables as parameters. In this case, we pass <code>AlignmentDim[AlignmentID]<\/code> as the column reference.<br><br><\/li>\n\n\n\n<li>For <strong>two columns<\/strong>, wrap them in a <code>UNION<\/code> function. For example, if you also need to verify that <code>EyeID<\/code> is distinct:<\/li>\n<\/ul>\n<\/div>\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">DEFINE\n    FUNCTION DataQuality.ANY.Tests = () =&gt;\n    UNION(\n        PQL.Assert.Col.ShouldBeDistinct(\"Verify AlignmentID is distinct\", 'AlignmentDim'[AlignmentID]),\n        PQL.Assert.Col.ShouldBeDistinct(\"Verify Eye Color is distinct\", 'EyeColorDim'[EyeID])\n    )\n\nEVALUATE DataQuality.ANY.Tests()<\/pre><\/div>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"274\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-14.png\" alt=\"An image showing the defining of a test with two distinct column assertions using UNION.\" class=\"wp-image-110327\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-14.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-14-300x94.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-14-768x240.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>Defining a test with two distinct column assertions using UNION<\/em><\/figcaption><\/figure>\n\n\n\n<p><strong>Click the Run button<\/strong>, check the results, and then you can click \u201cAdd to Model&#8221;:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"561\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-15.png\" alt=\"An image showing clicking the 'Run' button and adding the test to the model.\" class=\"wp-image-110328\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-15.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-15-300x192.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-15-768x492.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>Clicking the &#8216;Run&#8217; button and adding the test to the model<\/em><\/figcaption><\/figure>\n\n\n\n<p>All test functions return results following a standard schema:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><td>Column Name<\/td><td>Type<\/td><td>Description<\/td><\/tr><\/thead><tbody><tr><td>TestName<\/td><td>String<\/td><td>Description of the test being conducted<\/td><\/tr><tr><td>Expected<\/td><td>Any<\/td><td>What the test should result in<\/td><\/tr><tr><td>Actual<\/td><td>Any<\/td><td>The result of the test under the current dataset<\/td><\/tr><tr><td>Passed<\/td><td>Boolean<\/td><td>True if expected matches actual, otherwise false<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"241\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-16.png\" alt=\"Test results showing the standard schema with TestName, Expected, Actual, and Passed columns\" class=\"wp-image-110329\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-16.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-16-300x83.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-16-768x212.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>Test results showing the standard schema with TestName, Expected, Actual, and Passed columns<\/em><\/figcaption><\/figure>\n\n\n\n<p><strong>This test now stays with the model, so it can be validated any time the model is opened.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-to-automate-testing-with-user-defined-functions-in-powerbi\">How to automate testing with user-defined functions in PowerBI<\/h2>\n\n\n\n<p><strong>Manual testing is fine, but what about running these tests in the service? Can we automate them? Well, yes. There are two easy options:<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-the-power-automate-approach-to-testing-step-by-step-instructions\">The Power Automate approach to testing (step-by-step instructions)<\/h3>\n\n\n\n<p>The PQL.Assert repository includes a ready-made Power Automate solution you can import directly. You can download it from <a href=\"https:\/\/github.com\/clientfirsttech\/PQL.Assert\/tree\/main\/examples\/power-automate\" target=\"_blank\" rel=\"noreferrer noopener\">the examples\/power-automate folder<\/a> in the PQL.Assert GitHub repository. <\/p>\n\n\n\n<p>The solution ZIP file (named <code>PQLAssertviaPowerAutomate_x_x_x_x.zip<\/code>, where <code>x_x_x_x<\/code> will be a version number) contains a pre-built flow that handles test discovery, execution, and result evaluation for you.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-here-s-how-to-set-up-automated-testing-with-power-automate-step-by-step\">Here&#8217;s how to set up automated testing with Power Automate, step-by-step:<\/h4>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Download the solution<\/strong> from <a href=\"https:\/\/github.com\/clientfirsttech\/PQL.Assert\/tree\/main\/examples\/power-automate\" target=\"_blank\" rel=\"noreferrer noopener\"><em>examples\/power-automate<\/em><\/a> in the PQL.Assert repository. The file is named <code>PQLAssertviaPowerAutomate_x_x_x_x.zip<\/code>, where <code>x_x_x_x<\/code> will be a version number.<br><br><\/li>\n\n\n\n<li><strong>Navigate to <\/strong><a href=\"https:\/\/make.powerautomate.com\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Power Automate<\/strong><\/a> or <strong><a href=\"https:\/\/make.powerapps.com\" target=\"_blank\" rel=\"noreferrer noopener\">Power Apps<\/a>.<\/strong><br><br><\/li>\n\n\n\n<li><strong>Import the solution<\/strong> following Microsoft\u2019s guide: <a href=\"https:\/\/learn.microsoft.com\/en-us\/power-apps\/maker\/data-platform\/import-update-export-solutions\" target=\"_blank\" rel=\"noreferrer noopener\"><em>Import solutions<\/em><\/a>.<br><br><\/li>\n\n\n\n<li><strong>Set environment variables<\/strong> when prompted during import. You will need to provide two values:<br><br><ul><li><strong>Workspace GUID:<\/strong> The unique identifier of your Power BI workspace. You can find this in the workspace URL: <code>https:\/\/app.powerbi.com\/groups\/{workspace-guid}\/...<\/code><\/li><\/ul><br><div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Semantic Model ID:<\/strong> The unique identifier of your semantic model. You can find this in the dataset URL: <code>https:\/\/app.powerbi.com\/groups\/{workspace-guid}\/datasets\/{semantic-model-id}\/...<\/code><\/li>\n<\/ul>\n<\/div><\/li>\n<\/ul>\n<\/div>\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"840\" height=\"176\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-17.png\" alt=\"Retrieving the Workspace GUID and Semantic Model ID from the Power BI service URL\" class=\"wp-image-110330\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-17.png 840w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-17-300x63.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-17-768x161.png 768w\" sizes=\"auto, (max-width: 840px) 100vw, 840px\" \/><figcaption class=\"wp-element-caption\"><em>Retrieving the Workspace GUID and Semantic Model ID from the Power BI service URL<\/em><\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"625\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-18.png\" alt=\"Setting the Workspace GUID and Semantic Model ID environment variables during solution import\" class=\"wp-image-110331\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-18.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-18-300x214.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-18-768x549.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>Setting the Workspace GUID and Semantic Model ID environment variables during solution import<\/em><\/figcaption><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-once-imported-the-flow-does-the-following\">Once imported, the flow does the following:<\/h4>\n\n\n\n<p><strong>Retrieves tests<\/strong> by executing <code>PQL.Assert.RetrieveTestsByEnvironment<\/code> against your semantic model. This is the V1 function, which uses only <code>INFO.FUNCTIONS<\/code> and is compatible with the Power Automate \u201cExecute Dataset Query\u201d action. <\/p>\n\n\n\n<p>It returns a table of all functions ending with <code>.Test<\/code> or <code>.Tests<\/code> that match your target environment (e.g., <code>.PROD.<\/code> or <code>.ANY.<\/code>):<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"884\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-19.png\" alt=\"The Retrieve Tests step in the Power Automate flow discovering test functions\" class=\"wp-image-110332\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-19.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-19-297x300.png 297w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-19-768x776.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>The Retrieve Tests step in the Power Automate flow discovering test functions<\/em><\/figcaption><\/figure>\n\n\n\n<p><strong>Parses the results<\/strong> to extract the list of test function names from the response:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"857\" height=\"1024\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-20-857x1024.png\" alt=\"Parsing the retrieved test function names in the Power Automate flow\" class=\"wp-image-110333\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-20-857x1024.png 857w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-20-251x300.png 251w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-20-768x917.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-20.png 875w\" sizes=\"auto, (max-width: 857px) 100vw, 857px\" \/><figcaption class=\"wp-element-caption\"><em>Parsing the retrieved test function names in the Power Automate flow<\/em><\/figcaption><\/figure>\n\n\n\n<p><strong>Executes each test<\/strong> by running a \u201cRun a query against a dataset\u201d action for each discovered test function. For example:<\/p>\n\n\n\n<p><code>EVALUATE DataQuality.ANY.Tests()<\/code><\/p>\n\n\n\n<p><strong>Evaluates the Passed column<\/strong> and checks for any rows where <code>Passed = FALSE<\/code>:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"841\" height=\"578\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-21.png\" alt=\"Executing tests and evaluating results in the Power Automate flow\" class=\"wp-image-110334\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-21.png 841w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-21-300x206.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-21-768x528.png 768w\" sizes=\"auto, (max-width: 841px) 100vw, 841px\" \/><figcaption class=\"wp-element-caption\"><em>Executing tests and evaluating results in the Power Automate flow<\/em><\/figcaption><\/figure>\n\n\n\n<p><strong>This template allows you to handle test results however you see fit. The flow can be extended to do things like save results to <a href=\"https:\/\/www.microsoft.com\/en-gb\/microsoft-365\/sharepoint\/collaboration\" target=\"_blank\" rel=\"noreferrer noopener\">SharePoint<\/a>, post failed test results to a Microsoft Teams channel for immediate action, or send an email notification. The options are nearly endless.<\/strong><\/p>\n\n\n\n<p><em><strong>Important note:<\/strong> The Power Automate approach uses the V1 functions (<code>PQL.Assert.RetrieveTests \/ PQL.Assert.RetrieveTestsByEnvironment<\/code>). The V2 functions use <code>INFO.USERDEFINEDFUNCTIONS<\/code> and <code>INFO.ANNOTATIONS<\/code>, which are not supported in the Power Automate Execute Dataset Query action. <\/em><\/p>\n\n\n\n<p><em>This also means the Power Automate approach does not support <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/sql-server-row-level-security-introduction\/\" target=\"_blank\" rel=\"noreferrer noopener\">Row-Level Security (RLS)<\/a> testing via user impersonation. If you need RLS testing, use the Fabric Notebook approach below.<\/em><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-how-to-use-the-fabric-notebook-approach-for-testing-step-by-step\">How to use the Fabric Notebook approach for testing (step-by-step)<\/h3>\n\n\n\n<p><strong>The PQL.Assert repository also includes a ready-made <a href=\"https:\/\/learn.microsoft.com\/en-us\/fabric\/data-engineering\/how-to-use-notebook\" target=\"_blank\" rel=\"noreferrer noopener\">Fabric Notebook<\/a> you can upload directly to your workspace. You can download it from <a href=\"https:\/\/github.com\/clientfirsttech\/PQL.Assert\/tree\/main\/examples\/fabric-notebook\" target=\"_blank\" rel=\"noreferrer noopener\">the examples\/fabric-notebook folder<\/a> in the PQL.Assert GitHub repository.<\/strong><\/p>\n\n\n\n<p>The notebook (<code>RunPQLAssertTests.ipynb<\/code>) is a multi-workspace test runner that discovers all semantic models in each workspace, retrieves PQL.Assert test functions, and executes every test. <\/p>\n\n\n\n<p><strong>It also includes support for Row-Level Security (RLS) impersonation, which is a key advantage of this approach versus the Power Automate approach.<\/strong> <\/p>\n\n\n\n<p>This support of <strong>RLS testing<\/strong> works as so: when a test function has the <code>PQLAssert_ImpersonatedUserName<\/code> annotation, the notebook will execute the test as that user using the <code>semantic-link-labs<\/code> impersonation API. <\/p>\n\n\n\n<p>This lets you validate that your RLS rules are working correctly &#8211; something the Power Automate approach <em>cannot<\/em> do.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-how-to-add-annotations-in-the-tmdl-view\">How to add annotations in the TMDL view<\/h4>\n\n\n\n<p><strong>To enable RLS impersonation for a test, you need to add a <code>PQLAssert_ImpersonatedUserName<\/code> annotation to the test function in the <a href=\"https:\/\/www.red-gate.com\/simple-talk\/business-intelligence\/power-bi-tmdl-benefits-security-risks-best-practices\/\" target=\"_blank\" rel=\"noreferrer noopener\">TMDL view<\/a>.<\/strong> <\/p>\n\n\n\n<p>When you save your model in Power BI project format (<code>.pbip<\/code>), each DAX Query View tab\u2019s test function is stored as a TMDL file. You can add the annotation directly in the TMDL source. For example, in the PQL.Assert test suite, there is an RLS test defined in the DAX Query View:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">DEFINE\n    FUNCTION RLS.ANY.Tests = () =&gt;\n        PQL.Assert.ShouldEqualExactly(\n            \"West Security Group Should Not See Other Groups\",\n            0,\n            COUNTROWS(FILTER('Groups', Groups[Group Name] &lt;&gt; \"West\")) + 0\n        )\n\nEVALUATE RLS.ANY.Tests()<\/pre><\/div>\n\n\n\n<p>When this test is added to the model via \u201cAdd to Model,\u201d the corresponding TMDL file (found in the <code>TMDLScripts<\/code> folder of your <code>.pbip project<\/code>), will look like this:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">createOrReplace\n\n    function 'RLS.ANY.Tests' =\n            () =&gt;\n                    PQL.Assert.ShouldEqualExactly(\"West Security Group Should Not See Other Groups\",0,COUNTROWS(FILTER('Groups',Groups[Group Name]&lt;&gt;\"West\"))+0)\n        lineageTag: de968091-8b67-454c-bfa9-61fa543cbdad\n\n        annotation PQLAssert_ImpersonatedUserName = user@contoso.com<\/pre><\/div>\n\n\n\n<p>The annotation <code>PQLAssert_ImpersonatedUserName = user@contoso.com<\/code> line tells the notebook to execute this test function as that user. When the notebook calls <code>PQL.Assert.RetrieveTestsByEnvironmentV2<\/code>, it reads this annotation and applies impersonation automatically:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"261\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-22.png\" alt=\"Adding the PQLAssert_ImpersonatedUserName annotation in the TMDL view\" class=\"wp-image-110335\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-22.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-22-300x89.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-22-768x229.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>Adding the <code>PQLAssert_ImpersonatedUserName<\/code> annotation in the TMDL view<\/em><\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-how-to-set-up-automated-testing-with-the-fabric-notebook-step-by-step\">How to set up automated testing with the Fabric Notebook (step-by-step)<\/h3>\n\n\n\n<p>Here is a walkthrough to set up automated testing with the Fabric Notebook:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Download the notebook<\/strong> (<code>RunPQLAssertTests.ipynb<\/code>) from <a href=\"https:\/\/github.com\/clientfirsttech\/PQL.Assert\/tree\/main\/examples\/fabric-notebook\" target=\"_blank\" rel=\"noreferrer noopener\">examples\/fabric-notebook<\/a> in the PQL.Assert repository.<br><br><\/li>\n\n\n\n<li><strong>Upload the notebook<\/strong> to your Microsoft Fabric workspace. Navigate to your workspace in the Fabric portal and use the Import option to upload the <code>.ipynb<\/code> file.<br><br><\/li>\n\n\n\n<li><strong>Update the workspace GUIDs<\/strong> in Step 3 of the notebook. Replace the placeholder GUIDs with the real workspace GUIDs you want to scan:<\/li>\n<\/ul>\n<\/div>\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">WORKSPACE_GUIDS: list[str] = [\n    \"00000000-0000-0000-0000-000000000002\"  # Replace with your workspace GUID\n]\n\nENVIRONMENT: str = \"ANY\"  # Options: \"DEV\" | \"TEST\" | \"PROD\" | \"ANY\" | \"\"<\/pre><\/div>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Run Step 1<\/strong> to install dependencies. The notebook uses the <code>semantic-link-labs<\/code> package, which provides the impersonation capabilities on top of the standard <code>sempy.fabric<\/code> library.<br><br><\/li>\n\n\n\n<li><strong>Run Step 2<\/strong> to import the libraries (<code>sempy.fabric<\/code>, <code>sempy_labs<\/code>, and <code>pandas<\/code>).<\/li>\n<\/ul>\n<\/div>\n\n\n<h4 class=\"wp-block-heading\" id=\"h-the-fabric-notebook-then-handles-the-rest-automatically\">The Fabric Notebook then handles the rest automatically:<\/h4>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Discovers all semantic models<\/strong> in each workspace using <code>fabric.list_datasets<\/code>.<br><\/li>\n\n\n\n<li><strong>Retrieves tests<\/strong> for each model by calling <code>PQL.Assert.RetrieveTestsByEnvironmentV2<\/code>, which returns full metadata including the <code>[Name]<\/code>, <code>[Description]<\/code>, and <code>[PQLAssert_ImpersonatedUserName]<\/code> columns.<\/li>\n<\/ul>\n<\/div>\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"532\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-23.png\" alt=\"The notebook discovering test functions across workspaces and semantic models\" class=\"wp-image-110336\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-23.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-23-300x182.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-23-768x467.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>The notebook discovering test functions across workspaces and semantic models<\/em><\/figcaption><\/figure>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Executes each test<\/strong>, checking for impersonation. If <code>PQLAssert_ImpersonatedUserName<\/code> is populated, it calls <code>sempy_labs.evaluate_dax_impersonation<\/code> to run the test as that user, validating your RLS rules. If the annotation is blank, it executes the test normally via <code>fabric.evaluate_dax<\/code>.<br><br><\/li>\n\n\n\n<li><strong>Collects all results<\/strong> into a single output table annotated with workspace name, semantic model name, test function name, impersonated user, and the standard PQL.Assert columns (<code>TestName<\/code>, <code>Expected<\/code>, <code>Actual<\/code>, <code>Passed<\/code>).<br><br><\/li>\n\n\n\n<li><strong>Displays a summary<\/strong> showing totals for passed and failed tests across all workspaces and models.<\/li>\n<\/ul>\n<\/div>\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"511\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-24.png\" alt=\"Test results summary from the Fabric Notebook showing passed and failed tests\" class=\"wp-image-110337\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-24.png 875w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-24-300x175.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/05\/image-24-768x449.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><figcaption class=\"wp-element-caption\"><em>Test results summary from the Fabric Notebook showing passed and failed tests<\/em><\/figcaption><\/figure>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Schedule the notebook<\/strong> to run on a regular cadence using Fabric\u2019s built-in scheduling, such as after each data refresh.<br><br><\/li>\n\n\n\n<li>Optionally, <strong>log the results<\/strong> to a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/query-performance-analysis-in-lakehouses\/\" target=\"_blank\" rel=\"noreferrer noopener\">Lakehouse<\/a> table (or Eventhouse) for near real-time monitoring and historical tracking.<\/li>\n<\/ul>\n<\/div>\n\n\n<p><strong><em>Important<\/em><\/strong> <strong><em>tip: <\/em><\/strong><em>the notebook identity (or the capacity admin token) must have <strong>Build<\/strong> access (at a minimum) on each target workspace. For impersonated tests, the workspace <strong>must use RLS<\/strong>, and the <strong>target user must exist in the tenant<\/strong>, <strong>with the appropriate role for the semantic model.<\/strong><\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-these-steps-are-the-same-for-other-models-too\">These steps are the same for other models, too<\/h2>\n\n\n\n<p>The great news is, these steps are the same for other models as well. You just have to alter the tests for your specific requirements. By following this approach, we are effectively reusing the testing framework with PQL.Assert across <em>all <\/em>of our semantic models. <\/p>\n\n\n\n<p>And, by bringing tests into the model itself, they can be executed locally in Power BI Desktop during development, and remotely in the Power BI service through Power Automate or Fabric Notebooks. <\/p>\n\n\n\n<p>Better still, the automated testing behaves the same regardless of where it runs, simply because we&#8217;re reusing PQL.Assert functions across models to identify tests and execute tests consistently. <strong>That is the power of reuse.<\/strong><\/p>\n\n\n\n<p><strong>Any thoughts or feedback? I&#8217;d love to hear from you. Feel free to drop a comment below.<\/strong><\/p>\n\n\n\n<section id=\"my-first-block-block_202fce3df4be5eca8ee14c30dc6833f3\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Simple Talk is brought to you by Redgate Software<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Take control of your databases with the trusted Database DevOps solutions provider. Automate with confidence, scale securely, and unlock growth through AI.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/solutions\/overview\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Discover how Redgate can help you: Simple Talk is brought to you by Redgate Software\">Discover how Redgate can help you<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: How to test in Power BI with user-defined functions<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What are user-defined functions in Power BI?<\/h3>\n            <div class=\"faq-answer\">\n                <p>User-defined functions are reusable DAX functions introduced in Power BI in late 2025. They let you write logic once and share it across multiple models or projects, similar to how NuGet works in C# or npm in JavaScript.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. What is PQL.Assert?<\/h3>\n            <div class=\"faq-answer\">\n                <p>PQL.Assert is an open-source DAX library available through DAXLib that provides a standard set of assertion functions for testing Power BI semantic models, covering columns, tables, relationships, and basic conditions.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Can Power BI semantic model tests be automated?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Yes. Tests built with PQL.Assert can be automated using either a pre-built Power Automate flow or a Fabric Notebook, both available in the PQL.Assert GitHub repository.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. Does automated testing support Row-Level Security?<\/h3>\n            <div class=\"faq-answer\">\n                <p>The Fabric Notebook approach supports RLS testing via user impersonation. The Power Automate approach does not.<\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to test Power BI semantic models using User-Defined Functions and PQL.Assert. Catch errors early and automate testing with Power Automate or Fabric Notebooks.&hellip;<\/p>\n","protected":false},"author":344919,"featured_media":107492,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[159160,47,159166],"tags":[159390,101611,4150],"coauthors":[159224],"class_list":["post-110320","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-business-intelligence","category-data-science","category-powerbi","tag-dataops","tag-power-bi","tag-sql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/110320","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/344919"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=110320"}],"version-history":[{"count":6,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/110320\/revisions"}],"predecessor-version":[{"id":110591,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/110320\/revisions\/110591"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/107492"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=110320"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=110320"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=110320"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=110320"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}