[{"content":"A week ago, OWASP released the first version of their Top 10 for LLM Applications 2026 whitepaper. My first thought: finally — an official, comprehensive recommendation on AI security.\nWhat is it? The OWASP Top 10 for LLM Applications 2026 is a guide to the most critical security risks facing applications powered by large language models.\nThink of it as the classic OWASP Top 10 for web security — but built specifically for the AI/LLM era.\nLike the technology it covers, this list is a product of the community. It has been shaped by developers, data scientists, and security practitioners who brought their judgment and, this year, their incident records to the work.\nThe guide covers:\nDescriptions — a precise technical explanation of each risk, including all the ways it can surface, not just the obvious ones. Common examples of Risk of the most critical LLM application risks. Example attack scenarios with actionable mitigations for each risk. Prevention and Mitigation Strategies — concrete, actionable steps for each risk, mapped to industry frameworks. Who is it for? The whitepaper is written for a wide audience:\nDevelopers, architects and end-users building LLM-powered applications Security teams who need to assess and harden AI systems In short: if you work in IT and AI touches your work in any way, this is for you.\nWhat changed from 2025 to 2026? The order moved more than in previous years, and the moves reflect the gap between what practitioners believed was risky and what the incident record actually confirms.\nThe most notable shifts:\nExcessive Agency climbed to #3 — the biggest move on the list. Both the community vote and real-world incidents agree: agentic deployments are where damage is landing. Unbounded Consumption rose four places — practitioners now weigh resource and cost exhaustion more seriously than before. Improper Output Handling fell the furthest — from #5 to #10. Prompt Injection held #1, and Sensitive Information Disclosure held #2 — the one spot where belief and evidence simply agree. System Prompt Leakage was renamed to Hidden Context Exposure — a broader framing for the same underlying failure: trusting information that should never have been reachable. Several entries also grew in scope. Prompt Injection now covers cross-modal attacks — instructions hidden inside images or audio. Supply Chain now accounts for the trust failure when a model artifact is not what it claims to be. Data and Model Poisoning now absorbs fine-tuning subversion. Improper Output Handling now spans the insecure code that LLM assistants generate at scale.\nOne boundary also became clearer with this release. This list covers the risk when the model is a component inside your application. The moment that model becomes an actor — with tools, memory between sessions, and downstream consequences — the risk moves to the OWASP Agentic Top 10. Neither list covers that ground alone.\nSummary \u0026ldquo;Stop trying to build a model that cannot be fooled. Build the system around it, so that when the model is fooled, and it will be, nothing important breaks.\u0026rdquo;\n— Letter from the Project Leads, OWASP Top 10 for LLM Applications 2026\nThe AI landscape is evolving fast — and so are the attack surfaces. Having a structured, community-backed security guide is exactly what the industry needs right now. Whether you are building LLM-based products or managing the security of AI-integrated systems, reading this whitepaper is time well spent.\nI encourage everyone in IT to read it. It is available for free — there is no reason to skip it.\n📎 Resources OWASP Top 10 for LLM Applications 2026 – Download the whitepaper ","permalink":"https://rolandbende.com/posts/owasp-top-10-llm-applications-2026/","summary":"\u003cp\u003eA week ago, OWASP released the first version of their \u003cstrong\u003eTop 10 for LLM Applications 2026\u003c/strong\u003e whitepaper. My first thought: \u003cem\u003efinally\u003c/em\u003e — an official, comprehensive recommendation on AI security.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"what-is-it\"\u003eWhat is it?\u003c/h2\u003e\n\u003cp\u003eThe OWASP Top 10 for LLM Applications 2026 is a guide to the most critical security risks facing applications powered by large language models.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eThink of it as the classic OWASP Top 10 for web security — but built specifically for the AI/LLM era.\u003c/strong\u003e\u003c/p\u003e","title":"OWASP Top 10 for LLM Applications 2026"},{"content":"Nowdays I am often surprised to find that team members pass sensitive data directly to AI agents exactly as it is.\nI believe that in the AI ​​era, handling PII within spreadsheets is more important than ever.\nWhat is PII? Personally Identifiable Information (PII) is information that can be used to distinguish or trace an individual’s identity, either alone or when combined with other information that is linked or linkable to a specific individual.\nExamples of PII:\nNames, addresses, phone numbers, email addresses, and social security numbers IP addresses, login credentials, and device IDs Biometric data, such as facial recognition and fingerprints Health records, financial information, and employment history Online activities, such as browsing history and search queries Sharing PII can have its benefits, as it allows businesses to tailor products and services to the wants and needs of their customers, such as serving up more relevant search results in navigation apps. However, the growing storehouses of PII accumulated by organizations attract the attention of cybercriminals.\nHackers steal PII to commit identity theft, sell it on the black market, or hold it captive via ransomware. According to IBM’s Cost of a Data Breach 2024 report, the average cost of a data breach caused by a ransomware attack was USD 5.68 million. Individuals and information security professionals must navigate a complex IT and legal landscape to maintain data privacy in the face of these attacks.\nThat is why I came up with a solution to make hashing PII data in Google Sheets easier.\nWhat problem does it solve? Google Sheets doesn\u0026rsquo;t have a built-in SHA-256 function. If you\u0026rsquo;ve ever tried to generate SHA-256 hashes directly inside a spreadsheet, you know it\u0026rsquo;s surprisingly not a default feature.\nHow it works? Instead of exporting your data to another tool, you can actually create your own custom formula with a few lines of Google Apps Script — and use it just like any built-in Sheets function.\nOnce you add the script to your spreadsheet, the custom =SHA256() formula becomes available just like any built-in function.\n=SHA256(A1) That\u0026rsquo;s it. Point it at any cell and it returns the SHA-256 hash of its value.\nI wrote a quick walkthrough on how to set it up. The full guide, code and README are available in the public GitHub repository.\nSummary I originally built this for a marketing data use case: hashing email addresses and other PII before passing them to external tools. In a world where LLMs and AI pipelines process more and more personal data, hashing at the spreadsheet level is a simple but meaningful step to protect sensitive information before it leaves your control.\nIf you work with audience data, contact lists or any PII in Google Sheets — this is a small addition that can make a real difference.\n📎 Resources GitHub Repository: google-sheet-sha256 Guidance on the Protection of Personally Identifiable Information (PII) IBM - What is PII? ","permalink":"https://rolandbende.com/posts/google-sheets-sha256-custom-formula/","summary":"\u003cp\u003eNowdays I am often surprised to find that team members pass sensitive data directly to AI agents exactly as it is.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eI believe that in the AI ​​era, handling PII within spreadsheets is more important than ever.\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"custom-details\"\u003e\n    \u003cdetails\u003e\n        \u003csummary\u003eWhat is PII?\u003c/summary\u003e\n        \u003cdiv class=\"custom-details__inner\"\u003e\u003cp\u003ePersonally Identifiable Information (PII) is information that can be used to distinguish or trace an individual’s identity, either alone or when combined with other information that is linked or linkable to a specific individual.\u003c/p\u003e","title":"Google Sheets: SHA-256 Custom Formula with Google Apps Script"},{"content":" I am Roland Bende a Marketing Technology Specialist based in Budapest, Hungary.\nI'm a dedicated technical leader and software engineer who specializes in building marketing data pipelines.\nEver since I was a kid I\u0026rsquo;ve been obsessed with computers. It all started with gaming on a C64 which eventually led me to learn programming. The next pivotal moment was the arrival of the Internet. I joined the digital world in the era of dial-up connections, back when Netscape was our window to the web. I will never forget the distinctive screeching sound of the modem handshake and the Netscape\u0026rsquo;s lighthouse icon.\nThat spark turned into a career spanning over 20 years, during which I’ve actively participated in \u0026lsquo;building the internet.\u0026rsquo; I initially started with the visual side of web design, but eventually returned to my engineering roots. As my expertise grew, I transitioned from writing code to orchestrating complete web development projects and leading technical teams.\nI believe my \u0026lsquo;inner architect\u0026rsquo; was founded by my early childhood fascination with LEGO. This hobby trained my brain to visualize complex systems rapidly and assemble them mentally before laying a single brick, or writing a line of code. I carry this disciplined mindset into my work today: I am a \u0026lsquo;by the book\u0026rsquo; professional who relies on structured, critical thinking and strict adherence to standards.\nI am driven by an insatiable curiosity. I always need to understand the \u0026lsquo;why\u0026rsquo; and \u0026lsquo;how\u0026rsquo; behind things.\nThis trait transformed my approach to web development: it wasn\u0026rsquo;t enough to just build a website! I needed to validate its utility. That quest for answers led me to Google Analytics, which opened the door to the entire Google ecosystem: from Search Console and Google Tag Manager via Google Ads to BigQuery and DataForm.\nToday, I apply this holistic view to engineer comprehensive online ecosystems designed to deliver measurable business results.\nI am actively developing my knowledge in the field of IT security because I feel that the AI era has lowered the barrier to entry for IT — allowing almost anyone, regardless of prior training, to try their hand at various IT areas in both personal and corporate settings — while IT itself represents a significant business risk.\nIf I had to distill everything I believe in into a single thought, it would be this:\n\u0026ldquo;To leave the world a little better than you found it. That\u0026rsquo;s the best a man can ever do.\u0026rdquo;\nI fully agree with this and follow it as a guiding principle in both my personal life and my work.\n","permalink":"https://rolandbende.com/about/","summary":"\u003cdiv class=\"about-hero\"\u003e\n  \u003cimg src=\"/images/br-avatar.png\" alt=\"Roland Bende\" class=\"about-avatar\"\u003e\n  \u003cdiv class=\"about-hero-intro\"\u003e\n    \u003cp\u003eI am Roland Bende a Marketing Technology Specialist based in Budapest, Hungary.\u003c/p\u003e\n    \u003cp\u003e\u003cstrong\u003eI'm a dedicated technical leader and software engineer who specializes in building marketing data pipelines.\u003c/strong\u003e\u003c/p\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\u003cp\u003eEver since I was a kid I\u0026rsquo;ve been obsessed with computers. It all started with gaming on a \u003ccode\u003eC64\u003c/code\u003e which eventually led me to learn programming. The next pivotal moment was the arrival of the Internet. I joined the digital world in the era of dial-up connections, back when Netscape was our window to the web. I will never forget the distinctive screeching sound of the modem handshake and the Netscape\u0026rsquo;s lighthouse icon.\u003c/p\u003e","title":"About"},{"content":"I\u0026rsquo;ve just published my new official WordPress plugin: VerCheck API. It\u0026rsquo;s a small, practical REST API plugin that exposes your WordPress installation\u0026rsquo;s version data over HTTP — so you can query it from anywhere without logging into the admin dashboard.\nWhat problem does it solve? If you manage more than one WordPress site, keeping track of outdated core versions, plugins and themes gets tedious quickly. Without having WP-CLI you have only a few options. The WordPress dashboard only helps if you\u0026rsquo;re already inside it. So if you\u0026rsquo;re running on shared hosting, tools like WP-CLI are often unavailable and that\u0026rsquo;s a problem.\nVerCheck API solves this by giving you a simple, authenticated HTTP endpoint you can poll remotely — from a monitoring script, a CI/CD pipeline, or even a quick curl command.\nI personally find it most useful in two situations:\nOn shared hosting environments where WP-CLI isn\u0026rsquo;t an option and you still need automated version checks. Before taking on a support contract for a client site. A quick call to the audit endpoint gives me a complete inventory: WordPress core version, all installed themes and plugins, their versions and which ones are outdated. No wp-admin access needed beyond the API token. How it works? The plugin exposes three endpoints, all protected by a Bearer token:\n/wp-json/vercheck-api/v1/status — returns only the items that have available updates. Ideal for lightweight monitoring alerts. /wp-json/vercheck-api/v1/audit — returns a full inventory of everything installed: core, all themes and plugins with their current versions, available updates and active status. /wp-json/vercheck-api/v1/environment — returns server runtime versions: PHP, MySQL, WordPress and the plugin itself. All responses include a unique X-Request-ID header for traceability.\nYou could find the full endpoint documentation and response examples on the official plugin page.\nSummary For me VerCheck API fills a small but real gap. WordPress doesn\u0026rsquo;t have a native way to expose version data over an authenticated HTTP API, and most existing solutions are either overkill or require server-level access.\nIf you or your clients run WordPress on shared hosting, or if you simply want a clean way to audit a site before you start supporting it — VerCheck API is exactly for that.\nThe plugin is free, open-source and available directly from the WordPress Plugin Directory. I\u0026rsquo;d love to hear how you use it!\n📎 Resources VerCheck API – WordPress Plugin Directory Plugin Support Forum VerCheck API WordPress Plugin - Github repo ","permalink":"https://rolandbende.com/posts/vercheck-api-wordpress-plugin/","summary":"\u003cp\u003eI\u0026rsquo;ve just published my new official WordPress plugin: \u003cstrong\u003e\u003ca href=\"https://wordpress.org/plugins/vercheck-api/\"\u003eVerCheck API\u003c/a\u003e\u003c/strong\u003e. It\u0026rsquo;s a small, practical REST API plugin that exposes your WordPress installation\u0026rsquo;s version data over HTTP — so you can query it from anywhere without logging into the admin dashboard.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"what-problem-does-it-solve\"\u003eWhat problem does it solve?\u003c/h2\u003e\n\u003cp\u003eIf you manage more than one WordPress site, keeping track of outdated core versions, plugins and themes gets tedious quickly. Without having \u003ccode\u003eWP-CLI\u003c/code\u003e you have only a few options. The WordPress dashboard only helps if you\u0026rsquo;re already inside it. So if you\u0026rsquo;re running on \u003cstrong\u003eshared hosting\u003c/strong\u003e, tools like \u003ccode\u003eWP-CLI\u003c/code\u003e are often unavailable and that\u0026rsquo;s a problem.\u003c/p\u003e","title":"VerCheck API: A WordPress Plugin for Remote Version Monitoring"},{"content":"The fundamental problem was the sheer volume of news in the world. Keeping up with it daily took up a lot of time, yet I still wanted to stay informed. My solution was to track the most popular news stories using Google Trends and have AI read and summarize them. Every day at noon I receive a summary of the top 10 latest stories, complete with links to the original articles should I wish to read further. This way I cut down on \u0026ldquo;mental junk food\u0026rdquo; — managing the flood of clickbait and fake news — while staying up-to-date using trending data.\nWhat problem does it solve? The real issue isn\u0026rsquo;t the lack of news — it\u0026rsquo;s the opposite. There\u0026rsquo;s too much of it, and most of it is noise. Traditional news consumption is passive: you open an app, get hit with whatever the algorithm pushes, and lose 30 minutes without learning much.\nWhat if instead of chasing news, you let trending data tell you what actually matters right now — and have AI do the reading for you?\nThat\u0026rsquo;s the core idea. Instead of browsing manually, the workflow:\nFetches what people are actually searching for in real time via Google Trends Looks up relevant news articles from trusted RSS feeds Summarizes everything into a clean HTML email delivered at noon The N8N workflow The workflow is intentionally simple — 6 nodes, one linear flow.\nThe big picture Schedule Trigger (12:00) → AI Agent → Mailgun (email delivery) The AI Agent has two tools available: one to fetch trending topics, one to read RSS feeds. Everything else is handled by the LLM.\nNode breakdown 1.) Schedule Trigger\nFires once a day at noon. No manual intervention needed.\n2.) Google Gemini Chat Model\nPowers the AI Agent. I chose Google Gemini over OpenAI for cost reasons — for a daily personal automation, it\u0026rsquo;s more than capable.\n3.) SerpAPI – Google Trends\nThis is the data source for what\u0026rsquo;s trending. It calls the SerpAPI google_trends_trending_now endpoint, scoped to Hungary (geo=HU). The agent calls this tool first — before doing anything else.\nGET https://serpapi.com/search ?engine=google_trends_trending_now \u0026amp;geo=HU \u0026amp;hl=hu \u0026amp;api_key=... 4.) WebSearch (RSS Fetcher)\nA simple HTTP request tool. The agent uses it to GET the content of RSS feeds from Hungarian news sources and parse the XML to find articles matching the trending topics.\n5.) AI Agent\nThe brain of the workflow. It runs a strict 3-step sequence:\nCall google_trends tool — get the raw list of trending topics For each of the top 10 topics — determine category, pick the right RSS feed, fetch it, extract the matching article link Synthesize everything into an HTML newsletter Trimmed system prompt (the agent\u0026rsquo;s instructions) # Role You are an expert Trend Topic Newsletter Editor. # RESOURCES: RSS Feed Database You have access to the following direct RSS feeds: - General: https://index.hu/24ora/rss/ - General/Politics: https://hvg.hu/rss - General/Politics: https://telex.hu/rss - Economy: https://www.portfolio.hu/rss/all.xml - Sports/Football: https://feeds.bbci.co.uk/sport/football/rss.xml - IT security: https://nki.gov.hu/it-biztonsag/hirek/feed/ # (+ more sources) # EXECUTION ORDER 1. FIRST: Call the google_trends tool — get trending topics in Hungary 2. SECOND: For each of the top 10 topics: - Determine category (Sport, Politics, Economy...) - Fetch the most relevant RSS feed URL - Parse the XML, find a matching \u0026lt;item\u0026gt; - Extract the \u0026lt;link\u0026gt; 3. THIRD: Synthesize into HTML output # Output Structure - \u0026lt;h1\u0026gt; header with current date - \u0026lt;p\u0026gt;\u0026lt;i\u0026gt; one-sentence hook about the day\u0026#39;s mood - For each topic: \u0026lt;h3\u0026gt; topic name, \u0026lt;ul\u0026gt; with summary, background context, and source link 6.) Mailgun\nSends the final HTML email to my inbox every day at noon. Subject line includes the current date via an N8N expression:\nTrending topics - {{ $now.toFormat(\u0026#39;yyyy.MM.dd\u0026#39;) }} Understanding prompt levels: system vs. user vs. assistant When you interact with an LLM via an API — or configure one inside a tool like N8N — you\u0026rsquo;re actually sending a structured list of messages, each with a defined role. According to the OpenAI documentation, the most common roles are:\nRole Who it represents When it runs system The developer / application Once, at the start — sets the rules user The human sending a message Every turn — the actual request assistant The model\u0026rsquo;s previous replies Used in multi-turn conversations for context tool The result of a function/tool call After the agent calls an external tool The system prompt is fundamentally different from the others. While a user message is what you type into a chat box, the system message is the invisible layer that runs before everything else. It defines the model\u0026rsquo;s role, constraints, tone, and behaviour for the entire conversation.\nThink of it this way: the user prompt is the question, the system prompt is the job description that determines how the question will be answered.\nIn the workflow above, the system prompt is where I define the agent\u0026rsquo;s persona (\u0026ldquo;You are an expert Trend Topic Newsletter Editor\u0026rdquo;), its available data sources (the RSS feed list), the strict execution order it must follow, and the exact HTML output format it must produce. None of that would be possible to control reliably through a user message alone.\nThis is also why the system prompt deserves careful engineering. A well-written system prompt is the difference between an agent that behaves predictably and one that hallucinates, skips steps, or ignores your formatting rules. In my case, the MANDATORY EXECUTION ORDER section is the most critical part — without it, the agent would sometimes skip fetching RSS feeds and just make up article links.\nYou could find more information about message roles and prompt construction in the official OpenAI text generation guide.\nSummary I\u0026rsquo;ve been running this workflow for a while now and it genuinely changed how I consume news. The key insight is that Google Trends is a much better editorial filter than any news algorithm — it reflects what real people are curious about right now, not what an ad-driven platform wants you to click.\nIf you\u0026rsquo;re spending too much time on news and not feeling more informed for it — automating a daily summary like this is one of the most practical things you can build with N8N and an LLM.\nThe whole setup takes an afternoon to put together, costs almost nothing to run, and delivers something genuinely useful every day.\n📎 Resources N8N – Workflow Automation SerpAPI – Google Trends API OpenAI – Text Generation Guide ","permalink":"https://rolandbende.com/posts/daily-ai-newsletter-n8n/","summary":"\u003cp\u003eThe fundamental problem was the sheer volume of news in the world. Keeping up with it daily took up a lot of time, yet I still wanted to stay informed. My solution was to track the most popular news stories using \u003cstrong\u003eGoogle Trends\u003c/strong\u003e and have \u003cstrong\u003eAI read and summarize them\u003c/strong\u003e. Every day at noon I receive a summary of the top 10 latest stories, complete with links to the original articles should I wish to read further. This way I cut down on \u0026ldquo;mental junk food\u0026rdquo; — managing the flood of clickbait and fake news — while staying up-to-date using trending data.\u003c/p\u003e","title":"Daily AI Newsletter with N8N: How I Use Google Trends and LLMs to Stay Informed"},{"content":"Recently I\u0026rsquo;ve created a BigQuery related Python package: python-bigquery-migrations. It\u0026rsquo;s a lightweight CLI tool that brings database-style migrations to Google BigQuery — so you can create, version and manage your datasets and table schemas the same way you manage your application code.\nWhat problem does it solve? If you\u0026rsquo;ve ever worked on a project where BigQuery schemas were created by hand, copy-pasted between environments, or documented only in someone\u0026rsquo;s head — you know how quickly things go wrong. There\u0026rsquo;s no consistent way to track what changed, when, and why.\nMigrations are like version control for your database. They let you define your datasets and table schemas as code, keep them in source control, and apply them in a repeatable, predictable way across environments.\nThis is the same principle that tools like Laravel migrations or Flyway apply to traditional relational databases — but purpose-built for BigQuery.\nHow it works? 1. Install the package from PyPI pip install bigquery-migrations 2. Create your first migration file A migration file goes in the migrations/ subdirectory. The naming convention combines a timestamp with a descriptive name:\nmigrations/2024_12_01_120000_create_users_table.py Each migration file contains a class with two methods — up() to apply the change and down() to reverse it:\nfrom google.cloud import bigquery from bigquery_migrations import Migration class CreateUsersTable(Migration): def up(self): table_id = \u0026#34;your_project.your_dataset.users\u0026#34; schema = [ bigquery.SchemaField(\u0026#34;id\u0026#34;, \u0026#34;INTEGER\u0026#34;, mode=\u0026#34;REQUIRED\u0026#34;), bigquery.SchemaField(\u0026#34;email\u0026#34;, \u0026#34;STRING\u0026#34;, mode=\u0026#34;REQUIRED\u0026#34;), bigquery.SchemaField(\u0026#34;created_at\u0026#34;, \u0026#34;TIMESTAMP\u0026#34;, mode=\u0026#34;NULLABLE\u0026#34;), ] table = bigquery.Table(table_id, schema=schema) self.client.create_table(table) def down(self): table_id = \u0026#34;your_project.your_dataset.users\u0026#34; self.client.delete_table(table_id, not_found_ok=True) Having the schema definition inside the migration file is the real value. Your table structure lives in code, in version control, alongside your application — not scattered across ad hoc scripts or someone\u0026rsquo;s memory.\n3. Run your migrations Use this single command:\nbigquery-migrations run That\u0026rsquo;s it. The CLI picks up your migration files, checks which ones have already been applied, and runs the pending ones in order. No manual SQL execution, no copy-pasting schema definitions between environments.\nFor the full list of available commands and configuration options visit the official package page on PyPI.\nSummary I built this because I kept running into the same problem on data engineering projects: a random team gets assembled, stakeholders change, and ad hoc modifications pile up on the BigQuery database — with no standard process, no documentation and no clear history of what changed or why. The existing solutions were either too heavy or not designed with BigQuery in mind, so I decided to build something purpose-fit.\nIf you work with BigQuery and want a clean, repeatable way to manage your schemas — python-bigquery-migrations is exactly for that.\nThe package is free, open-source and available on PyPI.\n📎 Resources python-bigquery-migrations on PyPI GitHub Repository ","permalink":"https://rolandbende.com/posts/python-bigquery-migrations/","summary":"\u003cp\u003eRecently I\u0026rsquo;ve created a BigQuery related Python package: \u003cstrong\u003e\u003ca href=\"https://pypi.org/project/bigquery-migrations/\"\u003epython-bigquery-migrations\u003c/a\u003e\u003c/strong\u003e. It\u0026rsquo;s a lightweight CLI tool that brings database-style migrations to \u003cstrong\u003eGoogle BigQuery\u003c/strong\u003e — so you can create, version and manage your datasets and table schemas the same way you manage your application code.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"what-problem-does-it-solve\"\u003eWhat problem does it solve?\u003c/h2\u003e\n\u003cp\u003eIf you\u0026rsquo;ve ever worked on a project where BigQuery schemas were created by hand, copy-pasted between environments, or documented only in someone\u0026rsquo;s head — you know how quickly things go wrong. There\u0026rsquo;s no consistent way to track what changed, when, and why.\u003c/p\u003e","title":"python-bigquery-migrations: Version Control for Your BigQuery Schemas"},{"content":"In this advanced level guide I’ll introduce how to create \u0026amp; run multiple Server-side GTM Unit Tests. I will share some practical tricks with examples on how to overcome unexpected challenges when your Custom Client Template Unit tests try to claim the request right after each other when we run them in batch. For the better understanding, I\u0026rsquo;ve wrapped the technical parts in a fictional business story.\nIf you’re not interested in the story nor the planning or custom client coding part jump right to the section on Testing: Unit Testing our Client Template.\nBefore we begin it\u0026rsquo;s important to know that I assume that you are already aware of how to create a custom template in Google Tag Manager and the Tests feature and the Tests user interface. That\u0026rsquo;s why I won\u0026rsquo;t cover some basic details in this article e.g. template data object, test setup, mocking data \u0026amp; functions, making assertions etc.\nIf you aren\u0026rsquo;t familiar with custom template creation process I recommend you to read Custom templates guides for Google Tag Manager from Simo Ahava or if you haven’t got any knowledge of the Tests feature or the anatomy of GTM Test I would recommend You to read Simo\u0026rsquo;s comprehensive article about GTM Unit testing.\nIntroduction: A fictional business story Let\u0026rsquo;s imagine that we have a CRM called Fantasy CRM where we store online and offline data. The actual problem is that we don\u0026rsquo;t measure offline conversions.\nSo our primary goal is to transmit offline purchase data once, when sale actually happens and distribute(broadcast) it across different marketing platforms, e.g. Google Analytics, Google Ads, Facebook etc\u0026hellip; to measure conversions.\nThere are some secondary goals also. Our solution should be tailored to the CRM as much as possible and must be able to log and handle errors.\nPlaning: High level components \u0026amp; dataflow To reach our goal we came up with the following idea:\nWe will use the CRM\u0026rsquo;s webhook feature - as an alternative implementation of the Hollywood Principle \u0026ldquo;Don\u0026rsquo;t Call Us, We\u0026rsquo;ll Call You\u0026rdquo; - for sending CRM offline purchase data to the broadcast server and for broadcasting we will use server-side GTM.\nThe Big Picture To easily understand system operations and the role of individual parts of our system, we\u0026rsquo;ve created a dataflow diagram aka. DFD.\nThis is a logical dataflow diagram. It displays the theoretical process of moving information through the system, like where the data comes from, where it goes, how it changes, and where it ends up.\nDFD maps out the flow of information for our system:\nThe dataflow components tasks are the following:\nWebhook: Initiate event-driven communication between 2 applications. In our case whenever an offline purchase occured the CRM will send a HTTP request to the server-side GTM end-point URL what we have registered before. Client: Listen for incoming HTTP requests, convert requests into event data, and respond to the requests. Once the Client processes the request, it makes data usable for tags and triggers in the server-side GTM container. Tag: Take the event data object, map it to the correct format, and dispatch it to external vendors via HTTP request. External vendors: Receive \u0026amp; process offline conversions event data. Choosing the right sGTM parts For the sGTM tags there are many ready-made solutions available to us as templates. Most of them are official or published \u0026amp; maintained by well-known, trusted creators. So we don\u0026rsquo;t have to invent the wheel again!\nHowever in our fictional case there isn\u0026rsquo;t any official CRM-specific solution for the sGTM client that\u0026rsquo;s why we\u0026rsquo;ve decided to build a custom one on our own.\nWhat else influenced our sGTM client decision? To be perfectly honest, we can find a few generic 3rd-party HTTP request handler sGTM client templates on the Community Template Gallery, but(!) it is important to note that for quality \u0026amp; security reasons we should always prefer the official add-ons over third-party ones created by less-known authors. Even Google warns us about it: \u0026ldquo;Templates provided by third parties in the Google Tag Manager community template gallery are not provided by Google. Google makes no promises or obligations regarding the performance, quality, or content of the services and applications provided by the templates.\u0026rdquo; Long story short we should follow [OWASP principles][4] e.g. \u0026ldquo;Don’t trust services\u0026rdquo;.\nSecondly, generic templates, by their very nature, sadly go completely against the [UNIX DOTADIW][5] - \u0026ldquo;Do one thing and do it well\u0026rdquo; - philosophy. That is why they often contain unwanted functions, which makes our final solution more complicated than it needs to be. In addition, it is also possible that at the end of the day we are faced with untested, bloated code. This makes the template itself unnecessarily complex and may cause unexpected side effects or behavior.\nIn summary, since there is no official sGTM client in our case that fits our needs - e.g. maximise the controll over the CRM\u0026rsquo;s webhook request handling - and we don\u0026rsquo;t want to run experiments with generic templates to see if they really meet our needs or not, and since we have to take full responsibility for the final product and want to mitigate security risks we create one ourselves.\nSo first we have to examine the CRM\u0026rsquo;s webhook request process and than we will create a custom sGTM client for request handling.\nUnderstand CRM\u0026rsquo;s webhook request Let\u0026rsquo;s remember! Whenever an offline purchase occured the CRM will send a HTTP request.\nIf there isn’t any detailed CRM webhook documentation we have to capture the request to be able to examine it. We can write a small app in Python or PHP for that, or use off-the-shelf tools like pipedream\u0026rsquo;s requestbin.\nWe\u0026rsquo;ve captured the request and found out that the request method is POST and an example for the body of the request is as follows:\n{ \u0026#34;id\u0026#34;: \u0026#34;e45e3467-3e77-46f4-8504-7bfc42ebb17d\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;john.doe@sgtm.example.com\u0026#34;, \u0026#34;revenue\u0026#34;: 99.9 } Here are some key things to note about the webhook request body:\nIt\u0026rsquo;s in JSON format. That\u0026rsquo;s a good start! It\u0026rsquo;s a simple key-value pair object not an array of objects so from the sGTM point of view it contains only one event. It does\u0026rsquo;t contain any Common event data parameters as keys e.g. event_name! All of the above are important to know in advance for Custom sGTM Client development.\nNow that we have organized the required information, we will continue with the implementation of our project.\nImplementation: Creating a Client Template Clients are adapters between the software running on an external device and our sGTM container.\nThe main purpose of a Client is to parse the incoming HTTP request and generate a standard Event Data object for sGTM tags to utilize. The results are sent back to the requester.\nIMPORTANT! A single incoming HTTP request can be \u0026ldquo;claimed\u0026rdquo; by only one client. As soon as a client claims the request, no other client can be activated for that request anymore. We will come back to this in the Testing: Unit Testing our Client Template section.\nTo maximise the controll over the CRM\u0026rsquo;s webhook request handling in sGTM I\u0026rsquo;ve created a simple sGTM Custom Client Template.\nIt\u0026rsquo;s for educational purposes only! The template is publicly available on github:\nFantasy CRM - sGTM Custom Client Template Github repository The code part As I mentioned earlier, I won\u0026rsquo;t detail every step of the template creation process. I will just focus on the code part because this is the part that is heavily related to unit testing. Speaking about the code server container templates use only sandboxed JavaScript.\nWhat is Sandboxed Javascript? Sandboxed JavaScript is a simplified subset of the JavaScript language that runs in an isolated environment. This kind of simplicity and isolation is perfect for running \u0026amp; testing JavaScript code in a safely way without any unwanted side effects.\nThe downside is that we can\u0026rsquo;t use the full spectrum of JavaScript. For example there is no new keyword in GTM sandboxed JavaScript, and functions don\u0026rsquo;t have access to the this keyword. That\u0026rsquo;s why we can\u0026rsquo;t use OOP paradigm in sGTM templates.\nIf you would like to know more about what sandboxed JavaScript stands for I recommend you to read the [official Google documentation about the sandboxed JavaScript][10]\nLet\u0026rsquo;s see the Custom Client Template code now!\n// Include modules aka. sGTM APIs const claimRequest = require(\u0026#39;claimRequest\u0026#39;); const getRequestMethod = require(\u0026#39;getRequestMethod\u0026#39;); const getRequestPath = require(\u0026#39;getRequestPath\u0026#39;); const getRequestBody = require(\u0026#39;getRequestBody\u0026#39;); const JSON = require(\u0026#39;JSON\u0026#39;); const getType = require(\u0026#39;getType\u0026#39;); const runContainer = require(\u0026#39;runContainer\u0026#39;); const setResponseStatus = require(\u0026#39;setResponseStatus\u0026#39;); const setResponseBody = require(\u0026#39;setResponseBody\u0026#39;); const returnResponse = require(\u0026#39;returnResponse\u0026#39;); const logToConsole = require(\u0026#39;logToConsole\u0026#39;); // Decide whether logging is enabled const log = data.loggingIsEnabled ? logToConsole : (() =\u0026gt; { return undefined; }); // Do some basic logging for debuging purpose log(\u0026#34;Client template settings: \u0026#34;, data); log(\u0026#34;Request method: \u0026#34;, getRequestMethod()); log(\u0026#34;Request body: \u0026#34;, getRequestBody()); // Decide whether the Client is allowed to claim the request if (getRequestPath() === data.path \u0026amp;\u0026amp; getRequestMethod() === \u0026#39;POST\u0026#39;) { // Claim the request claimRequest(); // Convert HTTP request body to event object const parsedRequestBody = JSON.parse(getRequestBody()); // Do some very basic validation let requestBodyIsValid = true; if (getType(parsedRequestBody) !== \u0026#39;object\u0026#39;) { requestBodyIsValid = false; } if (!parsedRequestBody.id) { requestBodyIsValid = false; } if (!parsedRequestBody.email) { requestBodyIsValid = false; } if (!parsedRequestBody.revenue) { requestBodyIsValid = false; } // Decide whether the request body is valid if (requestBodyIsValid) { // Create the event object for the container const event = parsedRequestBody; event.event_name = data.eventName; // Run the container with the event \u0026amp; return response runContainer(event, () =\u0026gt; returnResponse()); } else { // Create an error message const errorMessage = \u0026#34;Invalid request payload\u0026#34;; // Log error log(errorMessage); // Send error response setResponseStatus(422); setResponseBody(errorMessage); returnResponse(); } } The comments should help you understand how the code works, but to make it even clearer let\u0026rsquo;s look at it part by part:\n1.) Include modules aka. sGTM APIs\nThe very first step is to require() the necessary APIs(functions) to be able to use them in the template code.\nconst claimRequest = require(\u0026#39;claimRequest\u0026#39;); const getRequestMethod = require(\u0026#39;getRequestMethod\u0026#39;); const getRequestPath = require(\u0026#39;getRequestPath\u0026#39;); // etc. You could find more information about the available APIs on the Google Developers platform.\n2.) Decide whether logging is enabled\nWe\u0026rsquo;ve created our own log function named log(). The reason for that is we want to add a feature to control logging. So the logic depends on the actual template UI settings configured by the end-user: If logging is enabled in the template options the script calls logToConsole() otherwise it calls an empty function and nothing will be logged.\nconst log = data.loggingIsEnabled ? logToConsole : (() =\u0026gt; { return undefined; }); 3.) Do some basic logging for debuging purpose\nOnly for debuging purpose we log() some basic details: First our actual template settings. Secondly we log the HTTP request path, method and body.\nlog(\u0026#34;Client template settings: \u0026#34;, data); log(\u0026#34;Request method: \u0026#34;, getRequestMethod()); log(\u0026#34;Request body: \u0026#34;, getRequestBody()); 4.) Decide whether the Client is allowed to claim the request\nDepending on the template UI path setting i.e. data.path and the HTTP request method i.e. getRequestMethod() values the script decide whether the client should claim the request or not.\nif (getRequestPath() === data.path \u0026amp;\u0026amp; getRequestMethod() === \u0026#39;POST\u0026#39;) 5.) Claim the request\nIf the client should handle the request we call the call the claimRequest().\nclaimRequest(); IMPORTANT! As the official Google documentation outlines we should call the claimRequest() to capture the request in the Custom Client Template code before we call the runContainer() API.\n6.) Convert HTTP request body to event object\nWe parse the request body string with JSON.parse() to an object.\nconst parsedRequestBody = JSON.parse(getRequestBody()); 7.) Do some very basic validation\nWe do a very basic validation: We use requestBodyIsValid variable as a flag. First we set it\u0026rsquo;s value to true and after that we start to validate the body step-by-step. If any validation fails we set the requestBodyIsValid value to false. That\u0026rsquo;s all!\nlet requestBodyIsValid = true; if (getType(parsedRequestBody) !== \u0026#39;object\u0026#39;) { requestBodyIsValid = false; } if (!parsedRequestBody.id) { requestBodyIsValid = false; } if (!parsedRequestBody.email) { requestBodyIsValid = false; } if (!parsedRequestBody.revenue) { requestBodyIsValid = false; } The actual snippet goes against the DRY principle, so it cries out for re-factoring!\nWhy incoming data validation is important? We should check for expected format, type, etc., to identify and prevent errors, inconsistencies, and fraud.\nOf course, a much more complex check should be carried out, but for the sake of simplicity, we only check basic things now.\nI will go into more depth about HTTP request testing \u0026amp; validation in a future article. Stay tuned!\n8.) Decide whether the request body is valid\nDepending on the requestBodyIsValid variable value the script decide whether to continue the request processing or not.\nif (requestBodyIsValid) { // Validation passed } else { // Validation failed } 9.a.) Request body is valid\nIf the body of the request has passed the validation, the request processing continues in 2 main steps:\nWe create the event object and we add the event name to it. const event = parsedRequestBody; event.event_name = data.eventName; The data.eventName receives value from from the template UI settings.\nWe start the container, passing the previously created event object to it. runContainer(event, () =\u0026gt; returnResponse()); In this case the container will return 200 OK status to the requester as a response. You could find more information about the runContainer() API on the related official Google Developers page.\n9.b.) Request body is invalid\nIf the body of the request has failed the validation, the request processing continues in 3 main steps:\nCreate an error message const errorMessage = \u0026#34;Invalid request payload\u0026#34;; Log the error log(errorMessage); Send error response setResponseStatus(422); setResponseBody(errorMessage); returnResponse(); A 422 Unprocessable Entity HTTP status code is used when a server understands the content type of a request, but the content is invalid.\nTesting: Unit Testing our Client Template Unit tests for Google Tag Manager custom templates help you validate the functionality of your code.\nWhat exactly is a unit test? Below is a simplified version of the testing pyramid from the opening keynote of the 2014 Google Test Automation Conference:\nIt\u0026rsquo;s a great visual metaphor telling you to think about different layers of testing. Unit tests form the foundation of this pyramid.\nIn testing unit represents the smallest testable part of the code.\nWhat qualifies as a unit in GTM context differs from traditional programming. In standard codebases, a unit is often a single function or class. In GTM Custom Templates, however, tests are executed at the template level. You don’t directly invoke internal helper functions in isolation. Instead, you execute the template’s code via the runCode API and validate its behavior using different mocked inputs and API expectations.\nUnit tests sit at the bottom because they are simple, fast, numerous, and run in strict isolation. Unlike complex ones like integration or end-to-end tests, which may require real HTTP requests, external servers. Unit tests operate entirely with mocks, never actually touching the network.\nUnit tests do have one major disadvantage: even if the units work well in isolation, you do not know if they work well together. But even then, you do not necessarily need end-to-end tests. For that, you can use an integration test. An integration test takes a small group of units, often two units, and tests their behavior as a whole, verifying that they coherently work together.\nUseful articles: Testing pyramid The practical test pyramid\nSetup The GTM template editor has a dedicated Setup tab. This is the place where we write code that runs before every test in our test suite. It\u0026rsquo;s the perfect spot to put shared mocks that apply across all of our tests, so we don\u0026rsquo;t have to repeat the same setup code over and over in each individual test.\nThink of it as the beforeEach block in a traditional testing framework — a single place to prepare the common ground that every test depends on.\nWe\u0026rsquo;ll start with a few lines of setup code:\n// Imports const json = require(\u0026#39;JSON\u0026#39;); // Mocks mock(\u0026#39;getRequestPath\u0026#39;, \u0026#39;/fantasy-crm-webhook\u0026#39;); mock(\u0026#39;getRequestMethod\u0026#39;, \u0026#39;POST\u0026#39;); Let\u0026rsquo;s go through it part by part:\n1.) Import JSON\nWe start by requiring the JSON module since we\u0026rsquo;ll need it in our individual tests to stringify the mock request body.\nconst json = require(\u0026#39;JSON\u0026#39;); 2.) Mock getRequestPath()\nWe mock the getRequestPath() API to always return our expected webhook path. Since we put this in the Setup tab, every test will assume that the request is coming to the correct endpoint path.\nmock(\u0026#39;getRequestPath\u0026#39;, \u0026#39;/fantasy-crm-webhook\u0026#39;); 3.) Mock getRequestMethod()\nSimilarly, we mock getRequestMethod() to always return 'POST'. Again, this eliminates the need to repeat it in each individual test.\nmock(\u0026#39;getRequestMethod\u0026#39;, \u0026#39;POST\u0026#39;); Tests While the Setup tab holds shared code that runs before every test, each individual Test tab represents a single, focused test case. This is where we define test-specific mocks, execute the template code via runCode(), and verify the expected behavior using assertions.\nTo put it simply: Setup is the shared foundation, Test is the actual scenario. Setup runs once before each Test. Tests run independently, one by one, or all at once in batch.\nEach test follows the same three-step structure:\nMock — define the test-specific inputs and API return values Run — execute the template code with runCode(mockData) Assert — verify that the expected APIs were called or not called Code example:\n// Mock const mockData = { \u0026#34;path\u0026#34;: \u0026#39;/fantasy-crm-webhook\u0026#39;, \u0026#34;eventName\u0026#34;: \u0026#39;fantasy-crm-offline-purchase\u0026#39;, \u0026#34;loggingIsEnabled\u0026#34;: false }; const mockEvent = { \u0026#34;id\u0026#34;: \u0026#34;e45e3467-3e77-46f4-8504-7bfc42ebb17d\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;john.doe@sgtm.example.com\u0026#34;, \u0026#34;revenue\u0026#34;: 99.9 }; mock(\u0026#39;getRequestBody\u0026#39;, json.stringify(mockEvent)); // Call runCode to run the template\u0026#39;s code. runCode(mockData); // Make assertions. assertApi(\u0026#39;runContainer\u0026#39;).wasCalled(); assertApi(\u0026#39;logToConsole\u0026#39;).wasNotCalled(); Concurrency problem When we try to run multiple test we are facing with an weird issue.\nI\u0026rsquo;ve created a video about it:\nIf we run our client tests separatelly everything is fine but if we run them in batch only the first test passed all other failed.\nThe root cause is that the GTM test runner shares a single simulated request context across all tests in a batch. So when the first test calls claimRequest(), the request is marked as claimed — and every subsequent test that tries to claim it triggers the exception. So once a request is claimed sGTM doesn\u0026rsquo;t run any additional clients.\nThe claimRequest API throws an exception if called after the client returns.\nYou could find more details about the exception in the official Google documentation here.\nSolving the concurrency problem Mocking is a technique in software testing where we replace a real dependency with a controlled substitute — a \u0026ldquo;mock\u0026rdquo; — that simulates the behavior of the original. Instead of calling a real function that does real work, we instruct the test to return a value we control.\nThis lets us test our template code in strict isolation, without relying on real network calls, external systems or side effects.\nIn GTM template tests, mocking is not optional — it\u0026rsquo;s mandatory. We simply can\u0026rsquo;t make real HTTP requests during a unit test. The mock() API is how we feed our template code controlled inputs and intercept API calls so we can assert on them.\nWe have to change our setup code for mocking:\n// Imports const json = require(\u0026#39;JSON\u0026#39;); // Mocks mock(\u0026#39;getRequestPath\u0026#39;, \u0026#39;/fantasy-crm-webhook\u0026#39;); mock(\u0026#39;getRequestMethod\u0026#39;, \u0026#39;POST\u0026#39;); // Change no.1.: Mock the claimRequest API mock(\u0026#39;claimRequest\u0026#39;, function () {}); // Change no.2.: Mock the runContainer API let containerCallback; let containerEvent; mock(\u0026#39;runContainer\u0026#39;, (e, cb) =\u0026gt; { containerEvent = e; containerCallback = cb; cb(); }); Let\u0026rsquo;s look at both changes:\nChange no.1: Mock claimRequest()\nThe claimRequest() API is designed to be called only once per incoming request. When we run multiple tests in batch, each test tries to invoke it — causing the exception we saw earlier. By mocking claimRequest() with an empty function we prevent the real API from firing, so every test can independently simulate claiming the request without interfering with the others.\nmock(\u0026#39;claimRequest\u0026#39;, function () {}); Change no.2: Mock runContainer()\nThe runContainer() API is asynchronous by nature. Mocking it allows us to capture the event object and the callback that our template passes to it, so we can inspect and assert on them in individual tests. We also immediately invoke cb() to simulate the container completing its execution.\nmock(\u0026#39;runContainer\u0026#39;, (e, cb) =\u0026gt; { containerEvent = e; containerCallback = cb; cb(); }); By storing the event and callback in the containerEvent and containerCallback variables we make them accessible to our test assertions. This way we can verify that our client is passing the correct event data to the container.\nTesting the sad path Not every request is valid — and our client handles that explicitly. This test covers the \u0026ldquo;Invalid request body (empty string) \u0026amp; logging enabled\u0026rdquo; scenario: when the request body is empty, the client should reject it with a 422 response and log the error.\n// Mock const mockData = { \u0026#34;path\u0026#34;: \u0026#39;/fantasy-crm-webhook\u0026#39;, \u0026#34;eventName\u0026#34;: \u0026#39;fantasy-crm-offline-purchase\u0026#39;, \u0026#34;loggingIsEnabled\u0026#34;: true }; const mockEvent = \u0026#34;\u0026#34;; mock(\u0026#39;getRequestBody\u0026#39;, json.stringify(mockEvent)); // Call runCode to run the template\u0026#39;s code. runCode(mockData); // Make assertions. assertApi(\u0026#39;setResponseStatus\u0026#39;).wasCalledWith(422); Notice that loggingIsEnabled is set to true here — so we can also assert that logToConsole was called, verifying that error logging behaves correctly in the invalid path. This is exactly the kind of edge case that\u0026rsquo;s easy to miss without a dedicated test.\nWhy does the client respond with 422 and not 400? The 422 Unprocessable Entity HTTP status code signals that the server understood the request format (so it\u0026rsquo;s not a 400 Bad Request), but the actual content of the request body failed semantic validation — in our case a missing or malformed field. It\u0026rsquo;s a more precise and descriptive response than a generic 400, which makes debugging much easier for whoever is sending the request. Summary Think of unit tests as your code’s safety belt.\nAs your logic evolves, these tests ensure that new changes don\u0026rsquo;t break existing functionality. They give you the confidence to refactor and improve your code, knowing that if you make a mistake, the tests will catch it before it reaches a live container.\nUnit testing is often treated as an afterthought in Tag Management, but it shouldn\u0026rsquo;t be. Tags and clients process real data, trigger real conversions, and directly affect business decisions and ad spend. A bug in your client template isn\u0026rsquo;t just a code issue — it\u0026rsquo;s a data quality issue. And bad data can silently corrupt everything downstream.\nThe mocking approach we\u0026rsquo;ve covered here is not a workaround — it\u0026rsquo;s the correct way to write isolated, repeatable and reliable unit tests. Once you start writing them, you\u0026rsquo;ll quickly realise how many edge cases you\u0026rsquo;d otherwise miss. Investing in unit tests early saves you from painful debugging sessions in production later.\nTo put it simply: if your code is worth writing, it\u0026rsquo;s worth testing.\n📎 Resources An introduction to server-side tagging Sandboxed JavaScript Template style guide Server-side tagging APIs Unit tests for Google Tag Manager custom templates Sandbox javascript Writing tests for custom templates in google tag manager GTM Template testing The Risks of Third-Party Browser Plugins ","permalink":"https://rolandbende.com/posts/sgtm-run-multiple-custom-client-template-unit-tests/","summary":"\u003cp\u003eIn this advanced level guide I’ll introduce how to \u003cstrong\u003ecreate \u0026amp; run multiple Server-side GTM Unit Tests.\u003c/strong\u003e I will share some \u003cstrong\u003epractical tricks with examples\u003c/strong\u003e on how to overcome unexpected challenges when your \u003cstrong\u003eCustom Client Template Unit tests\u003c/strong\u003e try to \u003cstrong\u003eclaim the request right after each other\u003c/strong\u003e when we \u003cstrong\u003erun\u003c/strong\u003e them \u003cstrong\u003ein batch\u003c/strong\u003e. For the better understanding, I\u0026rsquo;ve wrapped the technical parts in a fictional business story.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eIf you’re not interested in the story nor the planning or custom client coding part jump right to the section on \u003ca href=\"#-testing-unit-testing-our-client-template\"\u003eTesting: Unit Testing our Client Template\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e","title":"sGTM: Run Multiple Custom Client Template Unit Tests"}]