{"id":14374,"date":"2022-06-13T14:00:24","date_gmt":"2022-06-13T21:00:24","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/cse\/?p=14374"},"modified":"2023-07-26T06:19:03","modified_gmt":"2023-07-26T13:19:03","slug":"an-approach-to-unit-testing-adx-functions","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/an-approach-to-unit-testing-adx-functions\/","title":{"rendered":"An Approach to Unit Testing ADX Functions"},"content":{"rendered":"<h2 id=\"overview\" class=\"code-line\" dir=\"auto\" data-line=\"2\">Overview<\/h2>\n<p class=\"code-line\" dir=\"auto\" data-line=\"4\">Our application contains many functions that return data stored in Azure Data Explorer (ADX). We wrote these functions in Kusto Query Language (KQL) and each function returns a set of data based on the arguments passed. Although developers tested these functions as they wrote them, we needed a way to validate that the functions continued to work as the code and the data changed. Automated Unit testing is an essential part of any application development life cycle. It validates that code works properly and minimizes the risk that future code changes will break existing functionality. In this article, I will discuss the approach we took in automating the testing of ADX functions.<\/p>\n<h2 id=\"the-kql-assert-function\" class=\"code-line\" dir=\"auto\" data-line=\"8\">The KQL Assert Function<\/h2>\n<p class=\"code-line\" dir=\"auto\" data-line=\"10\">ADX does not ship with a unit testing framework, but KQL has a static\u00a0<a title=\"https:\/\/docs.microsoft.com\/en-us\/azure\/data-explorer\/kusto\/query\/assert-function\" href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/data-explorer\/kusto\/query\/assert-function\" data-href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/data-explorer\/kusto\/query\/assert-function\">assert<\/a>\u00a0function that can be used to test functions and queries.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"12\">The\u00a0<em>assert<\/em>\u00a0function accepts two arguments: a Boolean condition and a string. If the Boolean condition returns false, an error is raised, and the string is returned. We can use this to test that our ADX functions perform as expected.<\/p>\n<h2 id=\"a-sample-test\" class=\"code-line\" dir=\"auto\" data-line=\"15\">A Sample Test<\/h2>\n<p class=\"code-line\" dir=\"auto\" data-line=\"17\">For example, the following code tests if\u00a0<em>table1<\/em> contains any rows.<\/p>\n<div style=\"background: #ffffff; overflow: auto; width: auto; border: solid gray; border-width: .1em .1em .1em .8em; padding: .2em .6em;\">\n<pre style=\"margin: 0; line-height: 125%;\">let testTitle <span style=\"color: #333333;\">=<\/span> <span style=\"background-color: #fff0f0;\">'table1 should return some rows'<\/span>;\r\nlet checkfn <span style=\"color: #333333;\">=<\/span> (rowCount: long) \r\n<span style=\"color: #ff0000; background-color: #ffaaaa;\">{<\/span>\r\n  assert(rowCount <span style=\"color: #333333;\">&gt;<\/span> <span style=\"color: #0000dd; font-weight: bold;\">0<\/span>, testTitle)\r\n<span style=\"color: #ff0000; background-color: #ffaaaa;\">}<\/span>;\r\nlet results <span style=\"color: #333333;\">=<\/span> table1;\r\nprint testTitle, checkfn(toscalar(results\r\n    <span style=\"color: #333333;\">|<\/span> summarize rowCount <span style=\"color: #333333;\">=<\/span> <span style=\"color: #008800; font-weight: bold;\">count<\/span>()\r\n    ))\r\n<\/pre>\n<\/div>\n<p class=\"code-line\" dir=\"auto\" data-line=\"17\">The code above throws an exception and outputs &#8220;table1 should return some rows&#8221; if no data is in table1. We can be more deliberate in our tests and test the flow of data into the table (via Update Policies or into Materialized views) if we populate our tables with well-known data. ADX provides the\u00a0<em>.ingest<\/em>\u00a0command, which allows us to script data ingestion. For small datasets, we can use the\u00a0<em>.ingest<\/em>\u00a0inline command. For larger datasets, we can ingest from Azure storage. Assume we have an\u00a0<em>AssetHistory<\/em>\u00a0table with the data below:<\/p>\n<pre><code class=\"code-line language-kql\" dir=\"auto\" data-line=\"19\"><code><\/code><\/code><\/pre>\n<pre><code class=\"code-line language-kql\" dir=\"auto\" data-line=\"19\"><code><\/code><\/code><\/pre>\n<table class=\"code-line\" dir=\"auto\" data-line=\"36\">\n<thead class=\"code-line\" dir=\"auto\" data-line=\"36\">\n<tr class=\"code-line\" dir=\"auto\" data-line=\"36\">\n<th>timeStamp<\/th>\n<th>organizationId<\/th>\n<th>assetId<\/th>\n<th>location<\/th>\n<\/tr>\n<\/thead>\n<tbody class=\"code-line\" dir=\"auto\" data-line=\"38\">\n<tr class=\"code-line\" dir=\"auto\" data-line=\"38\">\n<td>2022-02-08T21:25:03Z<\/td>\n<td>ORG-0001<\/td>\n<td>90000001<\/td>\n<td>{&#8220;lat&#8221;:34.13803101,&#8221;lon&#8221;:-84.13373566}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"39\">\n<td>2022-02-08T21:25:04Z<\/td>\n<td>ORG-0001<\/td>\n<td>99999934<\/td>\n<td>{&#8220;lat&#8221;:34.13801193,&#8221;lon&#8221;:-84.13371277}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"40\">\n<td>2022-02-08T21:25:04Z<\/td>\n<td>ORG-0001<\/td>\n<td>99999939<\/td>\n<td>{&#8220;lat&#8221;:34.06805801,&#8221;lon&#8221;:-84.28517914}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"41\">\n<td>2022-02-08T21:25:06Z<\/td>\n<td>ORG-0001<\/td>\n<td>90000003<\/td>\n<td>{&#8220;lat&#8221;:34.11829376,&#8221;lon&#8221;:-84.24249268}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"42\">\n<td>2022-02-08T21:25:36Z<\/td>\n<td>ORG-0001<\/td>\n<td>99999931<\/td>\n<td>{&#8220;lat&#8221;:43.05483246,&#8221;lon&#8221;:-89.44286346}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"43\">\n<td>2022-02-08T21:27:08Z<\/td>\n<td>ORG-0001<\/td>\n<td>99999935<\/td>\n<td>{&#8220;lat&#8221;:40.12084579,&#8221;lon&#8221;:-75.38314056}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"44\">\n<td>2022-02-08T21:29:28Z<\/td>\n<td>ORG-0001<\/td>\n<td>99999936<\/td>\n<td>{&#8220;lat&#8221;:40.02775955,&#8221;lon&#8221;:-82.80722046}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"45\">\n<td>2022-02-08T21:29:38Z<\/td>\n<td>ORG-0001<\/td>\n<td>99991000<\/td>\n<td>{&#8220;lat&#8221;:43.86902237,&#8221;lon&#8221;:-78.85910034}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"46\">\n<td>2022-02-08T21:30:04Z<\/td>\n<td>ORG-0001<\/td>\n<td>99999939<\/td>\n<td>{&#8220;lat&#8221;:34.06805420,&#8221;lon&#8221;:-84.28518677}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"47\">\n<td>2022-02-08T21:30:04Z<\/td>\n<td>ORG-0001<\/td>\n<td>99999934<\/td>\n<td>{&#8220;lat&#8221;:34.13801193,&#8221;lon&#8221;:-84.13371277}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"48\">\n<td>2022-02-08T21:30:06Z<\/td>\n<td>ORG-0001<\/td>\n<td>90000003<\/td>\n<td>{&#8220;lat&#8221;:34.11829376,&#8221;lon&#8221;:-84.24250031}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"49\">\n<td>2022-02-08T21:32:08Z<\/td>\n<td>ORG-0001<\/td>\n<td>99999935<\/td>\n<td>{&#8220;lat&#8221;:40.12084579,&#8221;lon&#8221;:-75.38313293}<\/td>\n<\/tr>\n<tr class=\"code-line\" dir=\"auto\" data-line=\"50\">\n<td>2022-03-12T21:32:08Z<\/td>\n<td>ORG-0001<\/td>\n<td>99999935<\/td>\n<td>{&#8220;lat&#8221;:42.12084579,&#8221;lon&#8221;:-77.38313293}<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p class=\"code-line\" dir=\"auto\" data-line=\"52\">The\u00a0<em>getassetHistory<\/em>\u00a0function listed below filters the\u00a0<em>AssetHistory<\/em>\u00a0table by the input arguments. However, it also has some logic to ignore filters if an argument is not passed to the function.<\/p>\n<pre><code class=\"code-line language-kql\" dir=\"auto\" data-line=\"54\"><code><\/code><\/code><\/pre>\n<div>\n<div style=\"background: #ffffff; overflow: auto; width: auto; border: solid gray; border-width: .1em .1em .1em .8em; padding: .2em .6em;\">\n<pre style=\"margin: 0; line-height: 125%;\">.<span style=\"color: #008800; font-weight: bold;\">create<\/span><span style=\"color: #333333;\">-<\/span><span style=\"color: #008800; font-weight: bold;\">or<\/span><span style=\"color: #333333;\">-<\/span><span style=\"color: #008800; font-weight: bold;\">alter<\/span> <span style=\"color: #008800; font-weight: bold;\">function<\/span>\r\n  <span style=\"color: #008800; font-weight: bold;\">with<\/span> (docstring <span style=\"color: #333333;\">=<\/span> <span style=\"color: #aa6600;\">\"Find Asset History of an asset\"<\/span>, skipvalidation <span style=\"color: #333333;\">=<\/span> <span style=\"color: #aa6600;\">\"true\"<\/span> )\r\n    getassetHistory(\r\n        organizationIdFilter:string,\r\n        assetIdFilter:string,\r\n        startTimeStamp:datetime,\r\n        endTimeStamp:datetime,\r\n        locationInPolygon: string<span style=\"color: #333333;\">=<\/span><span style=\"color: #aa6600;\">\"\"<\/span>)<span style=\"color: #ff0000; background-color: #ffaaaa;\">{<\/span>\r\n        AssetHistory\r\n          <span style=\"color: #333333;\">|<\/span> <span style=\"color: #008800; font-weight: bold;\">where<\/span> <span style=\"color: #008800; font-weight: bold;\">isnull<\/span>(startTimeStamp) <span style=\"color: #008800; font-weight: bold;\">or<\/span> <span style=\"color: #008800; font-weight: bold;\">isnull<\/span>(endTimeStamp)\r\n              <span style=\"color: #008800; font-weight: bold;\">or<\/span> <span style=\"color: #008800; font-weight: bold;\">TimeStamp<\/span> <span style=\"color: #008800; font-weight: bold;\">between<\/span> (startTimeStamp .. endTimeStamp)\r\n          <span style=\"color: #333333;\">|<\/span> <span style=\"color: #008800; font-weight: bold;\">where<\/span> (isempty(organizationIdFilter) <span style=\"color: #008800; font-weight: bold;\">or<\/span> organizationId <span style=\"color: #333333;\">==<\/span> organizationIdFilter)\r\n              <span style=\"color: #008800; font-weight: bold;\">and<\/span> assetId <span style=\"color: #333333;\">==<\/span> assetIdFilter\r\n          <span style=\"color: #333333;\">|<\/span> <span style=\"color: #008800; font-weight: bold;\">where<\/span> isempty(locationInPolygon)\r\n              <span style=\"color: #008800; font-weight: bold;\">or<\/span> geo_point_in_polygon(todouble(<span style=\"color: #008800; font-weight: bold;\">location<\/span>.lon), todouble(<span style=\"color: #008800; font-weight: bold;\">location<\/span>.lat), todynamic(locationInPolygon))\r\n          <span style=\"color: #333333;\">|<\/span> project\r\n                <span style=\"color: #008800; font-weight: bold;\">TimeStamp<\/span>,\r\n                assetId,\r\n                organizationId,\r\n                tostring(<span style=\"color: #008800; font-weight: bold;\">location<\/span>)\r\n      ;\r\n    <span style=\"color: #ff0000; background-color: #ffaaaa;\">}<\/span>\r\n<\/pre>\n<\/div>\n<p class=\"code-line\" dir=\"auto\" data-line=\"79\">We can use\u00a0<em>assert<\/em>\u00a0to write some tests. We know in advance the data in the table, so we know how many rows the function should return for a given set of input arguments. Here are some examples:<\/p>\n<pre><code class=\"code-line language-kql\" dir=\"auto\" data-line=\"81\"><code><\/code><\/code><\/pre>\n<div>\n<div style=\"background: #ffffff; overflow: auto; width: auto; border: solid gray; border-width: .1em .1em .1em .8em; padding: .2em .6em;\">\n<pre style=\"margin: 0; line-height: 125%;\">let testTitle <span style=\"color: #333333;\">=<\/span> <span style=\"background-color: #fff0f0;\">'getassetHistory: brief time range should return 1 row'<\/span>;\r\nlet results <span style=\"color: #333333;\">=<\/span> getassetHistory(<span style=\"background-color: #fff0f0;\">'ORG-0001'<\/span>, <span style=\"background-color: #fff0f0;\">'99999935'<\/span>, <span style=\"background-color: #fff0f0;\">'2022-02-08T21:27:00Z'<\/span>, <span style=\"background-color: #fff0f0;\">'2022-02-08T21:28:00Z'<\/span>);\r\nprint testTitle, assert(toscalar(results <span style=\"color: #333333;\">|<\/span> <span style=\"color: #008800; font-weight: bold;\">count<\/span>) <span style=\"color: #333333;\">==<\/span> <span style=\"color: #0000dd; font-weight: bold;\">1<\/span>, testTitle)\r\n\r\nlet testTitle <span style=\"color: #333333;\">=<\/span> <span style=\"background-color: #fff0f0;\">'getassetHistory: longer time range should return 3 rows'<\/span>;\r\nlet results <span style=\"color: #333333;\">=<\/span> getassetHistory(<span style=\"background-color: #fff0f0;\">'ORG-0001'<\/span>, <span style=\"background-color: #fff0f0;\">'99999935'<\/span>, <span style=\"background-color: #fff0f0;\">'2022-02-08T21:27:00Z'<\/span>, <span style=\"background-color: #fff0f0;\">'2022-03-12T21:35:00Z'<\/span>);\r\nprint testTitle, assert(toscalar(results <span style=\"color: #333333;\">|<\/span> <span style=\"color: #008800; font-weight: bold;\">count<\/span>) <span style=\"color: #333333;\">==<\/span> <span style=\"color: #0000dd; font-weight: bold;\">3<\/span>, testTitle)\r\n\r\nlet testTitle <span style=\"color: #333333;\">=<\/span> <span style=\"background-color: #fff0f0;\">'getassetHistory: unknown organizationId should return 0 rows'<\/span>;\r\nlet results <span style=\"color: #333333;\">=<\/span> getassetHistory(<span style=\"background-color: #fff0f0;\">'ORG-9999'<\/span>, <span style=\"background-color: #fff0f0;\">'2022-02-08T21:25:03Z'<\/span>, <span style=\"background-color: #fff0f0;\">'2022-02-08T22:33:06Z'<\/span>);\r\nprint testTitle, assert(toscalar(results <span style=\"color: #333333;\">|<\/span> <span style=\"color: #008800; font-weight: bold;\">count<\/span>) <span style=\"color: #333333;\">==<\/span> <span style=\"color: #0000dd; font-weight: bold;\">0<\/span>, testTitle)\r\n<\/pre>\n<\/div>\n<p class=\"code-line\" dir=\"auto\" data-line=\"95\">As you can see, each of the tests above passes a distinct set of arguments to the function, so that each test filters the table for a different time range. Each test validates that the function correctly applies the filters by verifying that it returns the correct number of rows. But it also validates that the function handles things correctly when a null argument is passed or when an argument (e.g.,\u00a0<em>locationInPolygon<\/em>) is omitted.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"97\">We can even test passing dynamic data, as in this function call, which passes a polygon object to the function:<\/p>\n<pre><code class=\"code-line language-kql\" dir=\"auto\" data-line=\"99\"><code><\/code><\/code><\/pre>\n<div>\n<div style=\"background: #ffffff; overflow: auto; width: auto; border: solid gray; border-width: .1em .1em .1em .8em; padding: .2em .6em;\">\n<pre style=\"margin: 0; line-height: 125%;\">let polygon <span style=\"color: #333333;\">=<\/span> <span style=\"color: #008800; font-weight: bold;\">dynamic<\/span>(<span style=\"color: #ff0000; background-color: #ffaaaa;\">{<\/span>\r\n        <span style=\"color: #aa6600;\">\"type\"<\/span>: <span style=\"color: #aa6600;\">\"Polygon\"<\/span>,\r\n        <span style=\"color: #aa6600;\">\"coordinates\"<\/span>: [ [\r\n            [ <span style=\"color: #333333;\">-<\/span><span style=\"color: #0000dd; font-weight: bold;\">75<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">8056640625<\/span>, <span style=\"color: #0000dd; font-weight: bold;\">40<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">052847601823984<\/span>],\r\n            [ <span style=\"color: #333333;\">-<\/span><span style=\"color: #0000dd; font-weight: bold;\">74<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">970703125<\/span>, <span style=\"color: #0000dd; font-weight: bold;\">40<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">052847601823984<\/span> ],\r\n            [ <span style=\"color: #333333;\">-<\/span><span style=\"color: #0000dd; font-weight: bold;\">74<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">970703125<\/span>, <span style=\"color: #0000dd; font-weight: bold;\">40<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">43022363450862<\/span>  ],\r\n            [ <span style=\"color: #333333;\">-<\/span><span style=\"color: #0000dd; font-weight: bold;\">75<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">8056640625<\/span>, <span style=\"color: #0000dd; font-weight: bold;\">40<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">43022363450862<\/span> ],\r\n            [ <span style=\"color: #333333;\">-<\/span><span style=\"color: #0000dd; font-weight: bold;\">75<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">8056640625<\/span>, <span style=\"color: #0000dd; font-weight: bold;\">40<\/span>.<span style=\"color: #0000dd; font-weight: bold;\">052847601823984<\/span>] ] ]\r\n      <span style=\"color: #ff0000; background-color: #ffaaaa;\">}<\/span>);\r\nlet testTitle <span style=\"color: #333333;\">=<\/span> <span style=\"background-color: #fff0f0;\">'getassetHistory: with Bounding Box polygon should return 2 rows'<\/span>;\r\nlet startTime <span style=\"color: #333333;\">=<\/span> datetime(<span style=\"color: #0000dd; font-weight: bold;\">2022<\/span><span style=\"color: #333333;\">-<\/span><span style=\"color: #0000dd; font-weight: bold;\">01<\/span><span style=\"color: #333333;\">-<\/span><span style=\"color: #0000dd; font-weight: bold;\">09<\/span>T21:<span style=\"color: #0000dd; font-weight: bold;\">32<\/span>:<span style=\"color: #0000dd; font-weight: bold;\">08<\/span>Z);\r\nlet endTime <span style=\"color: #333333;\">=<\/span> datetime(<span style=\"color: #0000dd; font-weight: bold;\">2022<\/span><span style=\"color: #333333;\">-<\/span><span style=\"color: #0000dd; font-weight: bold;\">03<\/span><span style=\"color: #333333;\">-<\/span><span style=\"color: #0000dd; font-weight: bold;\">12<\/span>T21:<span style=\"color: #0000dd; font-weight: bold;\">32<\/span>:<span style=\"color: #0000dd; font-weight: bold;\">08<\/span>Z);\r\nlet results <span style=\"color: #333333;\">=<\/span> getassetHistory(<span style=\"background-color: #fff0f0;\">'ORG-0001'<\/span>, <span style=\"background-color: #fff0f0;\">'99999935'<\/span>, startTime, endTime, polygon);\r\nprint testTitle, assert(toscalar(results <span style=\"color: #333333;\">|<\/span> <span style=\"color: #008800; font-weight: bold;\">count<\/span>) <span style=\"color: #333333;\">==<\/span> <span style=\"color: #0000dd; font-weight: bold;\">2<\/span>, testTitle)\r\n<\/pre>\n<\/div>\n<p class=\"code-line\" dir=\"auto\" data-line=\"116\">Each of the tests described above expects the data to be in a well-known state, therefore it is important to initialize the data correctly.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"118\">For our project, we created automated scripts that would recreate all the database objects; then populate the tables with test data. We run this script before each test run, so we always start with a clean database. This made the data predictable, and it made our function calls idempotent with a given set of inputs.<\/p>\n<h2 id=\"other-testing-approaches\" class=\"code-line\" dir=\"auto\" data-line=\"120\">Other Testing Approaches<\/h2>\n<p class=\"code-line\" dir=\"auto\" data-line=\"122\">Alternatively, we can test a query by using the appropriate SDK to call it from a program written in Java, .NET, or some other high-level language. This has the advantage of allowing us to write tests in a robust testing framework, such as\u00a0<a title=\"https:\/\/xunit.net\/\" href=\"https:\/\/xunit.net\/\" data-href=\"https:\/\/xunit.net\/\">xUnit<\/a>, or\u00a0<a title=\"https:\/\/junit.org\/junit5\/\" href=\"https:\/\/junit.org\/junit5\/\" data-href=\"https:\/\/junit.org\/junit5\/\">JUnit<\/a>.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"124\">However, this approach adds complexity to our tests and introduces the possibility that errors may occur in our code outside ADX. Keeping all our tests in ADX tends to reduce the amount of code we need to write and focuses the tests on the KQL functions we have written.<\/p>\n<h2 dir=\"auto\" data-line=\"124\">Contributors<\/h2>\n<p>The code in this article is based on work done by the Microsoft Commercial Software Engineering GT team. Many of the original tests were written for our customer by <a href=\"https:\/\/www.linkedin.com\/in\/braden-eriksen-1568b680\/\" target=\"_self\" rel=\"nofollow noopener noreferrer\">Braden Eriksen<\/a>.<\/p>\n<h2 id=\"conclusion\" class=\"code-line\" dir=\"auto\" data-line=\"126\">Conclusion<\/h2>\n<p class=\"code-line\" dir=\"auto\" data-line=\"128\">The KQL tests above show how to use the\u00a0<em>assert<\/em>\u00a0function to create automated Unit Tests that validate that our ADX functions are working correctly. This is a powerful tool for improving and maintaining code quality.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"130\">You can read more about this pattern at the\u00a0<a title=\"https:\/\/docs.microsoft.com\/en-us\/azure\/data-explorer\/kusto\/query\/assert-function\" href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/data-explorer\/kusto\/query\/assert-function\" data-href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/data-explorer\/kusto\/query\/assert-function\">Microsoft Documentation site<\/a>.<\/p>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Overview Our application contains many functions that return data stored in Azure Data Explorer (ADX). We wrote these functions in Kusto Query Language (KQL) and each function returns a set of data based on the arguments passed. Although developers tested these functions as they wrote them, we needed a way to validate that the functions [&hellip;]<\/p>\n","protected":false},"author":73158,"featured_media":14378,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[3354,3355,367],"class_list":["post-14374","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cse","tag-adx","tag-kql","tag-unit-testing"],"acf":[],"blog_post_summary":"<p>Overview Our application contains many functions that return data stored in Azure Data Explorer (ADX). We wrote these functions in Kusto Query Language (KQL) and each function returns a set of data based on the arguments passed. Although developers tested these functions as they wrote them, we needed a way to validate that the functions [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/14374","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/73158"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=14374"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/14374\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/14378"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=14374"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=14374"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=14374"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}