[{"data":1,"prerenderedAt":614},["ShallowReactive",2],{"navigation":3,"\u002Fprojects\u002Fautomated-testing":50,"\u002Fprojects\u002Fautomated-testing-meta":595,"\u002Fprojects\u002Fautomated-testing-surround":608},[4],{"title":5,"path":6,"stem":7,"children":8,"page":49},"Blog","\u002Fblog","blog",[9,13,17,21,25,29,33,37,41,45],{"title":10,"path":11,"stem":12},"Data Catalog 3.0: Rise of the Active Metadata Platform","\u002Fblog\u002Fdata-catalog-3","blog\u002Fdata-catalog-3",{"title":14,"path":15,"stem":16},"dbt on Databricks: Data Transformation Pipelines","\u002Fblog\u002Fdbt-on-databricks","blog\u002Fdbt-on-databricks",{"title":18,"path":19,"stem":20},"Deploying Azure Resources with VS Code","\u002Fblog\u002Fdeploying-azure-resource-with-vs-code","blog\u002Fdeploying-azure-resource-with-vs-code",{"title":22,"path":23,"stem":24},"Dynamic Management Views (DMVs)","\u002Fblog\u002Fdynamic-management-views","blog\u002Fdynamic-management-views",{"title":26,"path":27,"stem":28},"General Delta Table Processing","\u002Fblog\u002Fgeneral-delta-table","blog\u002Fgeneral-delta-table",{"title":30,"path":31,"stem":32},"Microsoft Fabric as an All-in-One Analytics Solution","\u002Fblog\u002Fmicrosoft-fabric","blog\u002Fmicrosoft-fabric",{"title":34,"path":35,"stem":36},"Orchard Core Shapes: Demystifying the View Data Model","\u002Fblog\u002Forchard-core-shapes","blog\u002Forchard-core-shapes",{"title":38,"path":39,"stem":40},"Power BI DAX Masterclass","\u002Fblog\u002Fpower-bi-dax-masterclass","blog\u002Fpower-bi-dax-masterclass",{"title":42,"path":43,"stem":44},"Power BI Incremental Refresh","\u002Fblog\u002Fpower-bi-incremental-refresh","blog\u002Fpower-bi-incremental-refresh",{"title":46,"path":47,"stem":48},"Set Power BI Row-Level Security to SAP Cost Center","\u002Fblog\u002Fpower-bi-row-level-security","blog\u002Fpower-bi-row-level-security",false,{"id":51,"title":52,"body":53,"date":586,"description":587,"extension":588,"image":586,"meta":589,"navigation":590,"path":591,"seo":592,"stem":593,"__hash__":594},"projectsContent\u002Fprojects\u002Fautomated-testing.md","Metadata-Driven Automated Testing Framework",{"type":54,"value":55,"toc":569},"minimark",[56,60,65,100,104,107,208,212,215,359,363,370,375,413,417,420,424,427,453,457,460,486,489,493,496,523,526,530,562,566],[57,58,59],"p",{},"Designed and implemented a metadata-driven automated testing module, integrated directly into the existing ADF ingestion workflow. Rather than maintaining separate test infrastructure, the framework reuses the same generalised ADF Copy pipeline and Metadata database to load test data alongside production ingestion, then invokes a dedicated Databricks Python test class to validate results against configurable test templates. Test outcomes are fully traceable via the Metadata database using the object run ID.",[61,62,64],"h2",{"id":63},"technologies-stack","Technologies & Stack",[66,67,68,76,82,88,94],"ul",{},[69,70,71,75],"li",{},[72,73,74],"strong",{},"Azure Data Factory (ADF)"," — generalised Copy Activity pipelines for source data loading",[69,77,78,81],{},[72,79,80],{},"Databricks \u002F PySpark"," — Python test class for in-platform test execution and comparison",[69,83,84,87],{},[72,85,86],{},"Delta Lake"," — target tables validated as part of the test cycle",[69,89,90,93],{},[72,91,92],{},"Metadata Database"," — test template storage, object configuration, and results audit trail",[69,95,96,99],{},[72,97,98],{},"Metadata UI"," — user-facing interface for self-service test definition without code changes",[61,101,103],{"id":102},"end-to-end-pipeline-flow","End-to-End Pipeline Flow",[57,105,106],{},"The testing framework is embedded directly into the standard ingestion pipeline, executing automatically as part of every data load cycle. The six-stage flow below illustrates how test configuration, data loading, processing, and result recording are orchestrated end-to-end:",[108,109,110,126],"table",{},[111,112,113],"thead",{},[114,115,116,120,123],"tr",{},[117,118,119],"th",{},"#",[117,121,122],{},"Stage",[117,124,125],{},"Description",[127,128,129,143,156,169,182,195],"tbody",{},[114,130,131,135,140],{},[132,133,134],"td",{},"1",[132,136,137],{},[72,138,139],{},"UI Configuration",[132,141,142],{},"User defines test templates per object\u002Fsystem in Metadata DB UI — selects predefined tests or writes custom SQL",[114,144,145,148,153],{},[132,146,147],{},"2",[132,149,150],{},[72,151,152],{},"ADF Pipeline Trigger",[132,154,155],{},"Generalised ADF workflow pipeline is triggered for data ingestion; test templates are read from Metadata DB",[114,157,158,161,166],{},[132,159,160],{},"3",[132,162,163],{},[72,164,165],{},"Source Data Load",[132,167,168],{},"ADF Copy Activity loads source data into staging using the same ingestion pipeline — no duplicate infrastructure",[114,170,171,174,179],{},[132,172,173],{},"4",[132,175,176],{},[72,177,178],{},"Delta Processing",[132,180,181],{},"Data is processed into Delta Lake tables by the Delta Processing Framework",[114,183,184,187,192],{},[132,185,186],{},"5",[132,188,189],{},[72,190,191],{},"Databricks Test Class",[132,193,194],{},"Python test class is invoked; compares ingested staging data against Delta table results per test template",[114,196,197,200,205],{},[132,198,199],{},"6",[132,201,202],{},[72,203,204],{},"Result Recording",[132,206,207],{},"Test outcomes (pass\u002Ffail, counts, diffs) are written back to Metadata DB with the object run ID for full traceability",[61,209,211],{"id":210},"test-template-library","Test Template Library",[57,213,214],{},"Users configure test templates per object or system through the Metadata UI. Each template specifies which test type to apply, the target column or expression, and any threshold or comparison parameters. The framework ships with a library of predefined test types covering the most common data quality checks, and also supports fully custom SQL-based tests for advanced scenarios.",[108,216,217,229],{},[111,218,219],{},[114,220,221,224,226],{},[117,222,223],{},"Test Type",[117,225,125],{},[117,227,228],{},"Example Configuration",[127,230,231,247,262,277,296,314,329,344],{},[114,232,233,238,241],{},[132,234,235],{},[72,236,237],{},"row_count",[132,239,240],{},"Compares total row count between source and Delta table for the current load",[132,242,243],{},[244,245,246],"em",{},"tolerance: 0 (exact match)",[114,248,249,254,257],{},[132,250,251],{},[72,252,253],{},"not_null",[132,255,256],{},"Asserts that a specified column contains no NULL values after processing",[132,258,259],{},[244,260,261],{},"column: customer_id",[114,263,264,269,272],{},[132,265,266],{},[72,267,268],{},"freshness",[132,270,271],{},"Validates that the most recent record timestamp is within an expected recency window",[132,273,274],{},[244,275,276],{},"max_age_hours: 24, column: updated_at",[114,278,279,284,287],{},[132,280,281],{},[72,282,283],{},"grouped_count",[132,285,286],{},"Compares row counts per group (e.g. by date or category) between source and target",[132,288,289],{},[244,290,291,292],{},"group_by: ",[293,294,295],"span",{},"region, status",[114,297,298,303,306],{},[132,299,300],{},[72,301,302],{},"uniqueness",[132,304,305],{},"Checks that a column or composite key contains no duplicate values",[132,307,308],{},[244,309,310,311],{},"columns: ",[293,312,313],{},"order_id, line_no",[114,315,316,321,324],{},[132,317,318],{},[72,319,320],{},"referential",[132,322,323],{},"Validates foreign key relationships between Delta tables post-ingestion",[132,325,326],{},[244,327,328],{},"ref_table: dim_customer",[114,330,331,336,339],{},[132,332,333],{},[72,334,335],{},"custom_sql",[132,337,338],{},"User-defined SQL query; result set is compared for equality against a reference sample",[132,340,341],{},[244,342,343],{},"sql: SELECT ... FROM ...",[114,345,346,351,354],{},[132,347,348],{},[72,349,350],{},"sample_equality",[132,352,353],{},"Extracts a deterministic sample from source and Delta table and compares values row-by-row",[132,355,356],{},[244,357,358],{},"sample_pct: 5, seed: 42",[61,360,362],{"id":361},"databricks-test-class-design","Databricks Test Class Design",[57,364,365,366,369],{},"The core of the testing module is a Python class running on Databricks. It is invoked by the ADF pipeline as a notebook activity after ingestion and Delta processing complete. The class is structured to mirror the architecture of the ",[72,367,368],{},"Delta Processing Framework",", with the Metadata database as the single source of truth for test configuration.",[371,372,374],"h3",{"id":373},"responsibilities","Responsibilities",[66,376,377,383,389,395,401,407],{},[69,378,379,382],{},[72,380,381],{},"Read test templates"," for the current object and run ID from the Metadata database",[69,384,385,388],{},[72,386,387],{},"Load ingested staging data"," (the source copy produced by ADF) into a Spark DataFrame",[69,390,391,394],{},[72,392,393],{},"Load Delta table data"," for the same object and load window",[69,396,397,400],{},[72,398,399],{},"Execute each configured test"," by dispatching to the appropriate test method based on template type",[69,402,403,406],{},[72,404,405],{},"Evaluate pass\u002Ffail"," using configured thresholds, SQL comparisons, or sample equality checks",[69,408,409,412],{},[72,410,411],{},"Write results to Metadata DB"," with test name, status, expected vs. actual values, and the unique object run ID",[371,414,416],{"id":415},"custom-sql-sample-equality-tests","Custom SQL & Sample Equality Tests",[57,418,419],{},"Users who require tests beyond the predefined library can define a custom SQL query directly in the Metadata UI. The test class executes the query against both the staging data and the Delta table, then compares the result sets for equality. For sample equality tests, a deterministic sample is drawn from both datasets using a configurable percentage and random seed, enabling lightweight but meaningful comparison of data content without full table scans.",[61,421,423],{"id":422},"adf-pipeline-integration","ADF Pipeline Integration",[57,425,426],{},"A key design decision was to reuse the existing generalised ADF Copy pipeline for test data loading rather than building separate test infrastructure. During each ingestion run, the ADF workflow reads active test templates from the Metadata database and uses the same Copy Activity configuration to load a snapshot of the source data into a staging area. This approach delivers several advantages:",[66,428,429,435,441,447],{},[69,430,431,434],{},[72,432,433],{},"Zero additional pipeline maintenance"," — no separate test pipelines to manage or version",[69,436,437,440],{},[72,438,439],{},"Consistent data snapshots"," — test data is loaded in the same run as production data, eliminating timing discrepancies",[69,442,443,446],{},[72,444,445],{},"Governed by metadata"," — enabling or disabling tests for an object requires only a metadata configuration change",[69,448,449,452],{},[72,450,451],{},"Reuses existing monitoring"," — ADF run history and alerting covers both ingestion and test data loads",[61,454,456],{"id":455},"self-service-configuration-via-metadata-ui","Self-Service Configuration via Metadata UI",[57,458,459],{},"Data engineers and analysts configure tests through the Metadata UI without writing or deploying code. For each registered object or system, the user can:",[66,461,462,468,474,480],{},[69,463,464,467],{},[72,465,466],{},"Select one or more predefined test types"," from the template library",[69,469,470,473],{},[72,471,472],{},"Specify columns, thresholds, grouping keys, or freshness windows"," per test",[69,475,476,479],{},[72,477,478],{},"Write a custom SQL query"," and designate it as the test expression",[69,481,482,485],{},[72,483,484],{},"Activate or deactivate individual tests"," per object without touching pipeline code",[57,487,488],{},"This self-service model significantly reduces the time to add data quality coverage for new objects and empowers teams outside of core engineering to maintain and extend test suites independently.",[61,490,492],{"id":491},"traceability-audit-trail","Traceability & Audit Trail",[57,494,495],{},"Every test execution is recorded in the Metadata database with the following attributes, linked to the originating pipeline run via the object run ID:",[66,497,498,503,508,513,518],{},[69,499,500],{},[72,501,502],{},"Object and system identifier",[69,504,505],{},[72,506,507],{},"Test template name and type",[69,509,510],{},[72,511,512],{},"Expected value and actual observed value",[69,514,515],{},[72,516,517],{},"Pass \u002F fail status and failure reason",[69,519,520],{},[72,521,522],{},"Execution timestamp and Databricks run reference",[57,524,525],{},"This audit trail enables teams to query test history per object, identify recurring failures, and correlate data quality issues with specific ingestion runs — providing full end-to-end observability across the pipeline.",[61,527,529],{"id":528},"design-principles-benefits","Design Principles & Benefits",[66,531,532,538,544,550,556],{},[69,533,534,537],{},[72,535,536],{},"Infrastructure reuse:"," test data loading piggybacks on the production ADF Copy pipeline, minimising operational overhead",[69,539,540,543],{},[72,541,542],{},"Metadata-driven configuration:"," tests are defined entirely in the Metadata DB — no code deployment required to add or change tests",[69,545,546,549],{},[72,547,548],{},"Extensibility:"," the predefined test library is supplemented by custom SQL, making the framework applicable to any validation scenario",[69,551,552,555],{},[72,553,554],{},"Full traceability:"," every test result is recorded with its object run ID, enabling precise root-cause analysis",[69,557,558,561],{},[72,559,560],{},"Tight integration:"," the testing framework operates as a natural extension of the Delta Processing Framework, sharing the same metadata, run context, and logging patterns",[61,563,565],{"id":564},"outcome-impact","Outcome & Impact",[57,567,568],{},"The framework introduced automated, repeatable data quality validation across all pipeline-managed objects with no additional pipeline infrastructure. Data engineers can onboard new tests in minutes via the UI, and the full audit trail in the Metadata database provides immediate visibility into data quality trends over time. The reuse of existing ADF pipelines and the shared metadata model ensured the testing module could be adopted incrementally across all existing objects without rework.",{"title":570,"searchDepth":571,"depth":571,"links":572},"",2,[573,574,575,576,581,582,583,584,585],{"id":63,"depth":571,"text":64},{"id":102,"depth":571,"text":103},{"id":210,"depth":571,"text":211},{"id":361,"depth":571,"text":362,"children":577},[578,580],{"id":373,"depth":579,"text":374},3,{"id":415,"depth":579,"text":416},{"id":422,"depth":571,"text":423},{"id":455,"depth":571,"text":456},{"id":491,"depth":571,"text":492},{"id":528,"depth":571,"text":529},{"id":564,"depth":571,"text":565},null,"Built a data quality testing module integrated directly into the ADF ingestion workflow. Reuses the same generalised Copy pipeline and Metadata database — no separate test infrastructure. A Databricks Python test class executes row count, nullability, freshness, uniqueness, referential, and custom SQL checks after every ingestion run. All results are recorded against the object run ID for full traceability. Tests are configured via a self-service Metadata UI without code deployment.","md",{},true,"\u002Fprojects\u002Fautomated-testing",{"title":52,"description":587},"projects\u002Fautomated-testing","b-giikS438-7fnBizk-ARlGrsXCFx9MBZmcNfrSYk-U",{"id":596,"title":52,"date":597,"description":587,"extension":598,"image":599,"meta":600,"stem":593,"tags":601,"url":119,"__hash__":607},"projects\u002Fprojects\u002Fautomated-testing.yml","2024-01-01","yml","\u002Fprojects\u002Ftesting.png",{},[602,603,604,605,606],"Data Quality","Azure Data Factory","Databricks","Python","Testing","K0X05j3ZpJh7q8M1vnfV8C7zkfeiuDtIKRkUvmCPNiA",[586,609],{"title":610,"path":611,"stem":612,"description":613,"children":-1},"Data Governance & Script Generation Framework","\u002Fprojects\u002Fdata-governance","projects\u002Fdata-governance","Designed and delivered an end-to-end framework for generating and executing Snowflake and Databricks management scripts from a centralised template repository. Implemented as stored procedures (Snowflake) and an OOP TemplateGenerator class (Databricks\u002FPython). Uses owner's-rights execution and Service Principal privilege escalation so no human user holds GRANT or CREATE USER rights on any platform. A self-documenting help system is built directly into the interface.",1783165025449]