<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dennis Whalen</title>
    <description>The latest articles on DEV Community by Dennis Whalen (@dwwhalen).</description>
    <link>https://dev.to/dwwhalen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F197670%2F7e801472-bab5-4500-986d-b9c9629d7c96.jpeg</url>
      <title>DEV Community: Dennis Whalen</title>
      <link>https://dev.to/dwwhalen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dwwhalen"/>
    <language>en</language>
    <item>
      <title>Maestro: A Single Framework for Mobile and Web E2E Testing</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Fri, 26 Dec 2025 13:26:20 +0000</pubDate>
      <link>https://dev.to/leading-edje/maestro-a-single-framework-for-mobile-and-web-e2e-testing-b98</link>
      <guid>https://dev.to/leading-edje/maestro-a-single-framework-for-mobile-and-web-e2e-testing-b98</guid>
      <description>&lt;p&gt;I've recently been working on a personal project that has both mobile and web frontends. I wanted to include E2E tests, but I didn't want to spend a bunch of time getting all of that setup for web, iOS, and Android.&lt;/p&gt;

&lt;p&gt;I just wanted a handful of happy-path E2E tests for an app that could run on a desktop browser, mobile browser, and native mobile.&lt;/p&gt;

&lt;p&gt;Most importantly, I wanted to get this running quickly so I could focus on actually building the app.  That's when I found an open source tool called &lt;a href="https://maestro.dev/" rel="noopener noreferrer"&gt;Maestro&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;What immediately caught my attention with Maestro is that it's so easy to get setup, and it handles both web and mobile with the same tool and syntax. &lt;/p&gt;

&lt;h2&gt;
  
  
  Here's What a Test Looks Like
&lt;/h2&gt;

&lt;p&gt;Maestro tests are written in YAML. Here's a simple desktop browser example that searches DuckDuckGo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://duckduckgo.com&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;launchApp&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Search&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;without&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;being&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tracked'&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Maestro&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;e2e&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;testing'&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pressKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enter&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.*Maestro&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;open-source&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;framework.*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty straightforward, right? It opens DuckDuckGo, taps the search box, searches for "Maestro e2e testing", and verifies that the results contain "Maestro is an open-source framework". Note that for partial text matching, Maestro uses regex—the &lt;code&gt;.*&lt;/code&gt; pattern means "any characters", so &lt;code&gt;".*text.*"&lt;/code&gt; effectively does a "contains" match.&lt;/p&gt;

&lt;p&gt;To be honest, I was not super excited to work with a tool that uses YAML to define the tests.  In my regular job I spend a lot of time building out code-based automation suites, and that usually feels like the "right" way to do it.  But is that always the case?&lt;/p&gt;

&lt;p&gt;My personal project is not super complex, and I don't have a team of test automation folks.  I have one dev and one QA, and they are both me.  I want E2E tests, but I want to focus the majority of my time on building the app, not building fancy-pants automation frameworks.&lt;/p&gt;

&lt;p&gt;Let's run this test!&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;I am not assuming that everyone uses a Mac, but that's what I'm using so keep that in mind if you're reading this as a Windows or Unix person.  Maestro is cross-platform, but some of the install steps will be different.  See their &lt;a href="https://docs.maestro.dev/getting-started/installing-maestro" rel="noopener noreferrer"&gt;setup documentation&lt;/a&gt; for more details.&lt;/p&gt;

&lt;p&gt;First, let's install Maestro.  Open your terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="s2"&gt;"https://get.maestro.mobile.dev"&lt;/span&gt; | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you can use Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap mobile-dev-inc/tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;maestro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OK so what do I need to install next?  Huh, that's it??  Well then... let's run the test!&lt;/p&gt;

&lt;h2&gt;
  
  
  Running a Test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro &lt;span class="nb"&gt;test &lt;/span&gt;flows/duckduckgo-search-desktop.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maestro will open a browser, run through the test steps, and show you the results. If something fails, the output helps you figure out what went wrong, and you'll also get some detailed log files.  Hopefully your run will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0i83z23yirqxrjru2c7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0i83z23yirqxrjru2c7.png" alt="Maestro console output from desktop browser test" width="522" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Same Test on Mobile Browser
&lt;/h2&gt;

&lt;p&gt;You can run a similar test on a mobile browser. Here's the mobile version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.android.chrome&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;launchApp&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Search&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;URL"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://duckduckgo.com"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pressKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enter&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;searchbox_input"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Maestro&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;e2e&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;testing"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pressKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enter&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.*Maestro&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;open-source&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;framework.*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the syntax is almost identical.  The main difference is using &lt;code&gt;url:&lt;/code&gt; for desktop browsers and &lt;code&gt;appId:&lt;/code&gt; for mobile browsers. Other than that, Maestro uses the same commands for both.&lt;/p&gt;

&lt;p&gt;To run this, you'll need an Android emulator. If you have Android Studio installed, you can use the AVD Manager to create one. Make sure Chrome is installed on the emulator (it usually is by default).&lt;/p&gt;

&lt;p&gt;Once your emulator is running, just run the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro &lt;span class="nb"&gt;test &lt;/span&gt;flows/duckduckgo-search-mobile.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hopefully you'll see the same interactions that you saw with the desktop browser test, and the same green results, like this!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forondtneq605eu1cugd0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forondtneq605eu1cugd0.png" alt="Maestro console output from mobile browser test" width="541" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You now have a taste for browser-based Maestro testing on a desktop browser and a mobile browser.  Let's move away from the browser and use Maestro test a mobile app. &lt;/p&gt;

&lt;h2&gt;
  
  
  Testing a Native Mobile App
&lt;/h2&gt;

&lt;p&gt;The built-in Android Contacts app is perfect for this because it's available on every Android device and works great in an emulator. Notice how the syntax is the same as the web test.  Maestro uses the same commands whether you're testing web or native mobile.&lt;/p&gt;

&lt;p&gt;Here's a test that creates a new contact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.google.android.contacts&lt;/span&gt;
&lt;span class="na"&gt;jsEngine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;graaljs&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;evalScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output.firstName = faker.name().firstName()}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;evalScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output.lastName = faker.name().lastName()}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;evalScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output.phoneNumber = faker.phoneNumber().phoneNumber()}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;launchApp&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Create&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;contact"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;First&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output.firstName}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Last&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output.lastName}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;longPressOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Phone&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(Mobile)"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Select&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;All'&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;eraseText&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output.phoneNumber}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Save"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output.firstName + " " + output.lastName}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;scrollUntilVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Delete"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Delete"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Delete"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deleted"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test is a bit more advanced as it demonstrates Maestro's ability to generate dynamic test data using Faker. The &lt;code&gt;jsEngine: graaljs&lt;/code&gt; setting enables JavaScript execution, and the &lt;code&gt;evalScript&lt;/code&gt; commands at the top use Faker to generate random first names, last names, and phone numbers. These values are stored in the &lt;code&gt;output&lt;/code&gt; object and referenced throughout the test using &lt;code&gt;${output.variableName}&lt;/code&gt; syntax. &lt;/p&gt;

&lt;p&gt;This is just one example of integrating JavaScript with Maestro scripts.  More detail can be found &lt;a href="https://docs.maestro.dev/advanced/javascript" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running It
&lt;/h3&gt;

&lt;p&gt;With your emulator running, execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro &lt;span class="nb"&gt;test &lt;/span&gt;flows/contacts-app-android.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test will run, and you'll see the emulator actually perform the actions. If it passes, you'll see a nice success message. If it fails, Maestro will tell you what went wrong and where.  Here's what I see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpetx42gck7kceo2k5glx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpetx42gck7kceo2k5glx.png" alt="Maestro console output from Android Contacts app test" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Maestro MCP
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) is a standardized protocol that bridges tools (like Maestro) to LLMs (like Claude or ChatGPT). Think of it as a universal connector that lets these AI models access and interact with your development tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; If you're using these LLMs in your development workflow, Maestro includes an MCP that lets them interact with Maestro directly. They can read your test files, understand your test structure, suggest improvements, or even generate tests based on your app's behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to use it:&lt;/strong&gt; The MCP server comes bundled with Maestro. To use it in Cursor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Cursor Settings&lt;/li&gt;
&lt;li&gt;Navigate to the MCP section&lt;/li&gt;
&lt;li&gt;Click "Add new MCP Server"&lt;/li&gt;
&lt;li&gt;Configure it with:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"maestro"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"maestro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Save and restart Cursor&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Similar functionality is available in other tools like VS Code through MCP extensions. Once connected, the AI assistant can discover your Maestro flows, understand your test structure, and help you write better tests.&lt;/p&gt;

&lt;p&gt;More details can be found &lt;a href="https://docs.maestro.dev/getting-started/maestro-mcp" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few things I didn't cover but want to mention
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Maestro is easy to run on you CI platform, and also has a Cloud plan.  More info &lt;a href="https://docs.maestro.dev/cloud/ci-integration" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maestro has a ton of sample flows to help you learn more &lt;a href="https://docs.maestro.dev/getting-started/run-a-sample-flow" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maestro has an IDE to help with identifying UI elements, generating code, and running commands.  &lt;a href="https://docs.maestro.dev/getting-started/maestro-studio-cli" rel="noopener noreferrer"&gt;Check it out&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Take a look at &lt;a href="https://docs.maestro.dev" rel="noopener noreferrer"&gt;docs.maestro.dev&lt;/a&gt; for more examples, advanced features like nested flows and conditions, page objects, and tips for structuring larger test suites.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy building and testing.  Peace out! &lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>mobile</category>
      <category>qa</category>
    </item>
    <item>
      <title>Add Structured Testing to Your AI Vibe - with promptfoo</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Thu, 04 Sep 2025 11:46:23 +0000</pubDate>
      <link>https://dev.to/leading-edje/add-structured-testing-to-your-ai-vibe-with-promptfoo-5h3o</link>
      <guid>https://dev.to/leading-edje/add-structured-testing-to-your-ai-vibe-with-promptfoo-5h3o</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/leading-edje/automate-the-testing-of-your-llm-prompts-5038"&gt;previous promptfoo post&lt;/a&gt;, we covered the basics of testing LLM prompts with simple examples using &lt;a href="https://www.promptfoo.dev/docs/intro/" rel="noopener noreferrer"&gt;promptfoo&lt;/a&gt;. But when you're building an actual application that processes user-generated content at scale, you might discover that your carefully crafted prompt needs to handle far more complexity than you initially anticipated.&lt;/p&gt;

&lt;p&gt;Many teams are still doing "vibe testing" - manually checking a few examples, tweaking prompts based on gut feel, and hoping everything works in production. While this might get you started, a systematic evaluation framework puts you significantly ahead of the curve when it comes to building and maintaining reliable AI systems, and provides a mechanism to build a set of repeatable automated regression tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Assignment
&lt;/h2&gt;

&lt;p&gt;Let's consider an example.  You're working with a major ecommerce client, and your team is building a feature that will analyze user submitted product reviews. Your application needs to evaluate the product reviews, classify sentiment, extract key product features mentioned, detect potentially fake reviews, and make moderation decisions.  This will help customers find trustworthy reviews and help your business maintain review quality.&lt;/p&gt;

&lt;p&gt;The core of this system is a prompt that takes each incoming review and returns structured data, such as sentiment classification, confidence scores, extracted features, fake review indicators, and moderation recommendations. &lt;/p&gt;

&lt;p&gt;This prompt might work well during development, but once deployed, it needs to handle the messy reality of real user reviews. Your prompt will definitely need to be able to handle things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mixed sentiment reviews (loved the product, hated the shipping)&lt;/li&gt;
&lt;li&gt;Fake or suspicious reviews&lt;/li&gt;
&lt;li&gt;Reviews with profanity or inappropriate content&lt;/li&gt;
&lt;li&gt;Sarcastic or nuanced language&lt;/li&gt;
&lt;li&gt;Reviews that mention competitors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where a systematic process with multiple scenarios becomes crucial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Requirements
&lt;/h2&gt;

&lt;p&gt;Speaking of systematic processes, before we dive into building our prompt and setting up the prompfoo tests, let's outline what the requirements would look like.  We'll use our old friend gherkin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kd"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Product Review Analysis Prompt

  &lt;span class="kn"&gt;Scenario Outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Prompt analyzes product reviews correctly
    &lt;span class="nf"&gt;Given &lt;/span&gt;a product review analysis prompt
    &lt;span class="nf"&gt;And &lt;/span&gt;a &lt;span class="s"&gt;"&amp;lt;review_type&amp;gt;"&lt;/span&gt; product review
    &lt;span class="nf"&gt;When &lt;/span&gt;the prompt processes the review
    &lt;span class="nf"&gt;Then &lt;/span&gt;the sentiment should be classified as &lt;span class="s"&gt;"&amp;lt;expected_sentiment&amp;gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;And &lt;/span&gt;fake review indicators should be &lt;span class="s"&gt;"&amp;lt;fake_indicators&amp;gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;And &lt;/span&gt;the recommendation should be &lt;span class="s"&gt;"&amp;lt;expected_recommendation&amp;gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;And &lt;/span&gt;key features should be extracted

    &lt;span class="nn"&gt;Examples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;review_type&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;expected_sentiment&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;expected_fake_indicators&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;expected_recommendation&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;positive&lt;/span&gt;    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;positive&lt;/span&gt;           &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;absent&lt;/span&gt;                   &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;approve&lt;/span&gt;                 &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;negative&lt;/span&gt;    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;negative&lt;/span&gt;           &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;absent&lt;/span&gt;                   &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;approve&lt;/span&gt;                 &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;mixed&lt;/span&gt;       &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;mixed&lt;/span&gt;              &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;absent&lt;/span&gt;                   &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;flag_for_review&lt;/span&gt;         &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;suspicious&lt;/span&gt;  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;positive&lt;/span&gt;           &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;present&lt;/span&gt;                  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;flag_for_review&lt;/span&gt;         &lt;span class="p"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gherkin is just a way to describe requirements in plain language.  In this case, we have four main test scenarios: positive reviews, negative reviews, mixed sentiment reviews, and suspicious/fake reviews.&lt;/p&gt;

&lt;p&gt;Promptfoo doesn't use gherkin, but I do, and it helps me think through the scenarios we need to cover.  We'll translate these scenarios into actual promptfoo tests next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving Beyond Inline YAML: File-Based Organization
&lt;/h2&gt;

&lt;p&gt;In my &lt;a href="//../promptfoo-1-testing-custom-LLM-prompts"&gt;last post&lt;/a&gt; we defined the entire test in YAML.  Before diving into complex scenarios, let's improve our testing structure by moving prompts into separate files. This makes them easier to maintain, version control, and collaborate on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;promptfoo-product-reviews/
├── prompts/
│   └── analyze-review.txt
├── test-data/
│   ├── positive-review.txt
│   ├── negative-review.txt
│   ├── mixed-review.txt
│   └── suspicious-review.txt
├── analyze-review-spec.yaml
└── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating Our Review Analysis Prompt
&lt;/h3&gt;

&lt;p&gt;Let's first create a prompt specifically designed for ecommerce product review analysis:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;prompts/analyze-review.txt&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an expert product review analyzer for an ecommerce platform. Analyze the following product review and provide a structured assessment.

Product Review:
{{review_text}}

Provide your analysis in the following JSON format. Return ONLY the JSON object, no markdown code blocks, no explanations, no additional text:
{
  "sentiment": "positive|negative|mixed",
  "confidence": 0.0-1.0,
  "key_features_mentioned": ["feature1", "feature2"],
  "main_complaints": ["complaint1", "complaint2"],
  "main_praise": ["praise1", "praise2"],
  "suspected_fake": boolean,
  "fake_indicators": ["indicator1", "indicator2"],
  "recommendation": "approve|flag_for_review|reject",
  "summary": "Brief 1-2 sentence summary"
}

Focus on:
- Accurate sentiment classification, especially for mixed reviews
- Extracting specific product features mentioned
- Identifying potential fake review indicators such as generic language without specific details, suspicious patterns, overly positive language, and extreme superlatives, overly negative language
- Providing actionable moderation recommendations

IMPORTANT: Return ONLY valid JSON. Do not wrap in markdown code blocks or add any other text.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test Scenarios: Real-World Product Reviews
&lt;/h2&gt;

&lt;p&gt;So that's the prompt we're going to test. Now let's create diverse test scenarios that represent what you'd actually encounter in production.  You might make these up, or you might use some actual production reviews.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 1: Genuine Positive Review Example
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;test-data/positive-review.txt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've been using these wireless earbuds for 3 months now and I'm really impressed. The battery life is excellent - I get about 6-7 hours of continuous listening, and the case gives me 2-3 full charges. The sound quality is crisp and clear, with good bass response for the price point. They stay comfortable in my ears during workouts and haven't fallen out once. The touch controls take some getting used to but work reliably once you learn them. Only minor complaint is that the case is a bit bulky for my small pockets, but that's a trade-off for the extra battery. Would definitely recommend for anyone looking for reliable wireless earbuds under $100.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: Detailed Negative Review
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;test-data/negative-review.txt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Very disappointed with these earbuds. The connection constantly drops out, especially when my phone is in my pocket or more than a few feet away. The battery life is nowhere near the advertised 8 hours - I'm lucky to get 4 hours before they die. The sound quality is muddy and lacks clarity, particularly in the mid-range frequencies. They're also uncomfortable for extended wear - my ears start hurting after about an hour. The touch controls are oversensitive and constantly trigger accidentally when I adjust them. For the price, I expected much better quality. I've had $20 earbuds that performed better than these. Returning them and looking for alternatives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 3: Mixed Sentiment Review
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;test-data/mixed-review.txt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These earbuds are a mixed bag. On the positive side, the sound quality is really good - clear highs, decent bass, and good overall balance. The build quality feels solid and they look premium. The battery life meets expectations at around 6 hours. However, there are some significant issues. The Bluetooth connection is unreliable - frequent dropouts and sometimes one earbud stops working randomly. The fit is also problematic for me - they tend to slip out during exercise despite trying all the included ear tips. Customer service was helpful when I contacted them about the connection issues, but the firmware update they suggested didn't solve the problem. Overall, great sound quality let down by connectivity and fit issues. Might work better for others but not ideal for my use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 4: Suspicious/Fake Review
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;test-data/suspicious-review.txt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Amazing product! These earbuds are the best I have ever used in my entire life. The sound quality is absolutely perfect and the battery life is incredible. They are so comfortable and never fall out. The connection is always stable and strong. I love everything about these earbuds and they exceeded all my expectations. Everyone should buy these right now because they are the greatest earbuds ever made. Five stars without any doubt! Highly recommend to all people who want amazing earbuds with perfect quality and performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comprehensive Test Configuration
&lt;/h2&gt;

&lt;p&gt;Now let's create a promptfoo configuration that tests all these scenarios with appropriate assertions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;analyze-review-spec.yaml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;description: Product Review Analysis Testing

prompts:
  - file://prompts/analyze-review.txt

providers:
  - openai:chat:gpt-4o-mini

tests:
  # Test 1: Genuine Positive Review
  - vars:
      review_text: file://test-data/positive-review.txt
    assert:
      - type: is-json
      - type: javascript
        value: |
          const response = JSON.parse(output);
          response.sentiment === 'positive' &amp;amp;&amp;amp; response.confidence &amp;gt; 0.7
      - type: contains-json
        value:
          suspected_fake: false
      - type: llm-rubric
        value: "Should identify key positive features like battery life, sound quality, and comfort. Should not flag as fake since it contains specific details and minor complaints."

  # Test 2: Detailed Negative Review  
  - vars:
      review_text: file://test-data/negative-review.txt
    assert:
      - type: is-json
      - type: javascript
        value: |
          const response = JSON.parse(output);
          response.sentiment === 'negative' &amp;amp;&amp;amp; response.confidence &amp;gt; 0.7
      - type: contains-json
        value:
          suspected_fake: false
      - type: llm-rubric
        value: "Should identify specific complaints about connection, battery, sound quality, and comfort. Should extract main issues for product team review."

  # Test 3: Mixed Sentiment Review
  - vars:
      review_text: file://test-data/mixed-review.txt
    assert:
      - type: is-json
      - type: javascript
        value: |
          const response = JSON.parse(output);
          response.sentiment === 'mixed'
      - type: llm-rubric
        value: "Should correctly identify mixed sentiment, extracting both positive aspects (sound quality, build) and negative aspects (connectivity, fit). This is the most challenging scenario for sentiment analysis."

  # Test 4: Suspicious/Fake Review
  - vars:
      review_text: file://test-data/suspicious-review.txt
    assert:
      - type: is-json
      - type: contains-json
        value:
          suspected_fake: true
      - type: javascript
        value: |
          const response = JSON.parse(output);
          response.fake_indicators &amp;amp;&amp;amp; response.fake_indicators.length &amp;gt; 0
      - type: llm-rubric
        value: "Should detect fake review indicators: overly positive language, lack of specific details, generic praise, and extreme superlatives."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding the Test Specification
&lt;/h2&gt;

&lt;p&gt;Let's break down what this test configuration accomplishes. We have &lt;strong&gt;four distinct tests&lt;/strong&gt; that correspond to the four key scenarios mentioned above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test 1: Genuine Positive Review&lt;/strong&gt; - References &lt;code&gt;positive-review.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test 2: Detailed Negative Review&lt;/strong&gt; - References &lt;code&gt;negative-review.txt&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test 3: Mixed Sentiment Review&lt;/strong&gt; - References &lt;code&gt;mixed-review.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test 4: Suspicious/Fake Review&lt;/strong&gt; - References &lt;code&gt;suspicious-review.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each test loads its respective product review using the &lt;code&gt;file://&lt;/code&gt; syntax, which tells promptfoo to read the content from the specified file and inject it into the &lt;code&gt;review_text&lt;/code&gt; variable in our prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Layered Assertions
&lt;/h3&gt;

&lt;p&gt;Notice that we're using multiple types of assertions for comprehensive validation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;is-json&lt;/code&gt;&lt;/strong&gt; - Ensures the output is valid JSON format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;contains-json&lt;/code&gt;&lt;/strong&gt; - Checks for specific key-value pairs in the response&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;javascript&lt;/code&gt;&lt;/strong&gt; - Uses inline JavaScript for custom validation logic (like checking sentiment and confidence scores)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;llm-rubric&lt;/code&gt;&lt;/strong&gt; - Uses an LLM to evaluate whether the output meets human-readable criteria&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The inline JavaScript assertions are particularly powerful for complex validation. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sentiment&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;positive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This validates both the sentiment classification AND ensures the AI is confident in its assessment, helping us catch edge cases where the model might be uncertain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation &amp;amp; Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install as a dev dependency in your project&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; promptfoo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run the test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run the tests&lt;/span&gt;
npx promptfoo &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; promptfoo-product-reviews/analyze-review-spec.yaml &lt;span class="nt"&gt;--no-cache&lt;/span&gt;
&lt;span class="c"&gt;# View the results in web viewer&lt;/span&gt;
npx promptfoo view &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding the Results
&lt;/h2&gt;

&lt;p&gt;The web viewer has a lot going on, and I could do an entire walkthrough of its features. For now, let's focus on the key insights it provides into the test and evaluation results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdmp8rt9sa4sq7d36brt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdmp8rt9sa4sq7d36brt.png" alt="promptfoo web viewer" width="800" height="519"&gt;&lt;/a&gt;&lt;br&gt;
The results are displayed in a grid, and you can see our prompt in the first row.  The 2nd row shows the results of our first scenario, the positive review.&lt;/p&gt;

&lt;p&gt;Note the prompt did a pretty good job at analyzing the review based on our requirements, and displays the actual response from the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sentiment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"positive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key_features_mentioned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"battery life"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sound quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"comfort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"touch controls"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main_complaints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"case is bulky"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main_praise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"excellent battery life"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crisp and clear sound quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"comfortable during workouts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reliable touch controls"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"suspected_fake"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fake_indicators"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The reviewer expresses high satisfaction with the wireless earbuds, highlighting their excellent battery life and sound quality while noting a minor complaint about the case size."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding the tests to CI
&lt;/h2&gt;

&lt;p&gt;This is a great start, but we can take this a step further.  Since promptfoo just runs from the command line, we can include it as a regression test in our CI pipeline and ensure that future prompt changes don't break these tests. &lt;/p&gt;

&lt;p&gt;If we make changes to the prompt, or change the LLM provider, we can re-run this test and see if the results change.  If they do, we can investigate why.  &lt;/p&gt;

&lt;p&gt;As requirements change and morph, we can adapt the tests accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;In this post, we've explored how to set up a comprehensive testing framework for AI-generated product reviews using promptfoo. By defining clear test scenarios and leveraging multi-layered assertions, we can ensure our AI behaves as expected across a range of inputs.&lt;/p&gt;

&lt;p&gt;It might not surprise you to learn that my prompt was not perfect the first time.  Since I setup my automated tests first, it made it easy to iterate on the prompt development.  Sounds like test driven development, huh?&lt;/p&gt;

&lt;p&gt;That's it for now.  Stay tuned for another promptfoo post before too long!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>promptengineering</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Automate the Testing of Your LLM Prompts</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Sun, 24 Aug 2025 23:06:14 +0000</pubDate>
      <link>https://dev.to/leading-edje/automate-the-testing-of-your-llm-prompts-5038</link>
      <guid>https://dev.to/leading-edje/automate-the-testing-of-your-llm-prompts-5038</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;On a recent client engagement, we needed a mechanism to validate LLM responses for an application that used AI to summarize customer service call transcripts.&lt;/p&gt;

&lt;p&gt;The requirements were clear: each summary had to capture specific details (customer names, account numbers, actions taken, resolution details, etc.), and our validation process needed to be automated and repeatable. We needed to test our custom summarization prompts with the same rigor we apply to traditional software: pass/fail assertions, regression baselines, and systematic tracking.&lt;/p&gt;

&lt;p&gt;That's where &lt;a href="https://www.promptfoo.dev/" rel="noopener noreferrer"&gt;promptfoo&lt;/a&gt; came in.  Promptfoo let us codify these requirements into automated tests and iterate on prompt improvements with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Testing LLM Responses Is Different (And Why You Should Care)
&lt;/h2&gt;

&lt;p&gt;As software engineers and quality professionals, we're used to deterministic systems where the same input always produces the same output. LLM responses break that assumption: the same prompt can yield different valid answers, so traditional assertion patterns are often insufficient.&lt;/p&gt;

&lt;p&gt;Here's the challenge: How can you verify a prompt's response is contextually accurate when the response can vary with every request?&lt;/p&gt;

&lt;p&gt;The solution is to shift from testing exact outputs to testing output quality, accuracy, and safety. You need assertions that can evaluate whether a response contains required information, follows guidelines, and avoids harmful content, regardless of the exact wording.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional testing falls short with LLM prompt responses because:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-deterministic responses&lt;/strong&gt;: Same input, different valid outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context-dependent behavior&lt;/strong&gt;: Quality depends on conversation history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety concerns&lt;/strong&gt;: Content filtering and moderation requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance variability&lt;/strong&gt;: Response times and costs fluctuate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've been struggling with manual testing of AI features or relying on trial-and-error for prompt engineering, this guide will show you how promptfoo brings systematic testing to AI development.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Promptfoo?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Promptfoo&lt;/strong&gt; is an open-source testing framework specifically designed to enable test-driven development for LLM applications with structured, automated evaluation of prompts, models, and outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key capabilities:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assertion-based validation&lt;/strong&gt; with pass/fail criteria familiar to QA engineers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side-by-side prompt comparison&lt;/strong&gt; for A/B testing different prompts and approaches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated regression testing&lt;/strong&gt; to catch quality degradation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD integration&lt;/strong&gt; for your existing pipelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-model support&lt;/strong&gt; (OpenAI, Anthropic, Google, Azure, local models)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Promptfoo brings familiar testing methodologies to AI development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test-driven development&lt;/strong&gt; instead of trial-and-error and/or hoping for the best&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression testing&lt;/strong&gt; to catch quality degradation
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance monitoring&lt;/strong&gt; (latency, cost, accuracy)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started: Hands-On Examples
&lt;/h2&gt;

&lt;p&gt;The best way to understand promptfoo is to see it in action. Let's start with installation and work through practical examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation &amp;amp; Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install as a dev dependency in your project&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; promptfoo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration: YAML-Driven Testing
&lt;/h3&gt;

&lt;p&gt;Promptfoo uses YAML configuration files to define your tests. This approach will feel familiar if you've worked with other testing frameworks or CI/CD tools. The YAML file specifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt;: The actual prompts you want to test&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Providers&lt;/strong&gt;: Which AI models to use (OpenAI, Anthropic, Azure, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt;: Input variables and assertions used to validate responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test scenarios&lt;/strong&gt;: Different inputs and expected behaviors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This declarative approach makes it easy to version control your AI tests and collaborate with your team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 1: Simple Dataset Generation
&lt;/h3&gt;

&lt;p&gt;Let's start with a simple example. We want to test a prompt that generates a list of random numbers.  Of course an LLM is really not the right place to do this, but this is just for example purposes.  &lt;/p&gt;

&lt;p&gt;We're going to test this prompt against two different models: Claude and GPT-5-mini.  (FYI, you will need API tokens for any paid model you are referencing.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# examples-for-blog/ten_numbers.yaml&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generating a random list of integers between a range&lt;/span&gt;

&lt;span class="na"&gt;prompts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;are&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;JSON-only&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;responder.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OUTPUT&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;EXACTLY&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;JSON&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;array&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NOTHING&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ELSE.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Example:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;[10,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;20,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;30].&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Generate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ordered&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ten&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;random&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;integers&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;between&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{start}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{end}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(inclusive).&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;numeric&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;values&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;quotes),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sorted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ascending&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;include&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;commentary&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fences."&lt;/span&gt;

&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anthropic:messages:claude-3-haiku-20240307&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai:chat:gpt-5-mini&lt;/span&gt;

&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
      &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
    &lt;span class="na"&gt;assert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;is-json&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"type": "array",&lt;/span&gt;
            &lt;span class="s"&gt;"minItems": 10,&lt;/span&gt;
            &lt;span class="s"&gt;"maxItems": 10,&lt;/span&gt;
            &lt;span class="s"&gt;"items": {&lt;/span&gt;
              &lt;span class="s"&gt;"type": "integer",&lt;/span&gt;
              &lt;span class="s"&gt;"minimum": 10,&lt;/span&gt;
              &lt;span class="s"&gt;"maximum": 1000&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How this works:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When promptfoo runs this test, it substitutes the variables (&lt;code&gt;start: 10&lt;/code&gt; and &lt;code&gt;end: 1000&lt;/code&gt;) into the prompt and sends it to both Claude and GPT-5-mini. Each model generates a response.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;is-json&lt;/code&gt; assertion is evaluated by promptfoo after it parses the model output as JSON. In other words, promptfoo performs the JSON parsing and schema validation (not the model). If the model returns something that isn't valid JSON or doesn't match the schema, the assertion will fail and promptfoo will report the parsing error and the schema mismatch.&lt;/p&gt;

&lt;p&gt;This example demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variable substitution&lt;/strong&gt; with &lt;code&gt;{{start}}&lt;/code&gt; and &lt;code&gt;{{end}}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple model comparison&lt;/strong&gt; (Claude vs GPT-5-mini)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programmatic validation&lt;/strong&gt; using &lt;code&gt;is-json&lt;/code&gt; so validation happens in promptfoo, not in the LLM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Running the test is easy:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# run the test&lt;/span&gt;
npx promptfoo &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; examples-for-blog/ten_numbers.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see a side-by-side comparison showing how each model performed and whether they passed the validation criteria:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# open the web report for the last run&lt;/span&gt;
npx promptfoo view
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is our web view of the test results.  Note you can see variables, prompts, model responses, validation outcomes, and even performance and cost metrics, all in one place.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1qoffb63yzn6ww5ip6lq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1qoffb63yzn6ww5ip6lq.png" alt="Web view of promptfoo results" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 2: Call Summary Validation (Real-World Use Case)
&lt;/h3&gt;

&lt;p&gt;So Example 1 was interesting, but let's look at how we can validate the output of a prompt by using an LLM to grade that output.&lt;/p&gt;

&lt;p&gt;Here's a more complex example based on our actual client engagement I described earlier - testing an AI system that summarizes customer service calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# examples-for-blog/customer-call-summary.yaml&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Call Summary Quality Testing&lt;/span&gt;

&lt;span class="na"&gt;prompts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;Summarize this customer service call. Keep the summary succinct without unnecessary details. Pay special attention to include the agent's demeanor and indicate if they ever seemed unprofessional. Include:&lt;/span&gt;
    &lt;span class="s"&gt;- Customer name and account number&lt;/span&gt;
    &lt;span class="s"&gt;- Issue description&lt;/span&gt;
    &lt;span class="s"&gt;- Actions taken by agent&lt;/span&gt;
    &lt;span class="s"&gt;- Any order number that is mentioned&lt;/span&gt;
    &lt;span class="s"&gt;- Resolution status&lt;/span&gt;

    &lt;span class="s"&gt;Call transcript: {{transcript}}&lt;/span&gt;

&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;openai:chat:gpt-5-mini&lt;/span&gt;

&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;transcript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;Agent: Good morning, thank you for calling customer service. This is Maria, how can I help you today?&lt;/span&gt;
        &lt;span class="s"&gt;Customer: Hi Maria, I'm calling about an order I placed last week that was supposed to be delivered two days ago, but it still hasn't arrived.&lt;/span&gt;
        &lt;span class="s"&gt;Agent: I'm sorry to hear about the delay with your order. I'd be happy to help you track that down. Can I start by getting your first and last name please?&lt;/span&gt;
        &lt;span class="s"&gt;Customer: Yes, it's David Rodriguez.&lt;/span&gt;
        &lt;span class="s"&gt;Agent: Thank you Mr. Rodriguez. And can I also get your account number to verify your account?&lt;/span&gt;
        &lt;span class="s"&gt;Customer: Sure, it's account number 78942.&lt;/span&gt;
        &lt;span class="s"&gt;Agent: Perfect, thank you. Now, can you provide me with the order number for the package you're expecting?&lt;/span&gt;
        &lt;span class="s"&gt;Customer: Yes, the order number is ORD-2024-5583.&lt;/span&gt;
        &lt;span class="s"&gt;Agent: Great, and when did you place this order?&lt;/span&gt;
        &lt;span class="s"&gt;Customer: I placed it last Tuesday, January 16th.&lt;/span&gt;
        &lt;span class="s"&gt;Agent: Thank you for that information. Let me pull up your order details here... Okay, I can see order ORD-2024-5583 placed on January 16th, and you're absolutely right - it was originally scheduled for delivery on January 22nd. I sincerely apologize for this delay, Mr. Rodriguez.&lt;/span&gt;
        &lt;span class="s"&gt;Customer: So what happened? Why didn't it arrive when it was supposed to?&lt;/span&gt;
        &lt;span class="s"&gt;Agent: It looks like there was a sorting delay at our distribution center that affected several shipments in your area. Your package is currently in transit and I can see it's now scheduled to be delivered this Friday, January 26th, by end of day.&lt;/span&gt;
        &lt;span class="s"&gt;Customer: Friday? That's three days later than promised. This is really inconvenient.&lt;/span&gt;
        &lt;span class="s"&gt;Agent: I completely understand your frustration, and I apologize again for the inconvenience this has caused. To make up for the delay, I'm going to issue a $15 credit to your account, and I'll also send you tracking information via email so you can monitor the package's progress.&lt;/span&gt;
        &lt;span class="s"&gt;Customer: Okay, well I appreciate that. Will I get a notification when it's actually delivered?&lt;/span&gt;
        &lt;span class="s"&gt;Agent: Absolutely. You'll receive both an email and text notification once the package is delivered, and the tracking information will show real-time updates. Is there anything else I can help you with today?&lt;/span&gt;
        &lt;span class="s"&gt;Customer: No, that covers it. Thank you for your help, Maria.&lt;/span&gt;
        &lt;span class="s"&gt;Agent: You're very welcome to never ever call me again, Mr. Rodriguez. Again, I apologize for the delay, and thank you for your patience. Have a great day!&lt;/span&gt;
  &lt;span class="na"&gt;assert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;contains&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;David&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Rodriguez"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;contains&lt;/span&gt;  
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORD-2024-5583"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llm-rubric&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summary&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;should&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;indicate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;whether&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;seemed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;professional&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;should&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;include&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;details,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;including&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;taken&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;agent,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;resolution,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;compensation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;offered."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prompt embeds a long customer-service phone transcript that the model is asked to summarize succinctly while preserving key facts. To verify correctness we include a couple of deterministic assertions (exact-match checks) for the customer's name and the order number so those values must appear in the summary.&lt;/p&gt;

&lt;p&gt;We also include an &lt;code&gt;llm-rubric&lt;/code&gt; asset: promptfoo will call an LLM to grade the generated summary against the supplied rubric text, allowing us to assert on higher-level quality attributes such as professionalism, completeness, and whether the agent's actions and compensation were described.&lt;/p&gt;

&lt;p&gt;Now I can run that test and see how we do!!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# run the test&lt;/span&gt;
npx promptfoo &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; examples-for-blog/customer-call-summary.yaml
&lt;span class="c"&gt;# View results&lt;/span&gt;
npx promptfoo view
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here are our results:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkdbbvq2en4mymu8saops.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkdbbvq2en4mymu8saops.png" alt="Web view of call summary results" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note the prompt specifically requests to indicate the agent's demeanor, and we use the rubric to verify the output contains it.  Since I never trust a test unless I can see it fail, I'm going to temporarily remove the mention of demeanor in the prompt, but leave the assert alone, so we should get a failure.  Drumroll, please…&lt;/p&gt;

&lt;p&gt;And we do! &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fge2be458rkpig2yx2bri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fge2be458rkpig2yx2bri.png" alt="Web view of call summary failure results" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the test caught the error with our prompt:&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8vj6zvzubkg7mnr4pjp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8vj6zvzubkg7mnr4pjp.png" alt="failure message" width="800" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I got a little long-winded with this post, but I hope someone out there finds it useful. Promptfoo represents a paradigm shift from manual AI testing to systematic, automated evaluation. By bringing familiar testing methodologies to AI development, it enables teams to build reliable, secure, and high-quality AI applications.&lt;/p&gt;

&lt;p&gt;I'll be back soon with some more promptfoo content, and you should certainly check out the awesome documentation at &lt;a href="https://www.promptfoo.dev/" rel="noopener noreferrer"&gt;promptfoo.dev&lt;/a&gt; for excellent resources for getting started.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>promptengineering</category>
      <category>testing</category>
      <category>testdev</category>
    </item>
    <item>
      <title>Automating Browser-Based Performance Testing</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Sun, 17 Aug 2025 15:01:57 +0000</pubDate>
      <link>https://dev.to/leading-edje/automating-browser-based-performance-testing-1n6</link>
      <guid>https://dev.to/leading-edje/automating-browser-based-performance-testing-1n6</guid>
      <description>&lt;p&gt;Website performance directly affects what users feel and what your business earns.&lt;/p&gt;

&lt;p&gt;One way of identifying performance issues is via API-based load testing tools such as &lt;a href="https://k6.io" rel="noopener noreferrer"&gt;k6&lt;/a&gt;. API load tests tell you whether your services scale and how quickly they respond under load, but they don’t measure the full user experience.&lt;/p&gt;

&lt;p&gt;If you focus &lt;em&gt;only&lt;/em&gt; on load testing your backend, you might still ship a &lt;strong&gt;slow&lt;/strong&gt; or &lt;strong&gt;jittery&lt;/strong&gt; site because of render‑blocking CSS/JavaScript, heavy images/fonts, main‑thread work, layout shifts, and other front-end issues.&lt;br&gt;&lt;br&gt;
Ultimately users don't care where the performance issue resides, they just know your site is "slow".&lt;/p&gt;

&lt;p&gt;This slow performance can cost you customers, revenue, search visibility, and trust.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is Lighthouse?
&lt;/h2&gt;

&lt;p&gt;Lighthouse is an automated auditor built by Google and is part of the Chrome DevTools experience. While this post focuses on performance, Lighthouse also audits and provides actionable recommendations for accessibility, best practices, and SEO.&lt;/p&gt;
&lt;h2&gt;
  
  
  How Lighthouse works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Launches Chrome and navigates to your page using the Chrome DevTools Protocol.&lt;/li&gt;
&lt;li&gt;Emulates device, network, and CPU to keep runs comparable.&lt;/li&gt;
&lt;li&gt;Records a performance trace and analyzes it against a set of audits.&lt;/li&gt;
&lt;li&gt;Outputs scores and detailed metrics with fix ideas.&lt;/li&gt;
&lt;li&gt;Can be included in your CI pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Core Web Vitals: what they mean and why they matter
&lt;/h2&gt;

&lt;p&gt;These user‑focused metrics map to how fast content shows up, how responsive the page feels, and how stable it looks.&lt;/p&gt;
&lt;h3&gt;
  
  
  Core Web Vitals at a glance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Plain meaning&lt;/th&gt;
&lt;th&gt;Good target&lt;/th&gt;
&lt;th&gt;What you’ll see in Lighthouse&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LCP (Largest Contentful Paint)&lt;/td&gt;
&lt;td&gt;Time to show the largest thing in the initial viewport (often the primary image or a big text block).&lt;/td&gt;
&lt;td&gt;≤ 2.5 s&lt;/td&gt;
&lt;td&gt;LCP value in the Metrics section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FID (First Input Delay)&lt;/td&gt;
&lt;td&gt;Delay from a user’s first tap/click to when the page can start handling it. In Lighthouse runs, use Total Blocking Time (TBT) as the responsiveness indicator.&lt;/td&gt;
&lt;td&gt;FID ≤ 100 ms; aim for low TBT&lt;/td&gt;
&lt;td&gt;TBT value in the Metrics section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS (Cumulative Layout Shift)&lt;/td&gt;
&lt;td&gt;How much content unexpectedly moves while the page loads (visual stability).&lt;/td&gt;
&lt;td&gt;≤ 0.1&lt;/td&gt;
&lt;td&gt;CLS score in Metrics/Diagnostics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Sample Lighthouse Report
&lt;/h2&gt;

&lt;p&gt;Regardless of how you run Lighthouse, you get a detailed report with scores, metrics, and prioritized suggestions.&lt;/p&gt;

&lt;p&gt;Overall scores:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbo60wh9t3a441la40txv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbo60wh9t3a441la40txv.png" alt="Scores" width="800" height="649"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What went wrong?&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyde8xgzyh19oso4pxa0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyde8xgzyh19oso4pxa0w.png" alt="Diagnostics" width="676" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What looks good?&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pazpjoce7wzrmk8sobd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pazpjoce7wzrmk8sobd.png" alt="Passed audits" width="628" height="730"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Running Lighthouse
&lt;/h2&gt;

&lt;p&gt;Lighthouse can be run in a number of ways, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome DevTools (UI)&lt;/li&gt;
&lt;li&gt;Command line (CLI)&lt;/li&gt;
&lt;li&gt;Node module (programmatic)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Run Lighthouse from Chrome DevTools
&lt;/h3&gt;

&lt;p&gt;Open your site in Chrome → Right‑click Inspect → Lighthouse tab → Set your analysis options → Analyze. This generates a full HTML report inside DevTools.&lt;/p&gt;
&lt;h3&gt;
  
  
  Run Lighthouse from the command line
&lt;/h3&gt;

&lt;p&gt;Install Lighthouse (requires Node.js):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; lighthouse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Basic mobile audit and open the HTML report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lighthouse https://www.demoblaze.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;html &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./reports/lighthouse.html &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--view&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Export JSON for automation or tracking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lighthouse https://www.demoblaze.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./reports/lighthouse.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--chrome-flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--headless"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Desktop profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lighthouse https://www.demoblaze.com &lt;span class="nt"&gt;--preset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;desktop &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;html &lt;span class="nt"&gt;--output-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./reports/desktop.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use throttling to simulate slower networks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lighthouse https://www.demoblaze.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--throttling-method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;simulate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--throttling&lt;/span&gt;.rttMs&lt;span class="o"&gt;=&lt;/span&gt;150 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--throttling&lt;/span&gt;.throughputKbps&lt;span class="o"&gt;=&lt;/span&gt;1638.4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--throttling&lt;/span&gt;.cpuSlowdownMultiplier&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;html &lt;span class="nt"&gt;--output-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./reports/consistent.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Focus audits on key performance metrics with a config (&lt;code&gt;lighthouse-config.js&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lighthouse:default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;onlyAudits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first-contentful-paint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;largest-contentful-paint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cumulative-layout-shift&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;total-blocking-time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;throttlingMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;simulate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;throttling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rttMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;throughputKbps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1638.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cpuSlowdownMultiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with the config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lighthouse https://www.demoblaze.com &lt;span class="nt"&gt;--config-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./lighthouse-config.js &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;html &lt;span class="nt"&gt;--output-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./reports/focused.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Programmatic usage (Node)
&lt;/h3&gt;

&lt;p&gt;Why use this? Programmatic runs let you script real user interactions and measure performance along a flow (navigations, clicks, route changes). With Puppeteer + Lighthouse User Flows you can drive the browser, capture metrics per step, and generate a single report—perfect for CI, regression checks, and measuring critical journeys like signup or checkout.&lt;/p&gt;

&lt;p&gt;Note: Lighthouse currently only supports Puppeteer for programmatic user flows.&lt;/p&gt;

&lt;p&gt;Install packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i lighthouse puppeteer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save as &lt;code&gt;user-flow.mjs&lt;/code&gt; and run with &lt;code&gt;node user-flow.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;startFlow&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lighthouse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Navigate to Demoblaze&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.demoblaze.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Interaction-initiated navigation via a callback function&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[href="index.html"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Start/End a navigation around a user action&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startNavigation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a#cartur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// open Cart&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endNavigation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./reports/lh-flow-report.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateReport&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Saved ./reports/lh-flow-report.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrap‑up
&lt;/h2&gt;

&lt;p&gt;Start by running Lighthouse in DevTools (fast feedback) or the CLI (repeatable results). Focus on three things: LCP (how fast the main content shows), TBT (how responsive it feels), and CLS (how stable it looks).&lt;/p&gt;

&lt;p&gt;What’s next in this series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Containerize Lighthouse runs with Docker for consistent local and CI environments&lt;/li&gt;
&lt;li&gt;Add Lighthouse checks to a GitHub Actions workflow with performance budgets and PR comments&lt;/li&gt;
&lt;li&gt;Export key metrics to Prometheus for time‑series storage&lt;/li&gt;
&lt;li&gt;Visualize trends and budgets in a Grafana dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
      <category>testing</category>
      <category>testdev</category>
    </item>
    <item>
      <title>Open Source Load Testing with k6, Docker, Prometheus, and Grafana</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Sun, 18 May 2025 16:13:44 +0000</pubDate>
      <link>https://dev.to/leading-edje/open-source-load-testing-with-k6-docker-prometheus-and-grafana-5ej6</link>
      <guid>https://dev.to/leading-edje/open-source-load-testing-with-k6-docker-prometheus-and-grafana-5ej6</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Load testing is crucial for ensuring your applications can handle expected load volumes. In this guide, we'll set up a complete load testing environment using k6 for testing, Prometheus for metrics collection, and Grafana for visualization—all orchestrated with Docker.&lt;/p&gt;

&lt;p&gt;Although there are paid versions of these products, this guide will focus exclusively on a basic setup with their open source Docker images. &lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docker and Docker Compose installed&lt;/li&gt;
&lt;li&gt;Basic understanding of load testing concepts&lt;/li&gt;
&lt;li&gt;Familiarity with Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Our setup consists of four main components:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;k6&lt;/strong&gt;: An open-source load testing tool that enables you to write test scripts in JavaScript to simulate real user traffic, measure application performance, and export detailed metrics for analysis.&lt;br&gt;
&lt;strong&gt;Application&lt;/strong&gt;: A simple API-based application to test&lt;br&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt;: An open-source monitoring and alerting toolkit that collects, stores, and queries time-series metrics from k6 and other sources, making them available for analysis and visualization.&lt;br&gt;
&lt;strong&gt;Grafana&lt;/strong&gt;: An open-source analytics and visualization platform that lets you create interactive dashboards and graphs from a wide variety of data sources—including Prometheus, InfluxDB, Elasticsearch, MySQL, PostgreSQL, and many others.&lt;/p&gt;

&lt;p&gt;These components will be implemented with 4 Docker containers. Here's how these components interact:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmw4ewsyvufil8p0umt43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmw4ewsyvufil8p0umt43.png" alt="k6 architecture diagram" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Data Flow:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Load generation&lt;/strong&gt;: our k6 script sends HTTP requests to the Sample API to simulate user traffic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics Export&lt;/strong&gt;: as the test runs, performance metrics from k6 are exported to Prometheus via remote write&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Query&lt;/strong&gt;: Grafana uses PromQL to query Prometheus for metrics&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All components run within the same Docker network, enabling seamless communication between services.&lt;/p&gt;
&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k6-prometheus-grafana/
├── docker-compose.yml
├── prometheus/
│   └── prometheus.yml
├── grafana/
│   └── dashboards/
│       └── k6-dashboard.json
├── k6/
│   └── script.js
└── sample-api/
    └── Dockerfile
    └── server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 1: Create the Sample API
&lt;/h2&gt;

&lt;p&gt;First, let's create a simple Node.js API to test against:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sample-api/server.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Simulate some processing delay&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`User &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Simulate user creation&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User created successfully&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; 
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server running on port 3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And add a docker file that will start the app: &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sample-api/Dockerfile&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:16-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Create k6 Test Script
&lt;/h2&gt;

&lt;p&gt;This JavaScript test script defines how k6 will interact with our sample API during the load test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;k6/script.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k6/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Trend&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k6/metrics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Custom metrics - these allow us to track specific aspects of our test&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;errors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// Tracks percentage of errors&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myCounter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my_counter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// Simple incrementing counter&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responseTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Trend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;response_time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Tracks response time distribution&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Ramp up to 5 virtual users over 30 seconds&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;90s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Ramp to from 5 to 20 virtual users over 90 seconds&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3m&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Stay at 20 virtual users for 3 minutes&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// Gradually ramp down to 0 over 30 seconds&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;http_req_duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p(95)&amp;lt;500&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// 95% of requests must complete in less than 500ms for the test to pass&lt;/span&gt;
    &lt;span class="na"&gt;http_req_failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate&amp;lt;0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;    &lt;span class="c1"&gt;// Test fails if more than 10% of requests fail&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://sample-api:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Test GET endpoint - fetches a random user&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;getResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET status is 200&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET response time &amp;lt; 500ms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Track custom metrics for this request&lt;/span&gt;
  &lt;span class="nx"&gt;errorRate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;responseTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;myCounter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Pause for 1 second between requests&lt;/span&gt;

  &lt;span class="c1"&gt;// Test POST endpoint - creates a new user&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;postResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/users`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`TestUser_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`test_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;@example.com`&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST status is 201&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST response time &amp;lt; 1000ms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;errorRate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;myCounter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Configure Prometheus
&lt;/h2&gt;

&lt;p&gt;Prometheus is an open-source monitoring and alerting toolkit that collects and stores time-series metrics. The configuration below sets up Prometheus to scrape metrics from both itself and the k6 load testing tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;prometheus/prometheus.yml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;      &lt;span class="c1"&gt;# How frequently to scrape targets by default&lt;/span&gt;
  &lt;span class="na"&gt;evaluation_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;  &lt;span class="c1"&gt;# How frequently to evaluate rules&lt;/span&gt;
&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prometheus'&lt;/span&gt;  &lt;span class="c1"&gt;# Self-monitoring configuration&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost:9090'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Prometheus's own metrics endpoint&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;k6'&lt;/span&gt;          &lt;span class="c1"&gt;# Configuration to scrape k6 metrics&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;k6:6565'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# k6's metrics endpoint (using Docker service name)&lt;/span&gt;
    &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;     &lt;span class="c1"&gt;# More frequent scraping for k6 during tests&lt;/span&gt;
    &lt;span class="na"&gt;metrics_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/metrics&lt;/span&gt;  &lt;span class="c1"&gt;# Path where metrics are exposed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once Prometheus is collecting metrics, we'll be able to query this data directly or visualize it through Grafana in the next steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Grafana Dashboard Configuration
&lt;/h2&gt;

&lt;p&gt;Create a dashboard provisioning file for automatic setup:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;grafana/dashboards/dashboard.yml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default'&lt;/span&gt;
    &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;folder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;
    &lt;span class="na"&gt;disableDeletion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;editable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/grafana/provisioning/dashboards&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Docker Compose Configuration
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;docker-compose.yml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Sample API service to be load tested by k6&lt;/span&gt;
  &lt;span class="na"&gt;sample-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./sample-api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt; &lt;span class="c1"&gt;# Exposes API on localhost:3000&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k6-net&lt;/span&gt;

  &lt;span class="c1"&gt;# Prometheus for metrics collection&lt;/span&gt;
  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prom/prometheus:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9090:9090"&lt;/span&gt; &lt;span class="c1"&gt;# Prometheus UI available at localhost:9090&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml&lt;/span&gt; &lt;span class="c1"&gt;# Custom config&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--config.file=/etc/prometheus/prometheus.yml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--storage.tsdb.path=/prometheus'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.console.libraries=/etc/prometheus/console_libraries'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.console.templates=/etc/prometheus/consoles'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.enable-lifecycle'&lt;/span&gt; &lt;span class="c1"&gt;# Allows config reloads without restart&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.enable-remote-write-receiver'&lt;/span&gt; &lt;span class="c1"&gt;# Enables remote write endpoint for k6&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k6-net&lt;/span&gt;

  &lt;span class="c1"&gt;# Grafana for dashboarding and visualization&lt;/span&gt;
  &lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana/grafana:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3001:3000"&lt;/span&gt; &lt;span class="c1"&gt;# Grafana UI available at localhost:3001&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SECURITY_ADMIN_PASSWORD=admin&lt;/span&gt; &lt;span class="c1"&gt;# Default admin password&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;grafana-storage:/var/lib/grafana&lt;/span&gt; &lt;span class="c1"&gt;# Persistent storage for Grafana data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./grafana/dashboards:/etc/grafana/provisioning/dashboards&lt;/span&gt; &lt;span class="c1"&gt;# Pre-provisioned dashboards&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k6-net&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt; &lt;span class="c1"&gt;# Waits for Prometheus to be ready&lt;/span&gt;

  &lt;span class="c1"&gt;# k6 load testing tool with Prometheus remote write output&lt;/span&gt;
  &lt;span class="na"&gt;k6&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana/k6:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;k6&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6565:6565"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;K6_PROMETHEUS_RW_SERVER_URL=http://prometheus:9090/api/v1/write&lt;/span&gt; &lt;span class="c1"&gt;# Prometheus remote write endpoint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;K6_PROMETHEUS_RW_TREND_STATS=p(95),p(99),min,max&lt;/span&gt; &lt;span class="c1"&gt;# Custom trend stats&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./k6:/scripts&lt;/span&gt; &lt;span class="c1"&gt;# Mounts local k6 scripts&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run --out experimental-prometheus-rw /scripts/script.js&lt;/span&gt; &lt;span class="c1"&gt;# Runs the main k6 script&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k6-net&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sample-api&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;grafana-storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Named volume for Grafana data&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;k6-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt; &lt;span class="c1"&gt;# Isolated network for all services&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Start the stack
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start all services:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Setting Up a Pre-built K6 Dashboard
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Access Grafana&lt;/strong&gt;: Navigate to &lt;a href="http://localhost:3001" rel="noopener noreferrer"&gt;http://localhost:3001&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login&lt;/strong&gt;: Use admin/admin (you'll be prompted to change the password)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add Prometheus Data Source First&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Go to Configuration → Data Sources&lt;/li&gt;
&lt;li&gt;Click "Add data source"&lt;/li&gt;
&lt;li&gt;Select "Prometheus"&lt;/li&gt;
&lt;li&gt;Set URL to: &lt;code&gt;http://prometheus:9090&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click "Save &amp;amp; Test"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import K6 Dashboard&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Click the "+" icon in the left sidebar&lt;/li&gt;
&lt;li&gt;Select "Import"&lt;/li&gt;
&lt;li&gt;Use one of these dashboard IDs for Prometheus:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;19665&lt;/strong&gt; - K6 Prometheus (recommended)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10660&lt;/strong&gt; - K6 Load Testing Results (Prometheus)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;19634&lt;/strong&gt; - K6 Performance Test Dashboard&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click "Load"&lt;/li&gt;
&lt;li&gt;Select your Prometheus data source&lt;/li&gt;
&lt;li&gt;Click "Import"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 8: Run the load test
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the k6 test:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; k6 run &lt;span class="nt"&gt;--out&lt;/span&gt; experimental-prometheus-rw /scripts/script.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the test runs, k6 will send API requests to the sample API, and metrics will be collected and sent to Prometheus.  You can monitor the test progress in the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Monitor your test run in Grafana
&lt;/h2&gt;

&lt;p&gt;The Grafana UI available at &lt;a href="http://localhost:3001" rel="noopener noreferrer"&gt;http://localhost:3001&lt;/a&gt;.  Select your Dashboard from the left nav and you can monitor your test real time with the Grafana dashboard, which should look something like this:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqyzh6fbej5ta7zyjcr43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqyzh6fbej5ta7zyjcr43.png" alt="Grafana dashboard screenshot" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Stop and remove all containers and volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The point of this post was just to provide awareness of the open source options available to you as you consider k6 for load testing.  I skimmed over a lot of detail and explanation about k6, Prometheus, and Grafana.  I will likely fill in some detail with future posts.  Until then, this setup provides a complete observability stack for K6 load testing.&lt;/p&gt;

&lt;p&gt;The Docker-based approach ensures consistency across environments and makes it easy to integrate into CI/CD pipelines. And FYI, you can find all the code from this blog post &lt;a href="https://github.com/dwwhalen/k6-prometheus-grafana" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading and let me know if you have any questions or suggestions for future posts!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>performance</category>
      <category>k6</category>
      <category>grafana</category>
    </item>
    <item>
      <title>Automating Accessibility Testing With Playwright</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Sat, 07 Dec 2024 23:23:24 +0000</pubDate>
      <link>https://dev.to/leading-edje/automating-accessibility-testing-with-playwright-3el7</link>
      <guid>https://dev.to/leading-edje/automating-accessibility-testing-with-playwright-3el7</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/leading-edje/accessibility-testing-with-chrome-extension-53kn"&gt;previous post&lt;/a&gt;, I showed you how to use the axe DevTools chrome extension to test for accessibility issues on a webpage.  Today, I'm going to show you how to do the same thing with Playwright, enabling you to automate accessibility testing in your CI/CD pipeline.   Everything I'm going to demo can also be found in my &lt;a href="https://github.com/dwwhalen/cypress-accessibility" rel="noopener noreferrer"&gt;sample repo&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  The axe-core package
&lt;/h1&gt;

&lt;p&gt;The &lt;a href="https://www.npmjs.com/package/axe-core" rel="noopener noreferrer"&gt;axe-core&lt;/a&gt; package is a JavaScript library that can be used to run accessibility tests on a webpage.  It's the same library that powers the axe DevTools extension that we looked at in my &lt;a href="https://dev.to/leading-edje/accessibility-testing-with-chrome-extension-53kn"&gt;previous post&lt;/a&gt;, but it can be run programmatically in a variety of environments.  &lt;/p&gt;

&lt;p&gt;We're going to use it with Playwright, but axe-core packages exist for a number of automated testing frameworks, including Cypress, Selenium, WebdriverIO, and more.&lt;/p&gt;

&lt;h1&gt;
  
  
  Our first Playwright accessibility test
&lt;/h1&gt;

&lt;p&gt;Including accessibility tests in your Playwright suite is as simple as adding a few lines of code.  We've got a &lt;a href="https://www.washington.edu/accesscomputing/AU/before.html" rel="noopener noreferrer"&gt;sample website&lt;/a&gt; that we're going to test, and we're going to use Playwright to navigate to the page and run an accessibility check.&lt;/p&gt;

&lt;p&gt;Here's an example test that navigates to the webpage and runs an accessibility check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AxeBuilder&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@axe-core/playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accessibility University testing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Full page scan should not find accessibility issues&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.washington.edu/accesscomputing/AU/before.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLoadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessibilityScanResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AxeBuilder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessibilityScanResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;waitForLoadState('networkidle')&lt;/code&gt; waits for the page to finish loading before running the accessibility check.  This is important because we want to make sure the page is fully rendered before we check for accessibility issues.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;new AxeBuilder({ page }).analyze()&lt;/code&gt; runs the accessibility check on the page and returns the results.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expect(accessibilityScanResults.violations).toEqual([]);&lt;/code&gt; indicates that we are expecting 0 accessibility violations.  If violations are found, this test will fail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since our &lt;a href="https://www.washington.edu/accesscomputing/AU/before.html" rel="noopener noreferrer"&gt;sample website&lt;/a&gt; is specifically designed to have lots of accessibility issues, we're expecting this test to fail, and it does!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3muet1wdn1ib60a2yru.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3muet1wdn1ib60a2yru.png" alt="Failed test results in CLI" width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Reporting
&lt;/h1&gt;

&lt;p&gt;OK so we know that our test is failing, but what accessibility issues are being found?  The accessibilityScanResults object contains a lot of information about the accessibility issues that were found, and it's pretty easy to create a report with tweaks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AxeBuilder&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@axe-core/playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHtmlReport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axe-html-reporter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accessibility University testing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Full page scan should of BEFORE page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.washington.edu/accesscomputing/AU/before.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLoadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessibilityScanResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AxeBuilder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;createHtmlReport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;accessibilityScanResults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e2e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-results&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accessibility-results&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;reportFileName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`my-report.html`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessibilityScanResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createHtmlReport&lt;/code&gt; is imported from the axe-html-reporter package, and it creates an HTML report of the accessibility issues found.  The report is saved to the &lt;code&gt;e2e/test-results/accessibility-results&lt;/code&gt; directory with the name &lt;code&gt;my-report.html&lt;/code&gt;.  Here's a snippet of what the report for our test looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fquu316mkwjrxp6pj1jps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fquu316mkwjrxp6pj1jps.png" alt="sample accessibility report" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is just the first page of the report, and there are lots of details in following pages.  You'll notice that there are 50 total violations, which matches what we saw when we used on the axe DevTools extension in my &lt;a href="https://dev.to/leading-edje/accessibility-testing-with-chrome-extension-53kn"&gt;previous post&lt;/a&gt;.  &lt;/p&gt;

&lt;p&gt;The report also provides a detailed breakdown of the violations, including the rule that was violated, the impact of the violation, and a description of the issue.&lt;/p&gt;

&lt;p&gt;Just as we did with the Chrome extension, we can use this report to identify and fix the accessibility issues on our website.&lt;/p&gt;

&lt;h1&gt;
  
  
  Adding your tests to the CI/CD pipeline
&lt;/h1&gt;

&lt;p&gt;If you have some general familiarity with Playwright, you probably already know how to run your tests in your CI/CD pipeline.  If you don't, you can check out the &lt;a href="https://playwright.dev/docs/intro" rel="noopener noreferrer"&gt;Playwright documentation&lt;/a&gt; for more information.  Since these accessibility tests are just Playwright tests, you can run them in the same way you run your other Playwright tests.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this post I showed you a basic example of how to include accessibility tests in your Playwright suite.  Don't forget that  axe-core is not limited to Playwright, and can be used with a variety of automated testing frameworks, such as Cypress and Selenium.&lt;/p&gt;

&lt;p&gt;Some additional things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Although I was just testing in Chrome, axe-core supports all major browsers.  You should consider testing in multiple browsers and viewports to ensure that your website is accessible to all users with all devices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The scan can be configured to include or exclude certain rules, for example &lt;code&gt;color-contrast&lt;/code&gt; rules.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can limit the scan to a subset of the page by using the include and exclude options.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can limit the scan to a subset of the WCAG guidelines by using the rules option.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, it's probably appropriate to mention that a clean accessibility scan is a great &lt;em&gt;first&lt;/em&gt; step in making your website accessible, but it's not a silver bullet.  For example, take the test to verify the existence of alt-text for images.  The tool can tell you if an image is missing alt-text, but it can't tell you if the alt-text is meaningful. &lt;/p&gt;

&lt;p&gt;Manual validation via a screen reader is an additional step you can do to ensure that your website is accessible.  Automation tooling exists that can allow you to automate the manual screen reader testing process, but that's a topic for another post.  Stay tuned!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>qa</category>
      <category>a11y</category>
      <category>ui</category>
      <category>playwright</category>
    </item>
    <item>
      <title>Accessibility Testing with a Chrome Extension</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Mon, 02 Dec 2024 11:00:00 +0000</pubDate>
      <link>https://dev.to/leading-edje/accessibility-testing-with-chrome-extension-53kn</link>
      <guid>https://dev.to/leading-edje/accessibility-testing-with-chrome-extension-53kn</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;An accessible website ensures that all users, regardless of their physical or cognitive limitations, can navigate and interact with its content. &lt;/p&gt;

&lt;p&gt;Accessibility addresses issues faced by people with impairments, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;visual impairments (requiring screen readers or high contrast)&lt;/li&gt;
&lt;li&gt;hearing impairments (requiring captions for audio content)&lt;/li&gt;
&lt;li&gt;mobility challenges (navigating without a mouse)&lt;/li&gt;
&lt;li&gt;cognitive disabilities (requiring clear and consistent layouts).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;Web Content Accessibility Guidelines&lt;/a&gt; are the gold standard for web accessibility, offering a comprehensive set of standards to ensure websites are usable by everyone.&lt;/p&gt;

&lt;p&gt;Even with these standards, testing for accessibility can be challenging, particularly when done manually. The good news? Many aspects of accessibility testing can be automated.&lt;/p&gt;

&lt;p&gt;Let's get started with a Chrome browser extension, and a website that could really use some help with accessibility.  &lt;/p&gt;

&lt;h2&gt;
  
  
  axe DevTools extension for Chrome
&lt;/h2&gt;

&lt;p&gt;A number of browser extensions are available to help you test for accessibility issues. One of the most popular is the "axe DevTools" extension for Chrome. This tool allows you to run accessibility checks right in the browser and view the results in an easy-to-understand format.&lt;/p&gt;

&lt;p&gt;axe DevTools unlocks more features with a Pro subscription, but the free version is still quite powerful.  Everything we'll be doing here can be done with the free version.&lt;/p&gt;

&lt;p&gt;Enough chatter, let's see how this works!&lt;/p&gt;

&lt;h2&gt;
  
  
  The website
&lt;/h2&gt;

&lt;p&gt;I want to take a look at &lt;a href="https://www.washington.edu/accesscomputing/AU/before.html" rel="noopener noreferrer"&gt;this page&lt;/a&gt; and see what kind of accessibility issues I can find.  The page is specifically designed to have lots of accessibility issues, so it's a great place to start.  It looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fobnbe50qwp4uabr154gv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fobnbe50qwp4uabr154gv.png" alt="website to test" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the extension
&lt;/h2&gt;

&lt;p&gt;Install the &lt;a href="https://chromewebstore.google.com/detail/axe-devtools-web-accessib/lhdoppojpmngadmnindnejefpokejbdd" rel="noopener noreferrer"&gt;axe DevTools extension&lt;/a&gt; from the Chrome Web Store. Once installed, you can start it by selecting  axe DevTools from the Chrome Developer Tools menu:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fabjnebwetp775gv98bfn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fabjnebwetp775gv98bfn.png" alt="starting the extension" width="650" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And it will load, like this!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpehyz0gx24d70pdop3z1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpehyz0gx24d70pdop3z1.png" alt="axe extension" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running a scan
&lt;/h2&gt;

&lt;p&gt;Once you have the extension loaded, you can click the "&lt;code&gt;Full Page Scan&lt;/code&gt;" button to run a scan on the entire page.  This will check for a variety of accessibility issues and display the results in the panel, like this:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffr1u83ynzu0ee52261cq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffr1u83ynzu0ee52261cq.png" alt="full scan results" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see above there are a total of 50 issues, with 12 of them being critical. I'm going to click on the hyperlinked "12" to filter just the 12 critical issues:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fah1185fpebbm07y5uszr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fah1185fpebbm07y5uszr.png" alt="12 critical issues" width="608" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Alt-text issues
&lt;/h2&gt;

&lt;p&gt;OK now it's starting to get interesting.  Our first problem is we have two images with no alt-text.  This is a big no-no for accessibility, as screen readers rely on alt-text to describe images to users who can't see them.  &lt;/p&gt;

&lt;p&gt;When I click on the "Images must have alt-text" link above, I see this: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F782posaxvbfdsfr39iub.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F782posaxvbfdsfr39iub.png" alt="alt text issues" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is great info here.  You can see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the flagged element location in the DOM&lt;/li&gt;
&lt;li&gt;actionable steps to address the issue&lt;/li&gt;
&lt;li&gt;highlighted element on the page (pink border)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Form field issues
&lt;/h2&gt;

&lt;p&gt;Let's look at our other critical issues.  The next issue is "form fields without labels".  This is a big deal because users who rely on screen readers need to know what each form field is for.  If the form field is missing a label, or the label is not descriptive, it can be very confusing.  Looks like we have 10 of those issues.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F684fjrtnpbuuqqi1ko2a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F684fjrtnpbuuqqi1ko2a.png" alt="Form field issues" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our first example, we have the Name field, and just looking at it visually, it does appear to have a label.  But the label is not connected to the input field in the DOM.  This is a common issue with forms, and it's easy to fix.  &lt;/p&gt;

&lt;p&gt;You can see the problem element in the DOM, and the highlighted element on the page.  The actionable steps tell you to connect the label to the input field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;So there you have it, some basics about how to use the axe DevTools extension to identify and fix accessibility issues.  &lt;/p&gt;

&lt;p&gt;Also, along the top of the page you wil see an "After" button that can show you a version of the page with all of the issues addressed, and 0 accessibility issues found in the scan.&lt;/p&gt;

&lt;p&gt;Finally, we looked at the 12 critical issues, but remember there are 50 total issues, and this is just for one page!  Imagine how many issues you might find on a large site, especially one that has not been designed with accessibility in mind. &lt;/p&gt;

&lt;p&gt;In addition to the browser extension, we can do accessibility testing in the CI pipeline with tools like Playwright.  This approach ensures we catch these issues before they reach our codebase or get deployed to production.  I'll cover that in my next blog post, so stay tuned!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>ux</category>
      <category>accessibilty</category>
      <category>qa</category>
    </item>
    <item>
      <title>Playwright Visual Testing - Dynamic Data</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Mon, 11 Nov 2024 13:00:00 +0000</pubDate>
      <link>https://dev.to/leading-edje/playwright-visual-testing-dynamic-data-34ia</link>
      <guid>https://dev.to/leading-edje/playwright-visual-testing-dynamic-data-34ia</guid>
      <description>&lt;h1&gt;
  
  
  Intro
&lt;/h1&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/leading-edje/visual-testing-with-playwright-3fhh"&gt;last post&lt;/a&gt; I talked about how to get started with visual testing using Playwright. In this post, I’m going to cover how to handle dynamic data in your visual tests.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is dynamic data?
&lt;/h1&gt;

&lt;p&gt;For the purposes of this post, I’m defining dynamic data as anything on your web page that could change between test runs. This could be data that is generated randomly, data that is pulled from an API, data that is based on the current date or time, etc.&lt;/p&gt;

&lt;h1&gt;
  
  
  Options for dealing with dynamic data
&lt;/h1&gt;

&lt;p&gt;In visual testing, it's essential to ensure that tests are consistent and reliable. When applications contain dynamic data, a decision is needed on how to handle it within the visual tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Mock your dynamic data
&lt;/h2&gt;

&lt;p&gt;One option for handling dynamic data in your visual tests is to mock the data so that it is consistent each time you run the test. For example, if you have a list of items that can change over time (and break you test), you could mock the list so it's the same each time you run the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Hide the dynamic data
&lt;/h2&gt;

&lt;p&gt;Another option for handling dynamic data in your visual tests is to hide the data so that it is not visible in the screenshot. This is the option I’m going to focus on in this post, as it can be a simple and effective way to handle dynamic data in your visual tests.&lt;/p&gt;

&lt;h1&gt;
  
  
  Back to my sample application
&lt;/h1&gt;

&lt;p&gt;In my last post I had a sample ToDo app with a single Playwright visual testing.  I've tweaked the application to include the current date and time in the footer.  So now when I run the test it fails, because it doesn't match my baseline image:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8wn1n8eoltjcgr5f8gt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8wn1n8eoltjcgr5f8gt.png" alt="side-by-side view" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see the Actual page has the current date and time in the footer, but the Baseline image does not.  You can also look at the Diff image to see the highlighted differences between the two:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ybpvxhz70req1pgd78t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ybpvxhz70req1pgd78t.png" alt="diff view" width="460" height="502"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Hiding dynamic data
&lt;/h1&gt;

&lt;p&gt;The &lt;code&gt;toHaveScreenshot()&lt;/code&gt; function in Playwright is what we use to do the screen shot comparison, and it accepts a &lt;code&gt;stylePath&lt;/code&gt; parameter that allows you to hide elements on the page before taking the screenshot.  This is a great way to handle dynamic data in your visual tests.  In my sample application I created &lt;code&gt;screenshot.css&lt;/code&gt; that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;  &lt;span class="nf"&gt;#datetime&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then pass this file to the &lt;code&gt;toHaveScreenshot()&lt;/code&gt; function like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;landing.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;stylePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshot.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when I run the test, the dynamic data is hidden, and the test passes.  The Baseline image does not need to change, as the dynamic data is not visible in the screenshot.&lt;/p&gt;

&lt;h1&gt;
  
  
  Masking dynamic data
&lt;/h1&gt;

&lt;p&gt;Another option for dealing with dynamic data is to use Playwright's &lt;code&gt;mask&lt;/code&gt; parameter, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;landing.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#datetime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you what to go this route, you would need to update the Baseline image to include the mask.  This is a little more work, but it's a good option if you want a placeholder for the dynamic data in the Baseline image:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstchttiwilw89brljyx7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstchttiwilw89brljyx7.png" alt="Image description" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  But what if hiding dynamic data is also hiding a bug?
&lt;/h2&gt;

&lt;p&gt;Hey, great question!  The good news is you still have the ability to do functional validation of that dynamic data, just like you always have.&lt;/p&gt;

&lt;p&gt;For example, if I want to verify datetime is displayed and it matches the current date, I could just do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;datetimeValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#datetime-value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify that the datetime value element is visible&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;datetimeValue&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Get the text content of the datetime value element&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;datetimeText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;datetimeValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if datetimeText is a valid date string&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;datetimeText&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;datetimeText&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;datetimeDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;datetimeText&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleDateString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleDateString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="c1"&gt;// Assert that the date part of the datetime value is equal to the current date&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;datetimeDate&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentDate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid datetime text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code verifies the datetime is displayed, and it's the current date.  I can then let my visual comparison do the rest of the heavy lifting.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrap-up
&lt;/h1&gt;

&lt;p&gt;So that's about it for this post.  You now know how to hide dynamic data in your visual tests, as you continue using functional validation to ensure the dynamic data is correct.&lt;/p&gt;

&lt;p&gt;So far these blog posts have been focused on how Playwright supports visual testing, without considering cloud-based solutions like Applitools or Percy.  In my next post I'll cover how to integrate Playwright with Applitools (or Percy) for visual testing.  Stay tuned!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>devops</category>
      <category>qa</category>
    </item>
    <item>
      <title>Playwright Visual Testing - Getting Started</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Fri, 25 Oct 2024 17:25:42 +0000</pubDate>
      <link>https://dev.to/leading-edje/visual-testing-with-playwright-3fhh</link>
      <guid>https://dev.to/leading-edje/visual-testing-with-playwright-3fhh</guid>
      <description>&lt;h1&gt;
  
  
  Intro
&lt;/h1&gt;

&lt;p&gt;So, what’s the big deal with visual testing, and how is it different from functional testing?&lt;/p&gt;

&lt;p&gt;Visual testing is all about comparing an &lt;em&gt;actual&lt;/em&gt; web page to an &lt;em&gt;expected&lt;/em&gt; (baseline) image. Unlike functional testing, which checks if your application &lt;em&gt;works&lt;/em&gt; as expected, visual testing focuses on how your web pages &lt;em&gt;look&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Many times, visual testing is done manually, but you can (and should) automate it where appropriate using tools like Playwright.&lt;/p&gt;

&lt;p&gt;While Playwright is known for functional testing, it also comes with a straightforward API that lets you take screenshots and compare them to baseline images automatically. This way, you can easily include visual testing in your automated suite to ensure that as your application evolves, it continues to look great.&lt;/p&gt;

&lt;p&gt;A number of 3rd party cloud solutions exist that support visual testing and can be integrated into your Playwright functional tests, but for the moment I am going to focus solely on using only the Playwright core standalone functionality.&lt;/p&gt;

&lt;h1&gt;
  
  
  MANUAL Visual Testing Challenges
&lt;/h1&gt;

&lt;p&gt;Manual visual testing can be super useful, but, like anything else, it has its challenges. Here are a couple that come to mind:&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge #1: Manual validation is time consuming
&lt;/h2&gt;

&lt;p&gt;One of the trickiest parts of visual testing is ensuring your application looks good across different browsers, operating systems, and screen resolutions. What looks perfect in Chrome on your MacBook might not look so great in Safari on an iPhone 12.&lt;/p&gt;

&lt;p&gt;If you want to test your application across 3 browsers and 3 viewports, that’s 9 different combinations you need to test. And if you want to test across multiple operating systems, that’s even more combinations to test.  So even if you're able to manually test all these combinations without pulling your hair out, it’s time-consuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge #2: Manual validation is error-prone
&lt;/h2&gt;

&lt;p&gt;Manual testing is done by humans, which means human errors come with it. It’s easy to miss a visual bug, especially if you’re testing on multiple browsers and devices.&lt;/p&gt;

&lt;p&gt;Luckily, Playwright has some great features to help with these challenges, so let’s dig into how it works.&lt;/p&gt;

&lt;h1&gt;
  
  
  A Basic Example
&lt;/h1&gt;

&lt;p&gt;Let’s start with a simple example to show how easy it is to set up visual testing with Playwright. If you want to see all the details, I’ve got a &lt;a href="https://github.com/dwwhalen/visual-testing-sandbox" rel="noopener noreferrer"&gt;sample repo&lt;/a&gt; with a basic task-tracking application and some Playwright tests to go with it. Here’s the first test I wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New Todo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@visual should allow me to add todo items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;landing.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test is pretty basic. It just navigates to the application’s home page and uses Playwright’s &lt;code&gt;toHaveScreenshot()&lt;/code&gt; function to grab a screenshot of the page and compare it to the baseline image. When I run this test in the Chromium browser, I get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Error: A snapshot doesn't exist at /Users/denniswhalen/visual-testing-sandbox/e2e-tests/blog.spec.ts-snapshots/landing-chromium-desktop-darwin.png, writing actual.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test failed because Playwright didn’t find a baseline image, which makes sense since I don’t have one yet! Playwright created a baseline for me based on what the page looked like during the test:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqi2mb1nkywb9ln4uraf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqi2mb1nkywb9ln4uraf.png" alt="Image description" width="776" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After manually checking the baseline image and confirming it looked good, I can run the test again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;      ✓  1 [chromium-desktop] › blog.spec.ts:10:9 › New Todo › @visual should allow me to add todo items (1.5s)

  1 passed (3.0s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, Playwright found the baseline screenshot, compared it to the actual screenshot in the test, and verified they match. All good!&lt;/p&gt;

&lt;h1&gt;
  
  
  Dealing with Challenge #1: Different Browsers and Viewports
&lt;/h1&gt;

&lt;p&gt;That test ran in the Chromium desktop browser, but you can also run it in other browsers like Firefox and WebKit, and even on mobile viewports. To do that, I updated my &lt;code&gt;playwright.config.ts&lt;/code&gt; file to include the browsers and viewports I wanted to test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chrome-desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desktop Chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firefox-desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desktop Firefox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webkit-desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desktop Safari&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chrome-mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pixel 5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;safari-mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iPhone 12&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these updates, I can run the tests again, and they’ll run across all the browsers and viewports I specified.&lt;/p&gt;

&lt;p&gt;When the test runs, Playwright will look for a baseline image that matches the name of the project for which the test is running. Since I don’t yet have baseline images for the new projects, Playwright will generate them for me. After reviewing the images and rerunning the test, I’m all set.&lt;/p&gt;

&lt;p&gt;With that done, I have one simple test that runs in five browser/viewport combinations, verifying the screens against the baseline files. The baseline filenames include the page name (&lt;code&gt;landing&lt;/code&gt;), browser name, and OS (&lt;code&gt;-darwin&lt;/code&gt;), so it’s easy to track which baseline images are being used.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffibrkunselocxbz9tsbk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffibrkunselocxbz9tsbk.png" alt="Image description" width="684" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Boom! My first visual test is working locally. Next, I could write more tests, but first, I want to push this to GitHub and run it in the CI workflow, just to be sure it works. But really, what could go wrong?&lt;/p&gt;

&lt;h1&gt;
  
  
  Dealing with Challenge #1: Different Operating Systems
&lt;/h1&gt;

&lt;p&gt;Hmmm, when I ran it in CI, I got this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Error: A snapshot doesn't exist at /workspace/e2e-tests/blog.spec.ts-snapshots/landing-chromium-desktop-linux.png, writing actual.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This happened because the baseline image Playwright is looking for in the CI environment doesn’t exist. The baseline images I created earlier had names ending in &lt;code&gt;-darwin.png&lt;/code&gt; (Mac), but the CI system is running Linux, so it’s expecting a &lt;code&gt;-linux.png&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Playwright uses the operating system as part of the baseline filename, which is helpful, as mentioned in Challenge #1.&lt;/p&gt;

&lt;p&gt;So, now I need to create a Linux baseline image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker to the Rescue!
&lt;/h2&gt;

&lt;p&gt;To solve this, I used Docker on my MacBook to generate the Linux baseline images. Here’s the command I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -it --rm \
  --ipc=host \
  -v $(pwd):/workspace \
  -w /workspace \
  -e HOME=/tmp \
  mcr.microsoft.com/playwright:latest \
  /bin/bash -c "npx playwright install --with-deps &amp;amp;&amp;amp; npm run e2e:visual"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command runs the visual tests inside a Docker container and generates the baseline images for Linux, so now I have all the baseline images I need:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1h34qf9lxwjddt10snc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1h34qf9lxwjddt10snc.png" alt="Docker command" width="650" height="538"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;I also tweaked my GitHub workflow to run the visual tests with the same Docker image I used locally, so if the tests pass in Docker locally, they should also pass in the CI environment.&lt;/p&gt;

&lt;p&gt;After committing the changes to the repo, the tests now pass both locally on my Mac and in the CI workflow. Pretty slick, right?&lt;/p&gt;

&lt;h1&gt;
  
  
  Dealing with a Failed Test
&lt;/h1&gt;

&lt;p&gt;Let’s see how Playwright helps you when a test fails. I’m going to change the application so the ToDo textbox doesn’t have placeholder text. When I run the test, I get this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Error: Screenshot comparison failed:
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see the specific issue, I can check the HTML report that Playwright generates:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe35465y45yftvp1mw4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe35465y45yftvp1mw4k.png" alt="HTML report with side-by-side view" width="800" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I selected the side-by-side view, which shows the actual image on the left and the expected image on the right. You can easily spot differences and decide if they’re acceptable. If they are expected, you can update the baseline image. If not, you can update the page to fix the issue.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrap-up
&lt;/h1&gt;

&lt;p&gt;So that’s just a little taste of how you can use Playwright for visual testing. Hopefully, you can see the value of visual testing and how it helps catch visual bugs before your users do!&lt;/p&gt;

&lt;p&gt;One benefit that might not be as obvious is how visual testing can simplify your functional testing. In future posts, I’ll talk about that and cover how to handle dynamic data in automated visual testing. &lt;/p&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>automation</category>
      <category>playwright</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why I Blog</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Thu, 13 Jul 2023 20:27:54 +0000</pubDate>
      <link>https://dev.to/leading-edje/why-i-blog-47bm</link>
      <guid>https://dev.to/leading-edje/why-i-blog-47bm</guid>
      <description>&lt;p&gt;I've been blogging off-and-on for over 4 years.  It's really helped me grow, and I usually recommend it to others as great way to keep growing skills.&lt;/p&gt;

&lt;p&gt;Recently I was giving peer review feedback to a fellow employee.  As I was describing all of the reasons THEY might want to consider blogging, I realized I should create my own blog post that talks about the value of blogging.&lt;/p&gt;

&lt;h1&gt;
  
  
  Focused learning, for me!
&lt;/h1&gt;

&lt;p&gt;It probably sounds weird, but a lot of the things I blog about are things I don't know much about when I start the blog.  &lt;/p&gt;

&lt;p&gt;When I identify a skill or technology that's new to me and I want to learn more, many times I will write a blog as I work through my learning. &lt;/p&gt;

&lt;p&gt;For example, let's say I want to get some expertise on using Postman for API testing, and I also want to demonstrate how to include those Postman tests in a CI pipeline.  &lt;/p&gt;

&lt;p&gt;For something like that, I will want to build a working prototype that demonstrates the key pieces of the tech I'm learning.  As I build that prototype, I will also write a blog post that describes what I'm doing and why.  &lt;/p&gt;

&lt;p&gt;Writing the blog as I work through the prototype reinforces my learning, and it forces me to feel sure I understand how things work.  I don't want to share info with others that is incomplete or wrong.  &lt;/p&gt;

&lt;p&gt;I'll also have screenshots and a repo I can share in the blog.&lt;/p&gt;

&lt;p&gt;Feel free to look at the &lt;a href="https://dev.to/dwwhalen/series/17772"&gt;blog&lt;/a&gt; I wrote for Postman.  The blog actually started as a single post, but turned into a 2-part series.  Once I got into it, I realized it was probably too big for one post, so I split it up.&lt;/p&gt;

&lt;h1&gt;
  
  
  Introducing myself to a future client
&lt;/h1&gt;

&lt;p&gt;I'm an IT consultant.  That means I may be interviewing with new clients relatively frequently.  I've found that making my blog posts available to potential clients is an &lt;strong&gt;AWESOME&lt;/strong&gt; marketing tool!  &lt;/p&gt;

&lt;p&gt;If you have good content, and the person interviewing you has looked at it, that will get you a long way towards securing that next project or client or job.&lt;/p&gt;

&lt;h1&gt;
  
  
  For future reference
&lt;/h1&gt;

&lt;p&gt;Like many of us, I work with a lot of different technologies over time, and I need to be constantly growing my skills.&lt;/p&gt;

&lt;p&gt;Along with all this learning comes a LOT of forgetting.  The stuff that I was working with last year?  That stuff that I knew like the back of my hand??  Well, it might be a distant memory to me now.&lt;/p&gt;

&lt;p&gt;If I create a clear and concise blog about a topic as I work with it, I will have a future reference point that I can go back to as needed.  Even if no one else reads my blog post, &lt;strong&gt;I&lt;/strong&gt; can reference it.  &lt;/p&gt;

&lt;p&gt;Since it's written in my voice, about a topic that I've struggled through, my memory will get refreshed a lot quicker than googling "help me with postman".&lt;/p&gt;

&lt;h1&gt;
  
  
  Helping others
&lt;/h1&gt;

&lt;p&gt;I guess this is an obvious benefit to blogging, and a bit less selfish than the previous ones I mentioned.  &lt;/p&gt;

&lt;p&gt;We've all been helped by so many nameless and faceless folks on our journey to improve our skills and marketability.  Paying that back is a good thing and will make you feel good.&lt;/p&gt;

&lt;h1&gt;
  
  
  Some suggestions
&lt;/h1&gt;

&lt;p&gt;These are just some random blogging suggestions that I thought of as I composed this.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Don't compose in the blog provider's editor
&lt;/h2&gt;

&lt;p&gt;Avoid composing your blog directly in the blog platform's editor.  Instead, consider composing your blog locally and pushing it to a repo.  If you have a POC to support the blog, keep the blog with that code.  From there you can just copy/paste it to the blog platform editor.&lt;/p&gt;

&lt;p&gt;In the past I have run into issues where I've lost work with the blog platform editor, so I do everything locally.  I just use VS Code for authoring markdown files and store them in GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't get too hung up on visit and like counts
&lt;/h2&gt;

&lt;p&gt;I have yet to break the internet with any of my posts.  I've had 1 or 2 that have done ok, but most have 10 likes or less.  That's ok.  Remember, blogging is for the benefit of the author also!&lt;/p&gt;

&lt;p&gt;I just remember that I'm good enough, I'm smart enough, and doggone it, people like me!&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3diasw29uylm0wei9f10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3diasw29uylm0wei9f10.png" alt="Image description" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep your post to a 5-minute read, or less
&lt;/h2&gt;

&lt;p&gt;Like everything in this post, this is just my opinion.  I prefer shorter blog posts when reading them, so I try to do the same when writing them.  If I find I have too much content, i can just split it into multiple posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference your repo and use screen shots
&lt;/h2&gt;

&lt;p&gt;This is pretty self-explanatory.  If you are building something as you write you blog, include a link to your repo and don't forget to add some screen shots in the blog.  Pictures are good!&lt;/p&gt;

&lt;h2&gt;
  
  
  Hmm, that Visual Basic 6 post may be too old...
&lt;/h2&gt;

&lt;p&gt;Blogs get stale, and they need to be updated or pruned.  I have not done a good job with that, and I need to do better.  &lt;/p&gt;

&lt;p&gt;(No, I don't really have a VB 6 post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Blogging will give you more ideas
&lt;/h2&gt;

&lt;p&gt;Blogging will give you new ideas for more blogs, write that sh*t down so you don't forget!&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrap-up
&lt;/h1&gt;

&lt;p&gt;So, there you go.  That's a decent overview of why I blog.  What do you think?  Does this give you any ideas or motivation for blogging?  &lt;/p&gt;

&lt;p&gt;What are some other good reasons to blog?&lt;/p&gt;

&lt;p&gt;Feel free to share your thoughts in the comments!  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blog</category>
      <category>beginners</category>
      <category>writing</category>
      <category>learning</category>
    </item>
    <item>
      <title>Gherkin and Robot Framework</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Sun, 12 Feb 2023 17:21:13 +0000</pubDate>
      <link>https://dev.to/leading-edje/gherkin-and-robot-framework-5oe</link>
      <guid>https://dev.to/leading-edje/gherkin-and-robot-framework-5oe</guid>
      <description>&lt;p&gt;Greetings!  They say all good things must come to an end, and with this post, so it is with my series of posts covering &lt;a href="http://robotframework.org/" rel="noopener noreferrer"&gt;Robot Framework&lt;/a&gt;.  &lt;/p&gt;

&lt;p&gt;This post builds on what was covered in previous posts.  If you haven't checked out the &lt;a href="https://dev.to/dwwhalen/series/21110"&gt;other posts in the series&lt;/a&gt;, please do.  &lt;/p&gt;

&lt;h1&gt;
  
  
  Robot Framework and Gherkin
&lt;/h1&gt;

&lt;p&gt;In the last post we built a Robot test to validate some functionality of the ToDo app.  The test accessed the ToDo page, added a ToDo, and verified it was successfully added.  (The repo for the app and the tests can be found &lt;a href="https://github.com/dwwhalen/cypress-robot-todomvc" rel="noopener noreferrer"&gt;here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This is what our current Robot test looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;***&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt; &lt;span class="n"&gt;Case&lt;/span&gt;&lt;span class="o"&gt;***&lt;/span&gt;
&lt;span class="n"&gt;Add&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ToDo&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
  &lt;span class="n"&gt;Open&lt;/span&gt; &lt;span class="n"&gt;Browser&lt;/span&gt;    &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BROWSER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;Page&lt;/span&gt; &lt;span class="n"&gt;Should&lt;/span&gt; &lt;span class="n"&gt;Not&lt;/span&gt; &lt;span class="n"&gt;Contain&lt;/span&gt; &lt;span class="n"&gt;Element&lt;/span&gt;  &lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nd"&gt;@class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;Input&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;  &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;  &lt;span class="n"&gt;Finish&lt;/span&gt; &lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;Blog&lt;/span&gt; &lt;span class="n"&gt;Post&lt;/span&gt;
  &lt;span class="n"&gt;Press&lt;/span&gt; &lt;span class="n"&gt;Keys&lt;/span&gt;  &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;  &lt;span class="n"&gt;RETURN&lt;/span&gt;
  &lt;span class="n"&gt;Page&lt;/span&gt; &lt;span class="n"&gt;Should&lt;/span&gt; &lt;span class="n"&gt;Contain&lt;/span&gt; &lt;span class="n"&gt;Element&lt;/span&gt;  &lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nd"&gt;@class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actual_count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="n"&gt;SeleniumLibrary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt; &lt;span class="n"&gt;Element&lt;/span&gt; &lt;span class="n"&gt;Count&lt;/span&gt;  &lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nd"&gt;@class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;todo-list&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;
  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected_count_number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="n"&gt;Convert&lt;/span&gt; &lt;span class="n"&gt;To&lt;/span&gt; &lt;span class="n"&gt;Number&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="n"&gt;Should&lt;/span&gt; &lt;span class="n"&gt;Be&lt;/span&gt; &lt;span class="n"&gt;Equal&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected_count_number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actual_count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are at least a couple issues with this test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Page locators are not reusable.  Following this pattern, if I wanted to create another test (or 100 tests) that needs the count of ToDo items, I would probably just copy/paste that locator, &lt;code&gt;//ul[@class='todo-list']/li&lt;/code&gt; every time I needed it.  A better strategy would be to define the locator in a single place.  If the locator ever needs to change, I have one place to go to make my update.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To my eyes, the test is hard to read.  This is a pretty basic test, but It's not super clear what's going on.  Also, this pattern requires knowledge of the Robot syntax.  Product owners and BAs are not going to be interested in reading this, and probably no one else will be either.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's address these 2 issues.&lt;/p&gt;

&lt;p&gt;As a reminder, this is what that test is actually doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- open the ToDo page
- verify there are no ToDos
- add a ToDo
- verify the ToDo text matches what was added
- verify there is 1 ToDo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want my test to look a lot like this, without all the implementation details in my way. &lt;/p&gt;

&lt;h2&gt;
  
  
  Gherkin and Robot Framework
&lt;/h2&gt;

&lt;p&gt;In the real world, the above is a good example of an acceptance test.  It defines some basic functionality and describes how the application should react.  Automating the testing of the requirements is useful as a component of &lt;a href="https://en.wikipedia.org/wiki/Acceptance_test-driven_development" rel="noopener noreferrer"&gt;Acceptance Test Driven Development (ATDD)&lt;/a&gt;.  I usually see this go hand-in-hand with &lt;a href="https://cucumber.io/docs/bdd/" rel="noopener noreferrer"&gt;BDD&lt;/a&gt; and the gherkin syntax. In that example our gherkin-syntax test could look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="nf"&gt;When &lt;/span&gt;the User accesses the Home page
&lt;span class="nf"&gt;Then &lt;/span&gt;the ToDo count is  0
&lt;span class="nf"&gt;When &lt;/span&gt;the user enters new ToDo  learn Robot
&lt;span class="nf"&gt;Then &lt;/span&gt;the ToDo item is added to the to the list  learn Robot  1
&lt;span class="nf"&gt;And &lt;/span&gt;the ToDo count is  1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So how does Robot Framework facilitate our ability to write tests like this?&lt;/p&gt;

&lt;p&gt;First of all, remember Robot is a keyword-driven framework.  The first line of our test &lt;code&gt;When the User accesses the Home page&lt;/code&gt; is just a keyword to Robot.  As discussed in &lt;a href="https://dev.to/dwwhalen/series/21110"&gt;previous posts&lt;/a&gt;, we can easily hide the implementation details of this keyword in a Robot resource file.  Defining this keyword in a resource file could be as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;***&lt;/span&gt; &lt;span class="n"&gt;Keywords&lt;/span&gt; &lt;span class="o"&gt;***&lt;/span&gt;
&lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;accesses&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;Home&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;
    &lt;span class="n"&gt;Open&lt;/span&gt; &lt;span class="n"&gt;Browser&lt;/span&gt;  &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8888&lt;/span&gt;  &lt;span class="n"&gt;chrome&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here I have created a custom keyword &lt;code&gt;the User accesses the Home page&lt;/code&gt;, which uses the built-in keyword &lt;code&gt;Open Browser&lt;/code&gt;.  Note that my custom keyword does NOT begin with the word &lt;code&gt;When&lt;/code&gt;.  That's because if no match is found for the full keyword, Robot will ignore the prefixes "Given", "When", "Then", "And", "But" .  This allows Robot to easily facilitate our ability to build tests using the Gherkin syntax.&lt;/p&gt;

&lt;p&gt;Also, since we can pass parameters with Robot keywords, we're doing that with the step &lt;code&gt;When the user enters new ToDo  learn Robot&lt;/code&gt;.  We are passing the text of the ToDo, "learn robot". The implementation of that step in the resource file can look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;***&lt;/span&gt; &lt;span class="n"&gt;Keywords&lt;/span&gt; &lt;span class="o"&gt;***&lt;/span&gt;
&lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;enters&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ToDo&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Arguments&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;todo_to_enter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;Input&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;  &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;todo_to_enter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;Press&lt;/span&gt; &lt;span class="n"&gt;Keys&lt;/span&gt;  &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;  &lt;span class="n"&gt;RETURN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full resource file, including other examples, can be found &lt;a href="https://github.com/dwwhalen/cypress-robot-todomvc/blob/master/robot-framework/e2e/resources/ToDos_keywords.resource" rel="noopener noreferrer"&gt;here&lt;/a&gt;, with the gherkin test &lt;a href="https://github.com/dwwhalen/cypress-robot-todomvc/blob/master/robot-framework/e2e/suites/ToDos_basic_gherkin.robot" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To run these tests, just be sure to first start the app locally (&lt;code&gt;npm start&lt;/code&gt;).  If you can't open the app manually, the Robot test won't be able to either.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrap-up
&lt;/h1&gt;

&lt;p&gt;So that's it.  We had 2 goals for cleaning up that test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;facilitate reuse&lt;/li&gt;
&lt;li&gt;make it readable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With reusable and parameterized gherkin steps, I feel we've accomplished our goal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="nf"&gt;When &lt;/span&gt;the User accesses the Home page
&lt;span class="nf"&gt;Then &lt;/span&gt;the ToDo count is  0
&lt;span class="nf"&gt;When &lt;/span&gt;the user enters new ToDo  learn Robot
&lt;span class="nf"&gt;Then &lt;/span&gt;the ToDo item is added to the to the list  learn Robot  1
&lt;span class="nf"&gt;And &lt;/span&gt;the ToDo count is  1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope this series of posts has helped someone learn more about Robot Framework.  Of course I have barely scratched the surface, and the links below should help you continue your Robot journey!&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/robotframework/QuickStartGuide/blob/master/QuickStart.rst" rel="noopener noreferrer"&gt;https://github.com/robotframework/QuickStartGuide/blob/master/QuickStart.rst&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/robotframework/RobotDemo" rel="noopener noreferrer"&gt;https://github.com/robotframework/RobotDemo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html" rel="noopener noreferrer"&gt;https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html&lt;/a&gt;&lt;br&gt;
&lt;a href="https://robotframework.org/robotframework/latest/libraries/BuiltIn.html" rel="noopener noreferrer"&gt;https://robotframework.org/robotframework/latest/libraries/BuiltIn.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/leading-edje"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F5uo60qforg9yqdpgzncq.png" alt="Smart EDJE Image" width="800" height="280"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>gherkin</category>
      <category>robotframework</category>
      <category>python</category>
    </item>
    <item>
      <title>Web Testing With Robot Framework</title>
      <dc:creator>Dennis Whalen</dc:creator>
      <pubDate>Sun, 29 Jan 2023 21:32:44 +0000</pubDate>
      <link>https://dev.to/leading-edje/web-testing-with-robot-framework-3ngi</link>
      <guid>https://dev.to/leading-edje/web-testing-with-robot-framework-3ngi</guid>
      <description>&lt;p&gt;Liquid syntax error: 'raw' tag was never closed&lt;/p&gt;
</description>
      <category>workplace</category>
      <category>discuss</category>
      <category>career</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
