<?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: Serhat Ozdursun</title>
    <description>The latest articles on DEV Community by Serhat Ozdursun (@serhat_ozdursun_03644ef56).</description>
    <link>https://dev.to/serhat_ozdursun_03644ef56</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%2F2914056%2Fac8f5a0b-5810-4be3-92a8-2fc66227dc99.jpeg</url>
      <title>DEV Community: Serhat Ozdursun</title>
      <link>https://dev.to/serhat_ozdursun_03644ef56</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/serhat_ozdursun_03644ef56"/>
    <language>en</language>
    <item>
      <title>Shipping AI into Your CI: Coverage-Aware Reviews with Danger + OpenAI (and SonarCloud)</title>
      <dc:creator>Serhat Ozdursun</dc:creator>
      <pubDate>Wed, 03 Sep 2025 09:40:13 +0000</pubDate>
      <link>https://dev.to/serhat_ozdursun_03644ef56/shipping-ai-into-your-ci-coverage-aware-reviews-with-danger-openai-and-sonarcloud-noh</link>
      <guid>https://dev.to/serhat_ozdursun_03644ef56/shipping-ai-into-your-ci-coverage-aware-reviews-with-danger-openai-and-sonarcloud-noh</guid>
      <description>&lt;p&gt;I believe the best way to learn is by doing. But in a busy job, you don’t always get to try new tech on company time. That can’t be an excuse—our field moves fast, and AI is one of the biggest shifts right now.&lt;/p&gt;

&lt;p&gt;My day-to-day didn’t include AI work, so I turned to my personal website repo as a safe sandbox: &lt;a href="https://github.com/serhatozdursun/resume" rel="noopener noreferrer"&gt;serhatozdursun/resume&lt;/a&gt;. It’s the perfect “lab mouse.” I wanted to explore AI-assisted quality gates: the things we already rely on in QA—static analysis, unit tests, code reviews—augmented with an assistant that understands the diff and helps us ship better.&lt;/p&gt;

&lt;p&gt;This post walks through the pipeline I built: SonarCloud + Jest coverage + Danger + OpenAI, wired together with Secrets so I can tune behavior without touching YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;✅ SonarCloud for static analysis + PR decoration + coverage dashboards.&lt;br&gt;
✅ A coverage gate on changed code (e.g., 80% on lines you touched).&lt;br&gt;
✅ AI unit test suggestions generated directly from the diff (copy-paste-ready Jest/TS).&lt;br&gt;
✅ AI code review that posts a concise diff snippet (old → new) with an explanation.&lt;br&gt;
(No fragile line numbers; we still add inline notes when mapping is reliable.)&lt;br&gt;
✅ Secret-driven knobs so you can change thresholds/toggles from CI Secrets—no PRs to change policy.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture (bird’s eye)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Tests &amp;amp; coverage: Jest runs and writes coverage/lcov.info.&lt;/li&gt;
&lt;li&gt;SonarCloud: runs as usual for code smells and coverage on new code.&lt;/li&gt;
&lt;li&gt;Danger + OpenAI (same job):&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Reads the PR diff.&lt;/li&gt;
&lt;li&gt;Computes new-lines coverage per changed file from LCOV.&lt;/li&gt;
&lt;li&gt;If below MIN_FILE_COVERAGE, fails the PR and posts AI test ideas.&lt;/li&gt;
&lt;li&gt;Regardless of coverage, asks OpenAI to:

&lt;ul&gt;
&lt;li&gt;propose copy-pasteable Jest tests, and&lt;/li&gt;
&lt;li&gt;do a short code review with actionable notes and a tiny diff snippet.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;All behavior is toggled by environment variables you define as secrets.
&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%2Fs4wew8t18c7ordav7eow.png" alt="workflow" width="800" height="379"&gt;
## One workflow for both PRs and main (AI runs only for PRs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep one file so everything lives in one place. The workflow triggers on PRs and on pushes to main, but the AI step is gated so it only runs on PRs.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Code Quality (Tests + SonarCloud + AI Review)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ready_for_review&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;statuses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="c1"&gt;# Optional hardening: avoid duplicate runs while a PR is updated&lt;/span&gt;
&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qg-${{ github.workflow }}-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test-and-analyze&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&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="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0&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="s"&gt;Setup Node&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;22'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;yarn'&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="s"&gt;Install deps&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --frozen-lockfile&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="s"&gt;Run unit tests (with coverage)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --coverage&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="s"&gt;SonarCloud Scan&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SonarSource/sonarcloud-github-action@v5&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;SONAR_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SONAR_TOKEN }}&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="s"&gt;Upload coverage report (PRs only)&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event_name == 'pull_request' }}&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&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="s"&gt;coverage-html&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;coverage/lcov-report/&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="s"&gt;Install Danger deps&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event_name == 'pull_request' }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn add -D danger lcov-parse openai&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="s"&gt;Run AI coverage-aware review&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event_name == 'pull_request' }}&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Required&lt;/span&gt;
          &lt;span class="na"&gt;DANGER_GITHUB_API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;

          &lt;span class="c1"&gt;# Optional knobs (set via Secrets to avoid editing YAML)&lt;/span&gt;
          &lt;span class="c1"&gt;# MIN_FILE_COVERAGE:            ${{ secrets.MIN_FILE_COVERAGE }}   # default 80&lt;/span&gt;
          &lt;span class="c1"&gt;# MAX_FILES_TO_ANALYZE:         ${{ secrets.MAX_FILES_TO_ANALYZE }}# default 3&lt;/span&gt;
          &lt;span class="c1"&gt;# OPENAI_MODEL:                 ${{ secrets.OPENAI_MODEL }}        # default gpt-4o-mini&lt;/span&gt;
          &lt;span class="c1"&gt;# DANGER_ALWAYS_NEW_COMMENT:    ${{ secrets.DANGER_ALWAYS_NEW_COMMENT }} # default off&lt;/span&gt;
          &lt;span class="c1"&gt;# AI_REVIEW_ENABLED:            ${{ secrets.AI_REVIEW_ENABLED }}    # default 1&lt;/span&gt;
          &lt;span class="c1"&gt;# AI_REVIEW_INLINE:             ${{ secrets.AI_REVIEW_INLINE }}     # default 1&lt;/span&gt;
          &lt;span class="c1"&gt;# AI_REVIEW_BLOCK_ON_FINDINGS:  ${{ secrets.AI_REVIEW_BLOCK_ON_FINDINGS }} # default off&lt;/span&gt;
          &lt;span class="c1"&gt;# AI_REVIEW_MAX_CHARS:          ${{ secrets.AI_REVIEW_MAX_CHARS }}  # default 100000&lt;/span&gt;
          &lt;span class="c1"&gt;# AI_REVIEW_MAX_FINDINGS:       ${{ secrets.AI_REVIEW_MAX_FINDINGS }} # default 8&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx danger ci --dangerfile dangerfile.ts&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this pattern?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Single file keeps maintenance low.&lt;/li&gt;
&lt;li&gt;AI job runs only on PRs (if:), so merging to main doesn’t re-comment PRs.&lt;/li&gt;
&lt;li&gt;You still get tests + Sonar on pushes to main.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;dangerfile.ts&lt;/code&gt; — what it actually does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Coverage gate on changed code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;We parse coverage/lcov.info using lcov-parse.&lt;/li&gt;
&lt;li&gt;We fetch the PR diff and compute coverage on new/modified instrumented lines.&lt;/li&gt;
&lt;li&gt;If that effective coverage is below MIN_FILE_COVERAGE (default 80), we:

&lt;ul&gt;
&lt;li&gt;fail the PR,&lt;/li&gt;
&lt;li&gt;post a table showing per-file new-lines coverage and whole-file coverage, and&lt;/li&gt;
&lt;li&gt;ask the AI for test ideas (see next).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&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%2Fv4q49ch5jm4t8y0sdayc.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%2Fv4q49ch5jm4t8y0sdayc.png" alt="AI Coverage Message" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2) AI unit test suggestions (copy-paste ready)
&lt;/h3&gt;

&lt;p&gt;Prompt instructs the model: “You’re a senior TS/React dev. Output one Markdown code block: a runnable Jest TS test file. Keep it minimal but cover branches and edge cases implied by the diff. Use a file name like src/tests/.auto.test.ts.”&lt;/p&gt;

&lt;p&gt;You can always generate suggestions, or only when coverage dips below the threshold—toggle with Secrets.&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%2Fcdvhqao9hxr0w4jwsyt9.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%2Fcdvhqao9hxr0w4jwsyt9.png" alt="AI review comment" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3) AI code review (snippet-first + optional inline)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;We enumerate added lines per file and ask the AI to return JSON findings:
&lt;/li&gt;
&lt;/ul&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;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/pages/index.tsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;ul&gt;
&lt;li&gt;For each finding, we create a concise diff snippet centered around that added line and post it as a file-scoped comment, with explanation and suggested fix.&lt;/li&gt;
&lt;li&gt;(This avoids wrong line numbers; the snippet shows the exact old → new change.)&lt;/li&gt;
&lt;li&gt;If mapping is available, we also add an inline note (secondary signal).&lt;/li&gt;
&lt;li&gt;If you set AI_REVIEW_BLOCK_ON_FINDINGS="1", any fail severity will fail the PR.&lt;/li&gt;
&lt;/ul&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%2Ff440xhny9zwh94hf4i7x.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%2Ff440xhny9zwh94hf4i7x.png" alt="AI review" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  “Knobs” behind Secrets (and not just on GitHub)
&lt;/h2&gt;

&lt;p&gt;The behavior is entirely env-driven, so you can change policy without opening PRs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;DANGER_GITHUB_API_TOKEN (or GITHUB_TOKEN) — lets Danger comment &amp;amp; set statuses.&lt;/li&gt;
&lt;li&gt;OPENAI_API_KEY — used for test ideas + AI code review.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Optional (defaults already in dangerfile.ts)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;MIN_FILE_COVERAGE → default 80&lt;/li&gt;
&lt;li&gt;MAX_FILES_TO_ANALYZE → 3&lt;/li&gt;
&lt;li&gt;OPENAI_MODEL → gpt-4o-mini&lt;/li&gt;
&lt;li&gt;DANGER_ALWAYS_NEW_COMMENT → off (set "1" to force fresh comment per push)&lt;/li&gt;
&lt;li&gt;AI_REVIEW_ENABLED → "1"&lt;/li&gt;
&lt;li&gt;AI_REVIEW_INLINE → "1"&lt;/li&gt;
&lt;li&gt;AI_REVIEW_BLOCK_ON_FINDINGS → off (set "1" to enforce blocking)&lt;/li&gt;
&lt;li&gt;AI_REVIEW_MAX_CHARS → 100000&lt;/li&gt;
&lt;li&gt;AI_REVIEW_MAX_FINDINGS → 8&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Not using GitHub Actions? The exact same env names work in other CI/CDs—store them in that platform’s secret manager:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS CodeBuild/CodePipeline → AWS Secrets Manager / SSM Parameter Store&lt;/li&gt;
&lt;li&gt;Bitbucket Pipelines → Repository Variables (secured)&lt;/li&gt;
&lt;li&gt;Azure DevOps → Library Variable Groups and/or Azure Key Vault&lt;/li&gt;
&lt;li&gt;GitLab CI → CI/CD Variables (masked/protected)&lt;/li&gt;
&lt;li&gt;CircleCI → Project Environment Variables or Contexts&lt;/li&gt;
&lt;li&gt;The “secret knobs” pattern is portable; only the secret store changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Optional hardening
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Concurrency (already in the YAML): prevents duplicate runs while developers push more commits.&lt;/li&gt;
&lt;li&gt;Fail early if LCOV missing (add right before Danger step if you prefer a clear error):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&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="s"&gt;Assert LCOV present&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event_name == 'pull_request' }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;test -f coverage/lcov.info || (echo "coverage/lcov.info not found" &amp;amp;&amp;amp; exit 1)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Pin tool versions (optional):
&lt;code&gt;npx -y danger@&amp;lt;pinned&amp;gt; ci --dangerfile dangerfile.ts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rollout plan
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start with suggestions only (no blocking).&lt;/li&gt;
&lt;li&gt;Set MIN_FILE_COVERAGE="80"; raise to 85–90 if the team is comfortable.&lt;/li&gt;
&lt;li&gt;Keep snippet-first review; enable inline (AI_REVIEW_INLINE="1") for extra hints.&lt;/li&gt;
&lt;li&gt;Flip AI_REVIEW_BLOCK_ON_FINDINGS="1" when you’re ready for strict enforcement.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This setup lets you practice AI-augmented code quality in a real repo without disrupting anything. SonarCloud continues to do the heavy lifting for static analysis and coverage dashboards; &lt;strong&gt;Danger + OpenAI&lt;/strong&gt; add &lt;strong&gt;targeted, actionable&lt;/strong&gt; feedback on the code you changed today—and can enforce gates when you’re ready.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add the workflow.&lt;/li&gt;
&lt;li&gt;Drop in the dangerfile.ts.&lt;/li&gt;
&lt;li&gt;Set your Secrets.&lt;/li&gt;
&lt;li&gt;Open a PR and watch the reviewer go to work.&lt;/li&gt;
&lt;/ul&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%2Fhi12n3fc0ep4myili98t.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%2Fhi12n3fc0ep4myili98t.png" alt="SonarQube Quality Gate" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>qa</category>
      <category>ai</category>
      <category>unittest</category>
      <category>devops</category>
    </item>
    <item>
      <title>🧪 GitHub Actions Matrix Strategy - Part 2: Email Notifications with Artifact Links via EmailJS</title>
      <dc:creator>Serhat Ozdursun</dc:creator>
      <pubDate>Mon, 14 Apr 2025 13:50:51 +0000</pubDate>
      <link>https://dev.to/serhat_ozdursun_03644ef56/github-actions-matrix-strategy-part-2-email-notifications-with-artifact-links-via-emailjs-544h</link>
      <guid>https://dev.to/serhat_ozdursun_03644ef56/github-actions-matrix-strategy-part-2-email-notifications-with-artifact-links-via-emailjs-544h</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/serhat_ozdursun_03644ef56/what-is-github-actions-matrix-strategy-5gma"&gt;Part 1 of this series&lt;/a&gt;, we explored how to use &lt;strong&gt;GitHub Actions matrix strategy&lt;/strong&gt; to run UI automation tests across multiple browsers like Chrome and Firefox in parallel. It was fast, flexible, and scalable.&lt;/p&gt;

&lt;p&gt;But once your matrix jobs run… how do you &lt;strong&gt;get notified&lt;/strong&gt; when something breaks — and more importantly, &lt;strong&gt;which browser failed&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;In this article, we’ll take it one step further by sending &lt;strong&gt;smart email notifications per browser&lt;/strong&gt;, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📬 Pass/fail status
&lt;/li&gt;
&lt;li&gt;🧪 Commit SHA
&lt;/li&gt;
&lt;li&gt;📎 Direct link to the HTML test report (via GitHub Artifacts)
&lt;/li&gt;
&lt;li&gt;✅ Fully integrated with &lt;strong&gt;EmailJS dynamic templates&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s dive in!&lt;/p&gt;




&lt;h2&gt;
  
  
  📬 Why Use EmailJS?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  💡 What is EmailJS?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.emailjs.com/" rel="noopener noreferrer"&gt;EmailJS&lt;/a&gt; allows developers to &lt;strong&gt;send emails directly from frontend apps or serverless environments&lt;/strong&gt; — without needing to set up their own email server or SMTP credentials.&lt;/p&gt;

&lt;p&gt;You define templates with variables (like &lt;code&gt;{{result}}&lt;/code&gt;, &lt;code&gt;{{browser}}&lt;/code&gt;), and send data via a simple REST API. It’s perfect for tools like GitHub Actions or CI/CD pipelines.&lt;/p&gt;




&lt;h3&gt;
  
  
  🛠 Other Tools You Could Use
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Highlights&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SendGrid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full-featured transactional email API&lt;/td&gt;
&lt;td&gt;Setup is more involved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mailgun&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developer-focused with detailed tracking&lt;/td&gt;
&lt;td&gt;Ideal for backend use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SMTP2GO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Traditional SMTP service&lt;/td&gt;
&lt;td&gt;Works well with SMTP clients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Postmark&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reliable transactional delivery and analytics&lt;/td&gt;
&lt;td&gt;Paid plan, high deliverability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EmailJS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No server needed, simple setup, free tier&lt;/td&gt;
&lt;td&gt;Best for client-side &amp;amp; CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  ✅ Why I Chose EmailJS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No backend needed&lt;/strong&gt; – works directly from GitHub Actions
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy template setup&lt;/strong&gt; – fully customizable UI
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free plan&lt;/strong&gt; – generous for small projects or teams
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick integration&lt;/strong&gt; – REST API is simple and secure
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works well with matrix workflows&lt;/strong&gt; – send dynamic info per job
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📄 Dynamic Email Templates in EmailJS
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding email content in your GitHub workflow, you can define a template in EmailJS 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%2Fpld2ew3u44e245h2qovi.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%2Fpld2ew3u44e245h2qovi.png" alt="Tamplate" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we just pass values to these placeholders from GitHub Actions using template_params.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ Updated GitHub Actions Workflow (with EmailJS Integration)
&lt;/h2&gt;

&lt;p&gt;Here's the key part of your workflow that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sends an email for each matrix browser job&lt;/li&gt;
&lt;li&gt;Uses dynamic data&lt;/li&gt;
&lt;li&gt;Links directly to the uploaded HTML test report&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Success Email Step
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&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="s"&gt;Send Success Email for ${{ matrix.browser }}&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success()&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;EMAILJS_SERVICE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EMAILJS_SERVICE_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;EMAILJS_TEMPLATE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EMAILJS_TEMPLATE_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;EMAILJS_USER_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EMAILJS_USER_ID }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;ARTIFACTS_LINK="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"&lt;/span&gt;

    &lt;span class="s"&gt;cat &amp;lt;&amp;lt;EOF &amp;gt; email-payload-success.json&lt;/span&gt;
    &lt;span class="s"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"service_id": "${EMAILJS_SERVICE_ID}",&lt;/span&gt;
      &lt;span class="s"&gt;"template_id": "${EMAILJS_TEMPLATE_ID}",&lt;/span&gt;
      &lt;span class="s"&gt;"user_id": "${EMAILJS_USER_ID}",&lt;/span&gt;
      &lt;span class="s"&gt;"template_params": {&lt;/span&gt;
        &lt;span class="s"&gt;"title": "✅ UI Test Passed",&lt;/span&gt;
        &lt;span class="s"&gt;"result": "passed successfully",&lt;/span&gt;
        &lt;span class="s"&gt;"browser": "${{ matrix.browser }}",&lt;/span&gt;
        &lt;span class="s"&gt;"commit": "${{ github.sha }}",&lt;/span&gt;
        &lt;span class="s"&gt;"artifacts_link": "${ARTIFACTS_LINK}"&lt;/span&gt;
      &lt;span class="s"&gt;}&lt;/span&gt;
    &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;

    &lt;span class="s"&gt;curl -X POST https://api.emailjs.com/api/v1.0/email/send \&lt;/span&gt;
      &lt;span class="s"&gt;-H 'origin&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost' \&lt;/span&gt;
      &lt;span class="s"&gt;-H 'Content-Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json' \&lt;/span&gt;
      &lt;span class="s"&gt;-d @email-payload-success.json&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  ❌ Failure Email Step
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&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="s"&gt;Send Failure Email for ${{ matrix.browser }}&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;EMAILJS_SERVICE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EMAILJS_SERVICE_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;EMAILJS_TEMPLATE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EMAILJS_TEMPLATE_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;EMAILJS_USER_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EMAILJS_USER_ID }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;ARTIFACTS_LINK="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"&lt;/span&gt;

    &lt;span class="s"&gt;cat &amp;lt;&amp;lt;EOF &amp;gt; email-payload-failure.json&lt;/span&gt;
    &lt;span class="s"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"service_id": "${EMAILJS_SERVICE_ID}",&lt;/span&gt;
      &lt;span class="s"&gt;"template_id": "${EMAILJS_TEMPLATE_ID}",&lt;/span&gt;
      &lt;span class="s"&gt;"user_id": "${EMAILJS_USER_ID}",&lt;/span&gt;
      &lt;span class="s"&gt;"template_params": {&lt;/span&gt;
        &lt;span class="s"&gt;"title": "❌ UI Test Failed",&lt;/span&gt;
        &lt;span class="s"&gt;"result": "failed",&lt;/span&gt;
        &lt;span class="s"&gt;"browser": "${{ matrix.browser }}",&lt;/span&gt;
        &lt;span class="s"&gt;"commit": "${{ github.sha }}",&lt;/span&gt;
        &lt;span class="s"&gt;"artifacts_link": "${ARTIFACTS_LINK}"&lt;/span&gt;
      &lt;span class="s"&gt;}&lt;/span&gt;
    &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;

    &lt;span class="s"&gt;curl -X POST https://api.emailjs.com/api/v1.0/email/send \&lt;/span&gt;
      &lt;span class="s"&gt;-H 'origin&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost' \&lt;/span&gt;
      &lt;span class="s"&gt;-H 'Content-Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json' \&lt;/span&gt;
      &lt;span class="s"&gt;-d @email-payload-failure.json&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  📬 Real Email Example
&lt;/h2&gt;

&lt;p&gt;Here's what the email looks like when a test passes for the Firefox browser:&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%2Fn35thx8bo4jr9da51nvn.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%2Fn35thx8bo4jr9da51nvn.png" alt="Email" width="672" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is powered by an EmailJS template using variables like &lt;code&gt;{{browser}}&lt;/code&gt;, &lt;code&gt;{{commit}}&lt;/code&gt;, and &lt;code&gt;{{artifacts_link}}&lt;/code&gt;. Everything is injected dynamically per matrix job.&lt;/p&gt;

&lt;p&gt;You can even include emojis, links, or styling directly in the EmailJS editor!&lt;/p&gt;

&lt;h2&gt;
  
  
  🎁 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This setup gives you full visibility over your GitHub test matrix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who broke what browser&lt;/li&gt;
&lt;li&gt;Direct link to test artifacts&lt;/li&gt;
&lt;li&gt;Stylish notifications in your inbox&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No backend. No SMTP. Just clean, dynamic emails — in under 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 What’s Next?
&lt;/h2&gt;

&lt;p&gt;In the next article (Part 3), I’ll show you how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Combine multi-browser reports into one HTML dashboard&lt;/li&gt;
&lt;li&gt;Attach that in a Slack or Teams notification&lt;/li&gt;
&lt;li&gt;Make your matrix strategy 100% team-ready&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💬 Questions or improvements?&lt;br&gt;
💼 Connect with me on &lt;a href="https://www.linkedin.com/in/serhat-ozdursun/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;br&gt;
🌐 More at &lt;a href="//serhatozdursun.com"&gt;serhatozdursun.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>ci</category>
      <category>emailjs</category>
      <category>automation</category>
    </item>
    <item>
      <title>🧪 GitHub Actions Matrix Strategy - Part 2: Send Email Notifications with Test Artifacts</title>
      <dc:creator>Serhat Ozdursun</dc:creator>
      <pubDate>Mon, 14 Apr 2025 12:10:21 +0000</pubDate>
      <link>https://dev.to/serhat_ozdursun_03644ef56/github-actions-matrix-strategy-part-2-send-email-notifications-with-test-artifacts-223</link>
      <guid>https://dev.to/serhat_ozdursun_03644ef56/github-actions-matrix-strategy-part-2-send-email-notifications-with-test-artifacts-223</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/serhat_ozdursun_03644ef56/what-is-github-actions-matrix-strategy-5gma"&gt;Part 1 of this series&lt;/a&gt;, we explored how to use &lt;strong&gt;GitHub Actions matrix strategy&lt;/strong&gt; to run UI automation tests across multiple browsers like Chrome and Firefox in parallel. It was fast, flexible, and scalable.&lt;/p&gt;

&lt;p&gt;But once your matrix jobs run… how do you &lt;strong&gt;get notified&lt;/strong&gt; when something breaks — and more importantly, &lt;strong&gt;which browser failed&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;In this article, we’ll take it one step further by sending &lt;strong&gt;smart email notifications per browser&lt;/strong&gt;, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📬 Pass/fail status
&lt;/li&gt;
&lt;li&gt;🧪 Commit SHA
&lt;/li&gt;
&lt;li&gt;📎 Direct link to the HTML test report (via GitHub Artifacts)
&lt;/li&gt;
&lt;li&gt;✅ Fully integrated with &lt;strong&gt;EmailJS dynamic templates&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s dive in!&lt;/p&gt;




&lt;h2&gt;
  
  
  📬 Why Use EmailJS?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  💡 What is EmailJS?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.emailjs.com/" rel="noopener noreferrer"&gt;EmailJS&lt;/a&gt; allows developers to &lt;strong&gt;send emails directly from frontend apps or serverless environments&lt;/strong&gt; — without needing to set up their own email server or SMTP credentials.&lt;/p&gt;

&lt;p&gt;You define templates with variables (like &lt;code&gt;{{result}}&lt;/code&gt;, &lt;code&gt;{{browser}}&lt;/code&gt;), and send data via a simple REST API. It’s perfect for tools like GitHub Actions or CI/CD pipelines.&lt;/p&gt;




&lt;h3&gt;
  
  
  🛠 Other Tools You Could Use
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Highlights&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SendGrid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full-featured transactional email API&lt;/td&gt;
&lt;td&gt;Setup is more involved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mailgun&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developer-focused with detailed tracking&lt;/td&gt;
&lt;td&gt;Ideal for backend use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SMTP2GO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Traditional SMTP service&lt;/td&gt;
&lt;td&gt;Works well with SMTP clients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Postmark&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reliable transactional delivery and analytics&lt;/td&gt;
&lt;td&gt;Paid plan, high deliverability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EmailJS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No server needed, simple setup, free tier&lt;/td&gt;
&lt;td&gt;Best for client-side &amp;amp; CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  ✅ Why I Chose EmailJS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No backend needed&lt;/strong&gt; – works directly from GitHub Actions
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy template setup&lt;/strong&gt; – fully customizable UI
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free plan&lt;/strong&gt; – generous for small projects or teams
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick integration&lt;/strong&gt; – REST API is simple and secure
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works well with matrix workflows&lt;/strong&gt; – send dynamic info per job
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📄 Dynamic Email Templates in EmailJS
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding email content in your GitHub workflow, you can define a template in EmailJS like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subject:&lt;/strong&gt;&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

{{title}} - {{browser}} {{time}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
    </item>
    <item>
      <title>What is GitHub Actions Matrix Strategy?</title>
      <dc:creator>Serhat Ozdursun</dc:creator>
      <pubDate>Wed, 09 Apr 2025 16:21:42 +0000</pubDate>
      <link>https://dev.to/serhat_ozdursun_03644ef56/what-is-github-actions-matrix-strategy-5gma</link>
      <guid>https://dev.to/serhat_ozdursun_03644ef56/what-is-github-actions-matrix-strategy-5gma</guid>
      <description>&lt;p&gt;The &lt;strong&gt;GitHub Actions matrix strategy&lt;/strong&gt; is a powerful feature that allows you to run the same job across multiple combinations of variables—like environments, operating systems, or software versions—&lt;strong&gt;without duplicating code&lt;/strong&gt;. It's ideal for scenarios such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing across different browser environments&lt;/li&gt;
&lt;li&gt;Running code against multiple versions of a language&lt;/li&gt;
&lt;li&gt;Deploying across multiple platforms (e.g., Android and iOS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By using a &lt;code&gt;strategy.matrix&lt;/code&gt; section in your workflow, you can define all the variations you want to test or execute. GitHub will then automatically create a job for each combination. This not only helps keep workflows clean and scalable, but also provides a clear view of which combinations succeed or fail.&lt;/p&gt;

&lt;p&gt;For example:&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;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;windows-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;16&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;18&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration will run four jobs in parallel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu + Node 16&lt;/li&gt;
&lt;li&gt;Ubuntu + Node 18&lt;/li&gt;
&lt;li&gt;Windows + Node 16&lt;/li&gt;
&lt;li&gt;Windows + Node 18&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Learn more in the official &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow" rel="noopener noreferrer"&gt;GitHub Docs&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Browser UI Testing with GitHub Actions Matrix Strategy
&lt;/h2&gt;

&lt;p&gt;When building UI automation workflows, one of the most common challenges is ensuring consistent functionality across different browsers. Initially, I handled this by creating &lt;strong&gt;separate GitHub Actions YAML files for each browser:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ui_chrome_pytest.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ui_firefox_pytest.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each file had almost identical steps, with only minor differences:&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 Key Differences in the Old Files
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ui_chrome_pytest.yml&lt;/code&gt; manually installs &lt;strong&gt;Google Chrome and ChromeDriver.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ui_firefox_pytest.yml&lt;/code&gt; uses the &lt;code&gt;browser-actions/setup-firefox&lt;/code&gt; action for &lt;strong&gt;Firefox and GeckoDriver.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pytest command uses &lt;code&gt;--browser chrome&lt;/code&gt; in one and &lt;code&gt;--browser firefox&lt;/code&gt; in the other.&lt;/p&gt;

&lt;p&gt;Both upload the same artifacts, but the file names are not browser-specific (they would overwrite each other if run together).&lt;/p&gt;

&lt;h3&gt;
  
  
  This setup worked, but had several downsides:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;❌ Duplication of logic: Every change had to be applied in multiple files.&lt;/li&gt;
&lt;li&gt;❌ Error-prone: It’s easy to forget updating one file.&lt;/li&gt;
&lt;li&gt;❌ Scalability issues: Adding more browsers or platforms meant creating even more nearly-identical files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: Matrix Strategy
&lt;/h2&gt;

&lt;p&gt;So, to eliminate the duplication and better manage different browser setups, I leveraged the &lt;strong&gt;GitHub Actions matrix strategy&lt;/strong&gt;. This approach allows each browser to be tested in parallel, using a single, unified workflow file.&lt;/p&gt;

&lt;p&gt;To solve this, I created a new GitHub Actions workflow using a matrix strategy that dynamically switches between browsers:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ui_cross_browser_test.yml&lt;/code&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UI Automation Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build_and_test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;browser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;chrome&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;firefox&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# Matrix used to define browsers to test&lt;/span&gt;

    &lt;span class="na"&gt;steps&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="s"&gt;Checkout resume repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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="s"&gt;Set up Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;22'&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="s"&gt;Install dependencies and build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;yarn install&lt;/span&gt;
          &lt;span class="s"&gt;yarn build&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="s"&gt;Start the application&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;yarn start &amp;amp;&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&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="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;

      &lt;span class="c1"&gt;# Chrome-specific installation&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="s"&gt;Install Chrome and ChromeDriver&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.browser == 'chrome'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sudo apt update&lt;/span&gt;
          &lt;span class="s"&gt;sudo apt install -y wget gnupg&lt;/span&gt;
          &lt;span class="s"&gt;wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb&lt;/span&gt;
          &lt;span class="s"&gt;sudo apt install -y ./google-chrome-stable_current_amd64.deb&lt;/span&gt;
          &lt;span class="s"&gt;CHROMEDRIVER_VERSION=$(curl -sS https://chromedriver.storage.googleapis.com/LATEST_RELEASE)&lt;/span&gt;
          &lt;span class="s"&gt;wget https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip&lt;/span&gt;
          &lt;span class="s"&gt;unzip chromedriver_linux64.zip&lt;/span&gt;
          &lt;span class="s"&gt;sudo mv chromedriver /usr/local/bin/chromedriver&lt;/span&gt;
          &lt;span class="s"&gt;sudo chmod +x /usr/local/bin/chromedriver&lt;/span&gt;

      &lt;span class="c1"&gt;# Firefox-specific setup using reusable action&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="s"&gt;Set up Firefox and GeckoDriver&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.browser == 'firefox'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;browser-actions/setup-firefox@latest&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="s"&gt;Clone the test repository&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git clone https://github.com/serhatozdursun/serhatozdursun-ui-tests.git&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.UI_TEST_TOKEN }}&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="s"&gt;Install Python dependencies&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serhatozdursun-ui-tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;

      &lt;span class="c1"&gt;# Matrix-aware pytest command&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="s"&gt;Run tests on ${{ matrix.browser }}&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serhatozdursun-ui-tests&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;run_tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pytest --browser ${{ matrix.browser }} --base_url http://localhost:3000 --html=reports/html/report.html --junitxml=reports/report.xml&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="s"&gt;Upload HTML report&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&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="s"&gt;html-report-${{ matrix.browser }}&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;serhatozdursun-ui-tests/reports/html&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="s"&gt;Upload XML report&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&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="s"&gt;xml-report-${{ matrix.browser }}&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;serhatozdursun-ui-tests/reports/report.xml&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/serhatozdursun/resume/blob/main/.github/workflows/ui_cross_browser_test.yml" rel="noopener noreferrer"&gt;View the full workflow file&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Before vs After
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Separate files&lt;/td&gt;
&lt;td&gt;Easy to isolate logic&lt;/td&gt;
&lt;td&gt;Repetitive, hard to scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matrix strategy&lt;/td&gt;
&lt;td&gt;Clean, DRY, scalable, browser-aware&lt;/td&gt;
&lt;td&gt;Slightly more complex YAML logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Using GitHub Actions' &lt;strong&gt;matrix strategy&lt;/strong&gt; is a game-changer for cross-browser testing. If you're managing separate workflows per browser, consolidating them will make your CI setup much more efficient.&lt;/p&gt;

&lt;p&gt;Stay sharp, test smart! 🚀🧪&lt;/p&gt;

&lt;p&gt;Connect with me on GitHub: &lt;a href="https://serhatozdursun.com/" rel="noopener noreferrer"&gt;serhatozdursun.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>crossbrowsser</category>
      <category>testing</category>
    </item>
    <item>
      <title>Java Checkstyle Tool: Enforce Coding Standards with Ease</title>
      <dc:creator>Serhat Ozdursun</dc:creator>
      <pubDate>Tue, 25 Mar 2025 13:13:20 +0000</pubDate>
      <link>https://dev.to/serhat_ozdursun_03644ef56/java-checkstyle-tool-enforce-coding-standards-with-ease-loe</link>
      <guid>https://dev.to/serhat_ozdursun_03644ef56/java-checkstyle-tool-enforce-coding-standards-with-ease-loe</guid>
      <description>&lt;p&gt;As software projects grow, maintaining code consistency becomes increasingly challenging—especially in teams. Java, being a verbose and widely-used language, is particularly prone to inconsistencies in formatting, naming, and structure. That’s where Checkstyle comes in.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Checkstyle&lt;/strong&gt; is a static code analysis tool for Java that helps developers write code that adheres to a defined coding standard. It enforces style guidelines by checking source code against a configurable set of rules, making your codebase cleaner, more readable, and easier to maintain.&lt;/p&gt;

&lt;p&gt;Whether you're working solo or in a team, using Checkstyle ensures uniformity and reduces time spent in code reviews discussing stylistic issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Enforces coding standards (Google, Sun, or custom)&lt;/li&gt;
&lt;li&gt;Detects common code smells&lt;/li&gt;
&lt;li&gt;Integrates with Maven and Gradle&lt;/li&gt;
&lt;li&gt;Supports IDE plugins (IntelliJ, Eclipse)&lt;/li&gt;
&lt;li&gt;Highly customizable via XML config&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Maven Projects
&lt;/h3&gt;

&lt;p&gt;Add the following plugin to your pom.xml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.maven.plugins&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;maven-checkstyle-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;3.3.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;configLocation&amp;gt;&lt;/span&gt;google_checks.xml&lt;span class="nt"&gt;&amp;lt;/configLocation&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;encoding&amp;gt;&lt;/span&gt;UTF-8&lt;span class="nt"&gt;&amp;lt;/encoding&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;consoleOutput&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/consoleOutput&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;failsOnError&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/failsOnError&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;phase&amp;gt;&lt;/span&gt;validate&lt;span class="nt"&gt;&amp;lt;/phase&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;check&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For Gradle Projects
&lt;/h3&gt;

&lt;p&gt;Add the plugin to build.gradle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'checkstyle'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;checkstyle&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;toolVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'10.3.4'&lt;/span&gt;
    &lt;span class="n"&gt;configFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'config/checkstyle/checkstyle.xml'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Checkstyle&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Built-in or Custom Rules
&lt;/h2&gt;

&lt;p&gt;Checkstyle supports built-in configurations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;google_checks.xml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sun_checks.xml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or you can define custom rules. Below are some practical examples for test automation projects, like I did:&lt;/p&gt;

&lt;h3&gt;
  
  
  Prevent &lt;code&gt;Thread.sleep()&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;module&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Regexp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"format"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Thread\.sleep\(.*\)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"illegalPattern"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Do not use Thread.sleep(). Use an explicit wait instead."&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/module&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prevent &lt;code&gt;By.xpath(...)&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;module&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Regexp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"format"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"By\.xpath\(.*\)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"illegalPattern"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Avoid using XPath locators. Use ID, class, or accessibility locators instead."&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/module&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prevent &lt;code&gt;System.out.println&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;module&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Regexp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"format"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"System\.out\.println"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"illegalPattern"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Use a logger instead of System.out.println."&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/module&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running Checkstyle
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Maven&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;mvn checkstyle:check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gradle&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;./gradlew checkstyleMain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Manual CLI:&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;java &lt;span class="nt"&gt;-jar&lt;/span&gt; checkstyle-10.3-all.jar &lt;span class="nt"&gt;-c&lt;/span&gt; /path/to/config.xml MyClass.java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  GitHub Actions CI Integration
&lt;/h2&gt;

&lt;p&gt;You can automate Checkstyle checks with GitHub Actions and &lt;strong&gt;prevent merging code that doesn't comply with your defined coding standards.&lt;/strong&gt; This ensures every commit meets your team's style expectations before it's integrated into your main branch.&lt;/p&gt;

&lt;p&gt;You can automate Checkstyle checks with GitHub Actions:&lt;/p&gt;

&lt;p&gt;Create .github/workflows/checkstyle.yml:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkstyle&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;checkstyle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&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="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&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="s"&gt;Set up Java&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temurin'&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="s"&gt;Build and Run Checkstyle&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mvn checkstyle:check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;./gradlew checkstyleMain&lt;/code&gt; instead if you're on Gradle.&lt;/p&gt;

&lt;h2&gt;
  
  
  IDE Support
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IntelliJ IDEA:&lt;/strong&gt; Use the CheckStyle-IDEA plugin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eclipse:&lt;/strong&gt; Use the Checkstyle Plugin via Eclipse Marketplace&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Checkstyle is more than just a style enforcer—it's a tool that encourages code quality, team consistency, and professional discipline. Whether you're building a new application or maintaining legacy code, integrating &lt;strong&gt;Checkstyle&lt;/strong&gt; is a low-effort, high-reward decision.&lt;/p&gt;

&lt;p&gt;Start simple with Google or Sun checks, then evolve your configuration with custom rules that make sense for your team and project.&lt;/p&gt;

</description>
      <category>checkstyle</category>
      <category>java</category>
      <category>codequality</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Streamlining Software Development with GitHub Actions: Automation, Testing, and Deployment</title>
      <dc:creator>Serhat Ozdursun</dc:creator>
      <pubDate>Tue, 18 Mar 2025 12:19:32 +0000</pubDate>
      <link>https://dev.to/serhat_ozdursun_03644ef56/streamlining-software-development-with-github-actions-automation-testing-and-deployment-1bmc</link>
      <guid>https://dev.to/serhat_ozdursun_03644ef56/streamlining-software-development-with-github-actions-automation-testing-and-deployment-1bmc</guid>
      <description>&lt;p&gt;GitHub Actions is a powerful CI/CD (Continuous Integration and Continuous Deployment) tool integrated directly into GitHub, allowing developers and QA engineers to automate, customize, and execute workflows within repositories. This tool simplifies the software development lifecycle by automating build, test, deployment, and other routine tasks directly from GitHub repositories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features of GitHub Actions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integrated CI/CD Workflows:&lt;/strong&gt; Automate your development pipeline with ease.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform Support:&lt;/strong&gt; Execute workflows on Linux, macOS, and Windows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix Builds:&lt;/strong&gt; Run multiple workflows in parallel with different configurations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensive Marketplace:&lt;/strong&gt; Utilize pre-built actions from a rich community-driven marketplace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Feedback:&lt;/strong&gt; Immediate visibility into the state of workflows and tests directly within GitHub pull requests and commits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Secrets Management:&lt;/strong&gt; Store sensitive information securely with GitHub secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example: Static Code Analysis with CodeQL
&lt;/h2&gt;

&lt;p&gt;Below is a practical example workflow for performing static code analysis on pull requests using CodeQL to detect vulnerabilities and quality issues in TypeScript and JavaScript code:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CodeQL Static Code Analysis&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;analyze&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="s"&gt;Analyze&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;language&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;typescript'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;javascript'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;steps&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="s"&gt;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
        &lt;span class="c1"&gt;# Checks out your repository's code.&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="s"&gt;Initialize CodeQL&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/init@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;languages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.language }}&lt;/span&gt;
        &lt;span class="c1"&gt;# Initializes the CodeQL environment for specified languages.&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="s"&gt;Build code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;22'&lt;/span&gt;
        &lt;span class="c1"&gt;# Sets up Node.js environment.&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if [ -f package-lock.json ]; then&lt;/span&gt;
            &lt;span class="s"&gt;npm install&lt;/span&gt;
          &lt;span class="s"&gt;elif [ -f yarn.lock ]; then&lt;/span&gt;
            &lt;span class="s"&gt;yarn install&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
        &lt;span class="c1"&gt;# Installs dependencies based on lock files.&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="s"&gt;Perform CodeQL Analysis&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/analyze@v2&lt;/span&gt;
        &lt;span class="c1"&gt;# Runs the actual code analysis.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using GitHub Actions for QA Automation
&lt;/h2&gt;

&lt;p&gt;Quality Assurance engineers leverage GitHub Actions to run automated end-to-end (E2E) tests seamlessly within the development pipeline. Here’s a detailed example workflow for running UI Automation Tests on Chrome:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UI Automation Test for Chrome&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build_and_test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&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="s"&gt;Checkout resume repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
        &lt;span class="c1"&gt;# Fetches the latest code from the repository.&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="s"&gt;Set up Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;22'&lt;/span&gt;
        &lt;span class="c1"&gt;# Sets up the Node.js environment.&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="s"&gt;Install dependencies and build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;yarn install&lt;/span&gt;
          &lt;span class="s"&gt;yarn build&lt;/span&gt;
        &lt;span class="c1"&gt;# Installs necessary dependencies and builds the application.&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="s"&gt;Start the application&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;yarn start &amp;amp;&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
        &lt;span class="c1"&gt;# Starts the application on port 3000.&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="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;
        &lt;span class="c1"&gt;# Sets up the Python environment for running test scripts.&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="s"&gt;Install Chrome&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sudo apt update&lt;/span&gt;
          &lt;span class="s"&gt;sudo apt install -y wget gnupg&lt;/span&gt;
          &lt;span class="s"&gt;wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb&lt;/span&gt;
          &lt;span class="s"&gt;sudo apt install -y ./google-chrome-stable_current_amd64.deb&lt;/span&gt;
        &lt;span class="c1"&gt;# Installs Google Chrome for browser automation testing.&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="s"&gt;Install ChromeDriver&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;CHROMEDRIVER_VERSION=$(curl -sS https://chromedriver.storage.googleapis.com/LATEST_RELEASE)&lt;/span&gt;
          &lt;span class="s"&gt;wget https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip&lt;/span&gt;
          &lt;span class="s"&gt;unzip chromedriver_linux64.zip&lt;/span&gt;
          &lt;span class="s"&gt;sudo mv chromedriver /usr/local/bin/chromedriver&lt;/span&gt;
          &lt;span class="s"&gt;sudo chmod +x /usr/local/bin/chromedriver&lt;/span&gt;
        &lt;span class="c1"&gt;# Installs ChromeDriver required by Selenium tests.&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="s"&gt;Clone the test repository&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git clone https://github.com/serhatozdursun/serhatozdursun-ui-tests.git&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.UI_TEST_TOKEN }}&lt;/span&gt;
        &lt;span class="c1"&gt;# Clones the repository containing UI test scripts.&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="s"&gt;Install Python dependencies&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serhatozdursun-ui-tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
        &lt;span class="c1"&gt;# Installs Python dependencies required by the UI tests.&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="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serhatozdursun-ui-tests&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;run_tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pytest --browser chrome --base_url http://localhost:3000 --html=reports/html/report.html --junitxml=reports/report.xml&lt;/span&gt;
        &lt;span class="c1"&gt;# Executes the UI automation tests using pytest and Selenium.&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="s"&gt;Upload HTML report&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&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="s"&gt;html-report&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;serhatozdursun-ui-tests/reports/html&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="c1"&gt;# Uploads the HTML report for test results.&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="s"&gt;Upload XML report&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&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="s"&gt;xml-report&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;serhatozdursun-ui-tests/reports/report.xml&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="c1"&gt;# Uploads the XML report for test results.&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step-by-step summary and objectives:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checkout Repository:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Fetches the latest version of the application code from the repository.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up Node.js:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Prepares the environment required for Node.js application execution.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install Dependencies and Build:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Installs necessary packages and builds the application to verify code integrity and build stability.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start the Application:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Runs the application locally within the GitHub Actions environment to perform realistic E2E testing.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up Python Environment:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Configures Python, essential for running Selenium tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install Google Chrome:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Provides the browser needed to execute Selenium-based UI automation tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install ChromeDriver:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Installs the driver for Selenium to interface with Chrome for browser automation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clone Test Repository:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Retrieves the latest version of automated UI tests from the dedicated repository.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install Python Dependencies:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Ensures all required libraries and packages are available for the test scripts to run successfully.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Tests:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Executes Selenium tests against the locally running application, identifying potential UI or functional issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload HTML Report:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Provides an easy-to-read HTML format report for reviewing test results directly from the GitHub interface.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload XML Report:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Objective: Supplies detailed test results in XML format, useful for integration with test management systems or further analysis.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By setting up parallel workflows (one for Chrome, another for Firebase), you achieve effective cross-browser testing for each pull request, running tests in isolated containers before merging to master. This ensures that only thoroughly tested code is merged, significantly reducing potential issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comprehensive Deployment Workflow: Automating Reliable and Efficient Website Deployments
&lt;/h2&gt;

&lt;p&gt;This detailed DevOps automation example illustrates how to reliably deploy code changes to production. The workflow automatically triggers upon merging new code into the main branch, ensuring minimal downtime through structured steps, providing stability and reliability:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Maintenance Mode: Activates maintenance mode to inform users about ongoing updates.&lt;/li&gt;
&lt;li&gt;Server Shutdown &amp;amp; Update: Gracefully stops the running server, ensures no residual processes, and pulls the latest code from the repository.&lt;/li&gt;
&lt;li&gt;Application Build: Installs dependencies and builds the latest version of the application.&lt;/li&gt;
&lt;li&gt;Server Restart: Restarts the application server using PM2 to ensure the application is live again.&lt;/li&gt;
&lt;li&gt;Disable Maintenance Mode: Removes the maintenance notification, returning the website to normal operation.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Website&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;set_maintenance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&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="s"&gt;Set maintenance mode&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v0.1.10&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;touch /var/www/maintenance.flag&lt;/span&gt;
            &lt;span class="s"&gt;echo "Maintenance mode enabled."&lt;/span&gt;

  &lt;span class="na"&gt;stop_server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;set_maintenance&lt;/span&gt;
    &lt;span class="na"&gt;steps&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="s"&gt;Stop the server and fetch the latest code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v0.1.10&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;cd ~/repo/resume&lt;/span&gt;
            &lt;span class="s"&gt;export PATH=/root/.nvm/versions/node/v23.6.0/bin:$PATH&lt;/span&gt;
            &lt;span class="s"&gt;nvm alias default 23&lt;/span&gt;
            &lt;span class="s"&gt;pm2 stop so-website || true&lt;/span&gt;
            &lt;span class="s"&gt;PID=$(sudo netstat -tulnp | grep :3000 | awk '{print $7}' | cut -d'/' -f1)&lt;/span&gt;
            &lt;span class="s"&gt;if [ -n "$PID" ]; then&lt;/span&gt;
              &lt;span class="s"&gt;sudo kill -9 $PID&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
            &lt;span class="s"&gt;rm -rf .next&lt;/span&gt;
            &lt;span class="s"&gt;git pull origin main&lt;/span&gt;

  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stop_server&lt;/span&gt;
    &lt;span class="na"&gt;steps&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="s"&gt;Build the app&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v0.1.10&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;cd ~/repo/resume&lt;/span&gt;
            &lt;span class="s"&gt;export PATH=/root/.nvm/versions/node/v23.6.0/bin:$PATH&lt;/span&gt;
            &lt;span class="s"&gt;nvm alias default 23&lt;/span&gt;
            &lt;span class="s"&gt;yarn install&lt;/span&gt;
            &lt;span class="s"&gt;yarn build&lt;/span&gt;

  &lt;span class="na"&gt;start_server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;steps&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="s"&gt;Start the app&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v0.1.10&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;cd ~/repo/resume&lt;/span&gt;
            &lt;span class="s"&gt;export PATH=/root/.nvm/versions/node/v23.6.0/bin:$PATH&lt;/span&gt;
            &lt;span class="s"&gt;if ! command -v pm2 &amp;amp;&amp;gt; /dev/null; then&lt;/span&gt;
              &lt;span class="s"&gt;npm install -g pm2&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
            &lt;span class="s"&gt;pm2 start yarn --name "so-website" -- start&lt;/span&gt;
            &lt;span class="s"&gt;pm2 save&lt;/span&gt;

  &lt;span class="na"&gt;disable_maintenance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;start_server&lt;/span&gt;
    &lt;span class="na"&gt;steps&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="s"&gt;Disable maintenance mode&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v0.1.10&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;rm -f /var/www/maintenance.flag&lt;/span&gt;
            &lt;span class="s"&gt;echo "Maintenance mode disabled."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical Implementation
&lt;/h2&gt;

&lt;p&gt;I've proactively implemented all these GitHub Actions workflows for my personal website (&lt;a href="https://serhatozdursun.com/" rel="noopener noreferrer"&gt;serhatozdursun.com&lt;/a&gt;). You can find the repositories here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website Repository: &lt;a href="https://github.com/serhatozdursun/resume" rel="noopener noreferrer"&gt;serhatozdursun/resume&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Test Automation Repository: &lt;a href="https://github.com/serhatozdursun/serhatozdursun-ui-tests" rel="noopener noreferrer"&gt;serhatozdursun-ui-tests&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These workflows ensure code quality, robust testing, and seamless deployments, significantly enhancing the overall reliability and maintainability of the project.&lt;/p&gt;

</description>
      <category>git</category>
      <category>githubactions</category>
      <category>programming</category>
      <category>qa</category>
    </item>
    <item>
      <title>Understanding Component Testing: A Practical Guide</title>
      <dc:creator>Serhat Ozdursun</dc:creator>
      <pubDate>Thu, 13 Mar 2025 17:25:28 +0000</pubDate>
      <link>https://dev.to/serhat_ozdursun_03644ef56/understanding-component-testing-a-practical-guide-gj9</link>
      <guid>https://dev.to/serhat_ozdursun_03644ef56/understanding-component-testing-a-practical-guide-gj9</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As part of my personal website project, &lt;a href="//serhatozdursun.com"&gt;serhatozdursun.com&lt;/a&gt;, I have implemented component tests to validate the functionality of various UI elements. You can check out the component test package in my &lt;a href="https://github.com/serhatozdursun/resume/tree/main/src/tests" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This project initially served as a practice ground for improving my TypeScript and DevOps skills. I previously wrote a detailed article on the CI/CD and software development process from a QA perspective, which you can read &lt;a href="https://medium.com/@serhat-ozdursun/understanding-the-ci-cd-and-softare-development-proccess-as-a-qa-d2c1e384aaf8" rel="noopener noreferrer"&gt;here&lt;/a&gt;. That article was high-level and broad, so I’ve decided to start a new series focusing on specific topics, beginning with component testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a Component Test?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A component test is a type of front-end test that verifies whether an individual component behaves as expected in isolation. These tests ensure that the UI elements and their interactions function correctly, without requiring the entire application to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features of Component Tests:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1. Test a single component in isolation.&lt;/li&gt;
&lt;li&gt;3. Simulate user interactions (clicks, form submissions, input validation, etc.).&lt;/li&gt;
&lt;li&gt;5. Use mocks and spies to prevent real API calls. &lt;/li&gt;
&lt;li&gt;7. Ensure the correct rendering of UI elements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Writing a Component Test: Example Breakdown&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's analyze a real example of a component test from my &lt;code&gt;ContactForm&lt;/code&gt; component:&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="nx"&gt;React&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;react&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;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fireEvent&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;@testing-library/react&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/jest-dom&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;ContactForm&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;../components/ContactForm&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;userEvent&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;@testing-library/user-event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailjs-com&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="na"&gt;send&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mockResolvedValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email sent&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;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;ContactForm&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;displays error message when email exceeds max length&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;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ContactForm&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;fireEvent&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="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Send a message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nx"&gt;fireEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByPlaceholderText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Email&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;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&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@test.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email cannot exceed 50 characters.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;strong&gt;Breakdown of the Test&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rendering the Component: The render() function mounts the ContactForm component in a virtual DOM.&lt;/li&gt;
&lt;li&gt;Simulating User Interaction:

&lt;ul&gt;
&lt;li&gt;Clicking the 'Send a message' button. &lt;/li&gt;
&lt;li&gt;Typing an excessively long email ('&lt;a href="mailto:test@test.com"&gt;test@test.com&lt;/a&gt;'.repeat(10)).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Checking the Expected Output: 

&lt;ul&gt;
&lt;li&gt;The test ensures that the correct error message appears ('Email cannot exceed 50 characters.').&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This test helps us verify that the form correctly handles validation and displays error messages for incorrect user input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Component Tests vs. Other Types of Tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Component testing is distinct from other testing approaches, and understanding these differences is crucial.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;What Component Tests Are Not:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Integration Tests&lt;/strong&gt; → Component tests do not interact with multiple modules or external services like APIs. They focus solely on the behavior of a single component.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;End-to-End (E2E) Tests&lt;/strong&gt; → These tests do not simulate a full user journey within a real browser. Instead, they test components in isolation.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Why Component Tests Are Not Pure Unit Tests:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A pure unit test typically verifies a single function or method in complete isolation, without rendering the UI.&lt;/p&gt;

&lt;p&gt;In contrast, component tests render an entire component, meaning they check how multiple elements interact (e.g., inputs, buttons, validation messages).&lt;/p&gt;

&lt;p&gt;Since user interactions are simulated, component tests go beyond typical unit testing, though they can still be considered unit tests in the context of UI testing.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Where Component Tests Fit:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To better understand where component tests belong, let’s break down testing levels in software development:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Unit Tests (Lowest Level) 🛠️&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focuses&lt;/strong&gt; on individual functions or methods.&lt;/li&gt;
&lt;li&gt;Typically &lt;strong&gt;tests&lt;/strong&gt; pure logic without dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example&lt;/strong&gt;: Testing a function that formats dates.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Component Tests 🏗️ (Where our focus is!)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt; a single UI component in isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ensures&lt;/strong&gt; that a component renders correctly and responds to user interactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example&lt;/strong&gt;: Ensuring a form component displays validation errors when invalid input is provided.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Integration Tests 🔗&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt; interactions between multiple components or external services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifies&lt;/strong&gt; that modules communicate correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example&lt;/strong&gt;: Checking if a login form correctly sends credentials to an authentication API.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;End-to-End (E2E) Tests (Highest Level) 🌍&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simulates&lt;/strong&gt; a real user’s journey through the application.&lt;/li&gt;
&lt;li&gt;Ensures everything &lt;strong&gt;works together&lt;/strong&gt; as expected in a live-like environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example&lt;/strong&gt;: Testing an entire checkout process from selecting a product to completing a purchase.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why Are Component Tests Important?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Component testing offers multiple benefits, including:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fast Feedback&lt;/strong&gt; 🚀

&lt;ul&gt;
&lt;li&gt;Since components are tested in isolation, these tests run faster than integration or E2E tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early Bug Detection&lt;/strong&gt; 🐞

&lt;ul&gt;
&lt;li&gt;UI bugs are caught before they reach production, reducing costly fixes later.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ensures UI Stability&lt;/strong&gt; 🎨

&lt;ul&gt;
&lt;li&gt;Helps prevent accidental UI regressions when modifying components.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improves Developer Confidence&lt;/strong&gt; ✅

&lt;ul&gt;
&lt;li&gt;Developers can make changes without fear of breaking UI functionality.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Component tests are a crucial part of modern front-end testing strategies, ensuring that individual UI elements function as expected. In this article, we explored what component testing is, broke down a real-world example, and compared it with other types of tests.&lt;/p&gt;

&lt;p&gt;This is the first article in a new series where I’ll continue diving deeper into testing strategies for front-end development. Stay tuned for more!&lt;/p&gt;

&lt;p&gt;If you found this useful, check out my &lt;a href="https://github.com/serhatozdursun/resume" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; here and my other DevOps and QA articles on &lt;a href="https://medium.com/@serhat-ozdursun/understanding-the-ci-cd-and-softare-development-proccess-as-a-qa-d2c1e384aaf8" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;. 🚀&lt;/p&gt;

</description>
      <category>qa</category>
      <category>testing</category>
      <category>cicd</category>
      <category>programming</category>
    </item>
    <item>
      <title>Unlocking the Power of Testcontainers: Scalable, Reliable, and Efficient Testing</title>
      <dc:creator>Serhat Ozdursun</dc:creator>
      <pubDate>Mon, 10 Mar 2025 11:53:07 +0000</pubDate>
      <link>https://dev.to/serhat_ozdursun_03644ef56/unlocking-the-power-of-testcontainers-scalable-reliable-and-efficient-testing-4phl</link>
      <guid>https://dev.to/serhat_ozdursun_03644ef56/unlocking-the-power-of-testcontainers-scalable-reliable-and-efficient-testing-4phl</guid>
      <description>&lt;p&gt;&lt;strong&gt;What is Testcontainers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://testcontainers.com/" rel="noopener noreferrer"&gt;Testcontainers&lt;/a&gt; is a popular open-source library that allows developers to run lightweight, throwaway containers for integration testing. It provides real dependencies, such as databases, message brokers, and browsers, inside Docker containers, ensuring consistency across different environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Experience with Testcontainers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a test automation engineer, I have used Testcontainers in a TypeScript project with Selenium and Cucumber to improve test reliability and streamline execution in a Bitbucket CI/CD pipeline. It allowed me to run browser tests inside a disposable, isolated environment without worrying about local setup inconsistencies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By leveraging Testcontainers for Selenium, I could:&lt;/li&gt;
&lt;li&gt;Run Chrome browser tests in a containerized environment.&lt;/li&gt;
&lt;li&gt;Ensure a clean, fresh browser instance for each test run.&lt;/li&gt;
&lt;li&gt;Record executions as MP4 videos for debugging test failures.&lt;/li&gt;
&lt;li&gt;Easily integrate with a CI/CD pipeline to maintain consistency across different environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, let’s dive into how Testcontainers works and why it's a game-changer for integration testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why QA Engineers Love Testcontainers&lt;/strong&gt;&lt;br&gt;
Testcontainers provides a robust, real-world testing environment that eliminates unreliable test setups. Instead of using mocks or in-memory solutions, QA engineers can leverage real services running in isolated containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key benefits include:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable Test Environments:&lt;/strong&gt; Ensures every test starts with a clean, isolated state, reducing flaky test failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing with Real Dependencies:&lt;/strong&gt; Instead of mocks, Testcontainers provides actual databases, message brokers, and browser instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Browser UI Testing:&lt;/strong&gt; Supports headless and full-browser Selenium tests, ensuring compatibility across environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seamless CI/CD Integration:&lt;/strong&gt; Runs smoothly in Bitbucket Pipelines, GitHub Actions, and GitLab CI/CD.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MP4 Video Recording for Debugging:&lt;/strong&gt; With .withRecording(), every browser session can be recorded, helping teams debug failures visually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster Test Feedback with Wait Strategies:&lt;/strong&gt; Ensures containers fully initialize before tests start, preventing race conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable Environments:&lt;/strong&gt; QA teams can define test environments using GenericContainer or build custom Dockerfile-based containers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Testcontainers, QA teams can run more accurate, reliable, and efficient tests that closely mimic production conditions. 🚀&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Types of Containers Supported&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Databases:&lt;/strong&gt; PostgreSQL, MySQL, MongoDB, Redis, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message Brokers:&lt;/strong&gt; Kafka, RabbitMQ&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selenium:&lt;/strong&gt; Running headless browsers for UI testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Containers:&lt;/strong&gt; Define your own images and configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Wait Strategies&lt;/strong&gt;&lt;br&gt;
Testcontainers offers several Wait Strategies to ensure that a container is ready before interacting with it. These strategies help avoid race conditions where a test might attempt to use a container before it has fully initialized. Some common wait strategies include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wait.forLogMessage(regex, times)&lt;/strong&gt; – Waits for a specific log message to appear a certain number of times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait.forHealthCheck()&lt;/strong&gt; – Waits for a container’s health check to return a healthy status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait.forListeningPorts(port)&lt;/strong&gt; – Ensures that a specific port inside the container is open before proceeding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait.forHttp(path, statusCode)&lt;/strong&gt; – Waits for an HTTP endpoint to respond with a specific status code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example of using a wait strategy in Testcontainers for a PostgreSQL container:&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;GenericContainer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Wait&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="s2"&gt;testcontainers&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;container&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;GenericContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres:latest&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;withExposedPorts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POSTGRES_USER&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="s2"&gt;testuser&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;withEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POSTGRES_PASSWORD&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="s2"&gt;testpassword&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;withWaitStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forLogMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;database system is ready to accept connections&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using wait strategies ensures that the container is fully initialized before the test suite begins execution, reducing flakiness in tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building Your Own Images&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In addition to using pre-built container images, Testcontainers allows you to build and use custom container images tailored to your test needs. You can create a custom image using &lt;code&gt;GenericContainer&lt;/code&gt; and specify a Dockerfile or existing image:&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;GenericContainer&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="s2"&gt;testcontainers&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;container&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;GenericContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:latest&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;withCopyFileToContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./my-app&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="s2"&gt;/usr/src/app&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;withExposedPorts&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="nf"&gt;withCommand&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm&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="s2"&gt;start&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;start&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="s2"&gt;`Server running at http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMappedPort&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="s2"&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 approach is useful when testing applications that require custom dependencies or specific configurations. You can also build an image from a Dockerfile dynamically using withBuild:&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;container&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;GenericContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromDockerfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./path/to/Dockerfile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Using Testcontainers in TypeScript&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Below is an example of using &lt;strong&gt;Testcontainers with Selenium&lt;/strong&gt; in a TypeScript project for running UI tests in a CI/CD pipeline (e.g., Bitbucket).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker installed on your machine or CI environment.&lt;/li&gt;
&lt;li&gt;Node.js and TypeScript installed.&lt;/li&gt;
&lt;li&gt;Selenium WebDriver dependencies (selenium-webdriver, @testcontainers/selenium, dotenv, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Setting Up Testcontainers for Selenium&lt;/strong&gt;&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dotenv&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;dotenv&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;After&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AfterAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BeforeAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDefaultTimeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Status&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;@cucumber/cucumber&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;Browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Builder&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;selenium-webdriver&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;chrome&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;selenium-webdriver/chrome&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;SeleniumContainer&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;@testcontainers/selenium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;setDefaultTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;BeforeAll&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="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;container&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;SeleniumContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;seleniarm/standalone-chromium:latest&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;withRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Enables video recording of the test execution&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;Before&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chromeOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--start-maximized&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;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--disable-notifications&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;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--no-sandbox&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;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--headless&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CHROME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setChromeOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chromeOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usingServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getServerUrl&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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;manage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setTimeouts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;implicit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pageLoad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&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;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;https://example.com/login&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;manage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;deleteAllCookies&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;testCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&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="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FAILED&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;image&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;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64:image/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="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;quit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;AfterAll&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stoppedContainer&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;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&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;stoppedContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveRecording&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./image/lastExecution.mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Saves the execution video&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Running Testcontainers in a CI/CD Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Testcontainers integrates seamlessly into Bitbucket Pipelines (or other CI/CD environments) to ensure consistent test execution across different environments. Below is a sample bitbucket-pipelines.yml configuration for running Selenium tests:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:latest&lt;/span&gt;
&lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;step&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="s"&gt;Run Selenium Tests with Testcontainers&lt;/span&gt;
        &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup ensures that your tests run inside a Docker-enabled pipeline, leveraging Testcontainers for reliable and isolated test execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Testcontainers is a powerful tool that simplifies integration testing by providing real, disposable environments. Whether you're testing databases, message queues, or Selenium-based UI tests, it ensures that tests are reliable and consistent across different environments. Additionally, &lt;strong&gt;video recordings of test execution&lt;/strong&gt; allow for easy debugging and verification, making it a valuable tool for both local development and CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;For even more flexibility, &lt;strong&gt;building your own images&lt;/strong&gt; with &lt;code&gt;GenericContainer&lt;/code&gt; or &lt;code&gt;fromDockerfile&lt;/code&gt; enables custom test environments tailored to your application's needs. Using Wait Strategies ensures that containers are fully initialized before execution, reducing flakiness and improving test stability.&lt;/p&gt;

</description>
      <category>testcontainers</category>
      <category>typescript</category>
      <category>selenium</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Custom Waiters in Appium: Ensuring Screen Load Before Interaction</title>
      <dc:creator>Serhat Ozdursun</dc:creator>
      <pubDate>Wed, 05 Mar 2025 12:49:18 +0000</pubDate>
      <link>https://dev.to/serhat_ozdursun_03644ef56/custom-waiters-in-appium-ensuring-screen-load-before-interaction-10a9</link>
      <guid>https://dev.to/serhat_ozdursun_03644ef56/custom-waiters-in-appium-ensuring-screen-load-before-interaction-10a9</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why Do We Need Custom Waiters?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In mobile automation with Appium, there are scenarios where standard explicit waits (e.g., elementToBeVisible) are not sufficient. Some elements may be present in the DOM but not fully loaded, especially when dealing with dynamic content updates. A common case is waiting for a specific label or text to appear before proceeding with interactions.&lt;/p&gt;

&lt;p&gt;For instance, in a mobile application, a loading screen might disappear, but the main screen elements may still be rendering. If we try to interact with elements too soon, we may encounter NoSuchElementException or StaleElementReferenceException.&lt;/p&gt;

&lt;p&gt;A custom waiter can help ensure that the screen is fully loaded before continuing test execution.&lt;/p&gt;

&lt;p&gt;Example Scenario&lt;br&gt;
Let's say we have a mobile screen where a title dynamically loads. Instead of just waiting for the element to be present, we should wait until the label is actually populated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java Example&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isScreenFullyLoaded&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mobileDriver&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mobileDriver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElementByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screenTitle&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How This Helps&lt;br&gt;
Ensures that the screen title has loaded before proceeding.&lt;br&gt;
Avoids false positives where the element exists but is not ready.&lt;br&gt;
Prevents flaky tests by ensuring interactions occur only when the screen is fully rendered.&lt;br&gt;
TypeScript Example (WebdriverIO + Appium)&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isScreenFullyLoaded&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&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;return&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;waitUntil&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;label&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;$&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;screenTitle&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;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timeoutMsg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Screen did not fully load within the expected time&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;How This Helps&lt;br&gt;
Uses WebdriverIO's waitUntil function to check for the label.&lt;br&gt;
Ensures that the text is present before interacting with the screen.&lt;br&gt;
Provides a timeout message to improve debugging.&lt;br&gt;
Python Example (Appium-Python-Client)&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support.ui&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebDriverWait&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_screen_fully_loaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;WebDriverWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;mobile_driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  
  &lt;span class="n"&gt;mobile_driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element_by_accessibility_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screen_title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&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="sh"&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;strong&gt;How This Helps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Uses WebDriverWait with a lambda function to check if the label is populated.&lt;br&gt;
Prevents interacting with elements before they are fully rendered.&lt;br&gt;
Reduces flakiness in mobile automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Using a custom waiter like this improves test stability by ensuring that elements are not just present but fully loaded and ready for interaction. This approach is particularly useful when dealing with dynamically loaded content in mobile applications.&lt;/p&gt;

</description>
      <category>appium</category>
      <category>testing</category>
      <category>qa</category>
      <category>testautomation</category>
    </item>
  </channel>
</rss>
