Smart way of E2E testing using cypress new features cy.prompt

Smart way of E2E testing using cypress new features cy.prompt

How to use cy.prompt in Cypress; this blog introduces cy.prompt, an experimental tool from Cypress designed to simplify web automation by allowing users to write tests using natural language descriptions rather than complex CSS selectors. By leveraging artificial intelligence, the platform enables self-healing capabilities; as a result, tests automatically adapt to UI changes like renamed buttons without failing the entire build. This innovation significantly accelerates test authoring and maintenance, empowering team members without deep coding knowledge to participate in the quality assurance process. Furthermore, the system avoids the limitations of typical AI “black boxes” by providing transparent debugging logs and the option to export AI-generated steps into standard code for long-term stability and peer review.Ultimately, this technology promotes broader team participation by allowing non-technical members to contribute to the testing process without deep knowledge of JavaScript.

In 2025, the release of cy.prompt() fundamentally shifted how teams approach end-to-end testing by introducing a native, AI-powered way to write tests in plain English. This experimental feature, introduced in Cypress 15.4.0, allows you to describe user journeys in natural language, which Cypress then translates into executable commands.

Why use cy.prompt()?

  • Reduced Maintenance: If a UI change (like a renamed ID) breaks a test, cy.prompt() can automatically regenerate selectors through its self-healing capability.
  • Faster Test Creation: As a result, you can go from a business requirement to a running test in seconds without writing manual JavaScript or hunting for selectors.
  • Democratized Testing: Consequently, product managers and non-technical stakeholders are empowered to contribute to automation through Gherkin-style steps in the test suite.
  • Generate and Eject (For Stable Apps):To start, use cy.prompt() to scaffold your test. Once generated, click the “Code” button in the Command Log and save the static code to your spec file; this approach is ideal for CI/CD pipelines that require strictly deterministic, frozen code.
  • Continuous Self-Healing (For Fast-Paced Development): Keep the cy.prompt() commands in your repository. Cypress will use intelligent caching to run at near-native speeds on subsequent runs, only re-calling the AI if the UI changes significantly.

Why it’s “Smart”:

Self-Healing: If a developer changes a class to a test-id, cy.prompt() won’t fail; it re-evaluates the page to find the most logical element.

Speed: It uses Intelligent Caching. The AI is only invoked on the first run; subsequent runs use the cached selector paths, maintaining the lightning-fast speed Cypress is known for.

How to Get Started? How to use cy.prompt in Cypress?

1. Prerequisites and Setup

How to use cy.prompt in Cypress? for AI-driven end-to-end testing with self-healing selectors and faster test creation. Before you can run a program with cy.prompt(), you must configure your environment:

  • Version Requirement: Ensure you are using Cypress 15.4.0 or newer.
  • Enable the Feature: Open your cypress.config.js (or .ts) file and set the experimentalPromptCommand flag to true within the e2e configuration.
const { defineConfig } = require('cypress');


    module.exports = defineConfig({
  projectId: 'abc',
      e2e: {
        experimentalPromptCommand: true,
        setupNodeEvents(on, config) {
          // implement node event listeners here
        },
      },
    });
  • Authenticate with Cypress Cloud: cy.prompt() requires a connection to Cypress Cloud to access the AI models.
    • Local development: Log in to Cypress Cloud directly within the cypress app.
    • CI/CD: Use your record key with the –record –key flag. 

2. Writing Your First Test

  • The command accepts an array of strings representing your test steps. 
describe('Prompt command test', () => {
  it('runs prompt sequence', () => {
    cy.prompt([
      "Visit https://aicotravel.co",
      "Type 'Paris' in the destination field",
      "Click on the first search result",
      "Select 4 days from the duration dropdown",
      "Press the **Create Itinerary** button"
    ])
  })
})

The “smart” way to use cy.prompt() is to combine it with standard commands for a hybrid, high-reliability approach.

describe('User Checkout Flow', () => {
  it('should complete a purchase using AI prompts', () => {
    cy.visit('/store');
    // Simple natural language commands
    cy.prompt('Search for "Wireless Headphones" and click the first result');
    // Using placeholders for sensitive data to ensure privacy
    cy.prompt('Log in with {{email}} and {{password}}', {
      placeholders: {
        email: 'testuser@example.com',
        password: 'SuperSecretPassword123'
      }
    });
    // Verify UI state without complex assertions
    cy.prompt('Ensure the "Add to Cart" button is visible and green');
    cy.get('.cart-btn').click();
  });
});

3. The “Smart” Workflow: Prompt-to-Code

Most professional way to use cy.prompt() is as a code generator.

  1. Drafting: Write your test using cy.prompt().
  2. Execution: Run the test in the Cypress Open mode.
  3. Conversion: Once the AI successfully finds the elements, use the “Convert to Code” button in the Command Log.

Save to File: Copy the generated code and replace your cy.prompt() call with it. Consequently, this turns the AI-generated test into a stable, version-controlled test that runs without AI dependency.

Commit: However cypress will generate the standard .get().click() code based on the AI’s findings. You can then commit this hard-coded version to your repository to avoid unnecessary AI calls in your CI/CD pipeline.

4. Best Practices:

  • Imperative Verbs: Start prompts with “Click,” “Type,” “Select,” or “Verify.”
  • Contextual Accuracy: If a page has two “Submit” buttons, be specific: cy.prompt(‘Click the “Submit” button inside the Newsletter section’).
  • Security First: However, never pass raw passwords into the prompt string. Therefore, always use the placeholders configuration to keep sensitive strings out of the AI logs.
  • Hybrid Strategy: Ultimately, use cy.prompt() where flexibility is needed for complex UI interactions, and fall back to standard cy.get() for stable elements like navigation links.

GitHub:- https://github.com/jjadhav-dj/cypress-demo

Conclusion:

The introduction of cy.prompt() marks the end of “selector hell.” By treating AI as a pair-programmer that handles the tedious task of DOM traversing, we can write tests that are more readable, easier to maintain, and significantly more resilient to UI changes.

Click here to read more blogs like this.


Boosting Web Performance: Integrating Google Lighthouse with Automation Frameworks

Boosting Web Performance: Integrating Google Lighthouse with Automation Frameworks

The Silent Killer of User Experience

Integrating Google Lighthouse with Playwright; Picture this: Your development team just shipped a major feature update. The code passed all functional tests. QA signed off. Everything looks perfect in staging. You hit deploy with confidence.

Then the complaints start rolling in.

“The page takes forever to load.” “Images are broken on mobile.” “My browser is lagging.”

Sound familiar? According to Google, 53% of mobile users abandon sites that take longer than 3 seconds to load. Yet most teams only discover performance issues after they’ve reached production, when the damage to user experience and brand reputation is already done.

The real problem isn’t that teams don’t care about performance. It’s that performance testing is often manual, inconsistent, and disconnected from the development workflow. Performance degradation is gradual. It sneaks up on you. And by the time you notice, you’re playing catch-up instead of staying ahead.

The Gap Between Awareness and Action

Most engineering teams know they should monitor web performance. They’ve heard about Core Web Vitals, Time to Interactive, and First Contentful Paint. They understand that performance impacts SEO rankings, conversion rates, and user satisfaction.

But knowing and doing are two different things.

The challenge lies in making performance testing continuous, automated, and actionable. Manual audits are time-consuming and prone to human error. They create bottlenecks in the release pipeline. What teams need is a way to bake performance testing directly into their automation frameworks to treat performance as a first-class citizen alongside functional testing.

Integrating Google Lighthouse with Playwright

Enter Google Lighthouse.

What Is Google Lighthouse?

Google Lighthouse is an open-source, automated tool designed to improve the quality of web pages. Originally developed by Google’s Chrome team, Lighthouse has become the industry standard for web performance auditing by Integrating Google Lighthouse with Playwright.

But here’s what makes Lighthouse truly powerful: it doesn’t just measure performance it provides actionable insights.

When you run a Lighthouse audit, you get comprehensive scores across five key categories:

  • Performance: Load times, rendering metrics, and resource optimization
  • Accessibility: ARIA attributes, color contrast, semantic HTML
  • Best Practices: Security, modern web standards, browser compatibility
  • SEO: Meta tags, mobile-friendliness, structured data
  • Progressive Web App: Service workers, offline functionality, installability

Each category receives a score from 0 to 100, with detailed breakdowns of what’s working and what needs improvement. The tool analyzes critical metrics like:

  • First Contentful Paint (FCP): When the first content renders
  • Largest Contentful Paint (LCP): When the main content is visible
  • Total Blocking Time (TBT): How long the page is unresponsive
  • Cumulative Layout Shift (CLS): Visual stability during load
  • Speed Index: How quickly content is visually populated

These metrics align directly with Google’s Core Web Vitals the signals that impact search rankings and user experience.

Why Performance Can’t Be an Afterthought

Let’s talk numbers, because performance isn’t just a technical concern it’s a business imperative.

Amazon found that every 100ms of latency cost them 1% in sales. Pinterest increased sign-ups by 15% after reducing perceived wait time by 40%. The BBC discovered they lost an additional 10% of users for every extra second their site took to load.

The data is clear: performance directly impacts your bottom line.

But beyond revenue, there’s the SEO factor. Since 2021, Google has used Core Web Vitals as ranking signals. Sites with poor performance scores get pushed down in search results. You could have the most comprehensive content in your niche, but if your LCP is above 4 seconds, you’re losing visibility.

The question isn’t whether performance matters. The question is: how do you ensure performance doesn’t degrade as your application evolves?

The Power of Integration: Lighthouse Meets Automation

This is where the magic happens when you integrate Google Lighthouse into your automation frameworks.

By Integrating Google Lighthouse with Playwright, Selenium, or Cypress, you transform performance from a periodic manual check into a continuous, automated quality gate.

Here’s what this integration delivers:

1. Consistency Across Environments

Automated Lighthouse tests run in controlled environments with consistent configurations, giving you reliable, comparable data across test runs.

2. Early Detection of Performance Regressions

Instead of discovering performance issues in production, you catch them during development. A developer adds a large unoptimized image? The Lighthouse test fails before the code merges.

3. Performance Budgets and Thresholds

You can set specific performance budgets for example, “Performance score must be above 90.” If a change violates these budgets, the build fails, just like a failing functional test.

4. Comprehensive Reporting

Lighthouse generates detailed HTML and JSON reports with visual breakdowns, diagnostic information, and specific recommendations. These reports become part of your test artifacts.

Google Lighthouse with Playwright

How Integration Works: A High-Level Flow

You don’t need to be a performance expert to integrate Lighthouse into your automation framework. The process is straightforward and fits naturally into existing testing workflows.

Step 1: Install Lighthouse Lighthouse is available as an npm package, making it easy to add to any Node.js-based automation project. It integrates seamlessly with popular frameworks.

Step 2: Configure Your Audits Define what you want to test which pages, which metrics, and what thresholds constitute a pass or fail. You can customize Lighthouse to focus on specific categories or run full audits across all five areas.

Step 3: Integrate with Your Test Suite Add Lighthouse audits to your existing test files. Your automation framework handles navigation and setup, then hands off to Lighthouse for the performance audit. The results come back as structured data you can assert against.

Step 4: Set Performance Budgets Define acceptable thresholds for key metrics. These become your quality gates if performance drops below the threshold, the test fails and the pipeline stops.

Step 5: Generate and Store Reports Configure Lighthouse to generate HTML and JSON reports. Store these as test artifacts in your CI/CD system, making them accessible for review and historical analysis.

Step 6: Integrate with CI/CD Run Lighthouse tests as part of your continuous integration pipeline. Every pull request, every deployment performance gets validated automatically.

The beauty of this approach is that it requires minimal changes to your existing workflow. You’re not replacing your automation framework you’re enhancing it with performance capabilities.

Practical Implementation: Code Examples

Let’s look at how this works in practice with a real Playwright automation framework. Here’s how you can create a reusable Lighthouse runner:

Creating the Lighthouse Runner Utility

async function runLighthouse(url, thresholds = { 
  performance: 50, 
  accessibility: 90, 
  seo: 40, 
  bestPractices: 45 
}) {
  const playwright = await import('playwright');
  const lighthouse = await import('lighthouse');
  const fs = await import('fs');
  const path = await import('path');
  const assert = (await import('assert')).default;

  // Launch browser with debugging port for Lighthouse
  const browser = await playwright.chromium.launch({
    headless: true,
    args: ['--remote-debugging-port=9222']
  });

  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto(url);

  // Configure Lighthouse options
  const options = {
    logLevel: 'info',
    output: 'html',
    onlyCategories: ['performance', 'accessibility', 'seo', 'best-practices'],
    port: 9222,
    preset: 'desktop'
  };

  try {
    // Run Lighthouse audit
    const runnerResult = await lighthouse.default(url, options);
    const report = runnerResult.report;
    
    // Save reports
    const reportFolder = path.resolve(__dirname, '../lighthouse-reports');
    if (!fs.existsSync(reportFolder)) fs.mkdirSync(reportFolder);

    const reportFilename = path.join(reportFolder, `lighthouse-report-${Date.now()}.html`);
    const jsonReportFilename = path.join(reportFolder, `lighthouse-report-${Date.now()}.json`);
    
    fs.writeFileSync(reportFilename, report);
    fs.writeFileSync(jsonReportFilename, JSON.stringify(runnerResult, null, 2));
    
    await browser.close();

    // Extract scores
    const lhr = runnerResult.lhr;
    const performanceScore = lhr.categories.performance.score * 100;
    const accessibilityScore = lhr.categories.accessibility.score * 100;
    const seoScore = lhr.categories.seo.score * 100;
    const bestPracticesScore = lhr.categories['best-practices'].score * 100;

    console.log(`Performance Score: ${performanceScore}`);
    console.log(`Accessibility Score: ${accessibilityScore}`);
    console.log(`SEO Score: ${seoScore}`);
    console.log(`Best Practices Score: ${bestPracticesScore}`);

    // Assert against thresholds
    assert(performanceScore >= thresholds.performance, 
      `Performance score is too low: ${performanceScore}`);
    assert(accessibilityScore >= thresholds.accessibility, 
      `Accessibility score is too low: ${accessibilityScore}`);
    assert(seoScore >= thresholds.seo, 
      `SEO score is too low: ${seoScore}`);
    assert(bestPracticesScore >= thresholds.bestPractices, 
      `Best Practices score is too low: ${bestPracticesScore}`);

    console.log("All assertions passed!");
    return lhr;
    
  } catch (error) {
    console.error(`Lighthouse audit failed: ${error.message}`);
    await browser.close();
    throw error;
  }
}

module.exports = { runLighthouse };

Integrating with Your Page Objects

const { runLighthouse } = require("../Utility/lighthouseRunner");

class LighthousePage {
  async visitWebPage() {
    await global.newPage.goto(process.env.WEBURL, { timeout: 30000 });
  }
  
  async initiateLighthouseAudit() {
    await runLighthouse(await global.newPage.url());
  }
}

module.exports = LighthousePage;

BDD Test Scenario with Cucumber

Feature: Integrating Google Lighthouse with the Test Automation Framework

  This feature leverages Google Lighthouse to evaluate the performance, 
  accessibility, SEO, and best practices of web pages.

  @test
  Scenario: Validate the Lighthouse Performance Score for the Playwright Official Page
    Given I navigate to the Playwright official website
    When I initiate the Lighthouse audit
    And I click on the "Get started" button
    And I wait for the Lighthouse report to be generated
    Then I generate the Lighthouse report

Decoding Lighthouse Reports: What the Data Tells You

Lighthouse reports are information-rich, but they’re designed to be actionable, not overwhelming. Let’s break down what you get:

The Performance Score

This is your headline number a weighted average of key performance metrics. A score of 90-100 is excellent, 50-89 needs improvement, and below 50 requires immediate attention.

Metric Breakdown

Each performance metric gets its own score and timing. You’ll see exactly how long FCP, LCP, TBT, CLS, and Speed Index took, color-coded to show if they’re in the green, orange, or red zone.

Opportunities

This section is gold. Lighthouse identifies specific optimizations that would improve performance, ranked by potential impact. “Eliminate render-blocking resources” might save 2.5 seconds. “Properly size images” could save 1.8 seconds. Each opportunity includes technical details and implementation guidance.

Diagnostics

These are additional insights that don’t directly impact the performance score but highlight areas for improvement things like excessive DOM size, unused JavaScript, or inefficient cache policies.

Passed Audits

Don’t ignore these! They show what you’re doing right, which is valuable for understanding your performance baseline and maintaining good practices.

Accessibility and SEO Insights

Beyond performance, you get actionable feedback on accessibility issues (missing alt text, poor color contrast) and SEO problems (missing meta descriptions, unreadable font sizes on mobile).

The JSON output is equally valuable for programmatic analysis. You can extract specific metrics, track them over time, and build custom dashboards or alerts based on performance trends.

Integrating Google Lighthouse for web

Real-World Impact

Let’s look at practical scenarios where this integration delivers measurable value:

E-Commerce Platform

An online retailer integrated Lighthouse into their Playwright test suite, running audits on product pages and checkout flows. They set a performance budget requiring scores above 90. Within three months, they caught 14 performance regressions before production, including a third-party analytics script blocking rendering.

Result: Maintained consistent page load times, avoiding potential revenue loss.

SaaS Application

A B2B SaaS company added Lighthouse audits to their test suite, focusing on dashboard interfaces. They discovered their data visualization library was causing significant Total Blocking Time. The Lighthouse diagnostics pointed them to specific JavaScript bundles needing code-splitting.

Result: Reduced TBT by 60%, improving perceived responsiveness and reducing support tickets.

Content Publisher

A media company integrated Lighthouse into their deployment pipeline, auditing article pages with strict accessibility and SEO thresholds. This caught issues like missing alt text, poor heading hierarchy, and oversized media files.

Result: Improved SEO rankings, increased organic traffic by 23%, and ensured WCAG compliance.

The Competitive Advantage

Here’s what separates high-performing teams from the rest: they treat performance as a feature, not an afterthought.

By integrating Google Lighthouse with Playwright or any other automation framework, you’re building a culture of performance awareness. Developers get immediate feedback on the performance impact of their changes. Stakeholders get clear, visual reports demonstrating the business value of optimization work.

You shift from reactive firefighting to proactive prevention. Instead of scrambling to fix performance issues after users complain, you prevent them from ever reaching production.

Getting Started

You don’t need to overhaul your entire testing infrastructure. Start small:

  1. Pick one critical user journey maybe your homepage or checkout flow
  2. Add a single Lighthouse audit to your existing test suite
  3. Set a baseline by running the audit and recording current scores
  4. Define one performance budget perhaps a performance score above 80
  5. Integrate it into your CI/CD pipeline so it runs automatically

From there, you can expand add more pages, tighten thresholds, incorporate additional metrics. The key is to start building that performance feedback loop.

Conclusion: Performance as a Continuous Practice

Integrating Google Lighthouse with Playwright; Web performance isn’t a one-time fix. It’s an ongoing commitment that requires visibility, consistency, and automation. Google Lighthouse provides the measurement and insights. Your automation framework provides the execution and integration. Together, they create a powerful system for maintaining and improving web performance at scale.

The teams that win in today’s digital landscape are those that make performance testing as routine as functional testing. They’re the ones catching regressions early, maintaining high standards, and delivering consistently fast experiences to their users.

The question is: will you be one of them?

Would you be ready to boost your web performance? You can start by integrating Google Lighthouse into your automation framework today. Your users and your bottom line will thank you.

Click here to read more blogs like this.

Top 10 tips to debug your Java code with IntelliJ

Top 10 tips to debug your Java code with IntelliJ

In today’s fast-paced development world, debugging can easily become a dreaded task, so here is the complete guide to Debugging Java code in IntelliJ. You write what seems like perfect code, only to watch it fail mysteriously during runtime. Furthermore, maybe a NullPointerException crashes your app at the worst moment, or a complex bug hides in tangled logic, causing hours of frustration. Even with AI-powered coding assistants helping generate boilerplate, the need to understand and troubleshoot your code deeply has never been greater, especially when debugging Java code in IntelliJ.

For example, imagine spending a whole afternoon chasing an elusive bug that breaks customer workflows—only to realize it was a simple off-by-one error or a condition you never tested. This experience is all too real for developers, and mastering your debugging tools can mean the difference between headaches and smooth sailing when debugging Java code in IntelliJ.

That’s where IntelliJ IDEA’s powerful debugger steps in — it lets you pause execution, inspect variables, explore call stacks, and follow exactly what’s going wrong step by step. Whether you’re investigating a tricky edge case or validating AI-generated code, sharpening your IntelliJ debugging skills transforms guesswork into confidence.

This post will guide you through practical, hands-on tips to debug Java effectively with IntelliJ, ultimately turning one of the most daunting parts of development into your secret weapon for quality, speed, and sanity.

Why do we debug code?

When code behaves unexpectedly, running it isn’t enough — you need to inspect what’s happening at runtime. Debugging lets you:

  • Pause execution at a chosen line and then inspect variables.
  • Examine call stacks and then jump into functions.
  • Evaluate expressions on the fly and then change values.
  • Reproduce tricky bugs (race conditions, exceptions, bad input) with minimal trial-and-error.

Additionally, good debugging saves time and reduces guesswork. Moreover, it complements logging and tests: use logs for high-level tracing and debugging Java code in IntelliJ for interactive investigation.

Debugging Java code in IntelliJ

Prerequisites for Debugging Java code in IntelliJ

  • IntelliJ IDEA (Community or Ultimate). Screenshots and shortcuts below assume a modern IntelliJ release.
  • JDK installed (e.g., Java 21 or whichever version your project targets).
  • A runnable Java project in IntelliJ (Maven/Gradle or a simple Java application).

Key debugger features and how to use them

1. Breakpoints

A breakpoint stops program execution at a particular line so you can inspect the state.

  • How to add a breakpoint: Click the gutter (left margin) next to a line number or press the toggle shortcut. The red dot indicates a breakpoint.

Breakpoint variants:

  • Simple breakpoint: pause at a line.
  • Conditional breakpoint: pause only when a boolean condition is true.
    • Right-click a breakpoint → “More” or “Condition”, then enter an expression (e.g., numbers[i] == 40).
  • Log message / Print to console: configure a breakpoint to log text instead of pausing (helpful when you want tracing without stopping).
  • Method breakpoint: pause when a specific method is entered or exited (note: method breakpoints can be slower — use sparingly).
  • Exception breakpoint: pause when a particular exception is thrown (e.g., NullPointerException). Add via Run → View Breakpoints (or Ctrl+Shift+F8) →  Java Exception Breakpoint.

Example (conditional):

for (int i = 0; i < numbers.length; i++) {
    System.out.println("Processing number: " + numbers[i]); // set breakpoint here with condition numbers[i]==40
}

Expected behavior: the debugger pauses only when the evaluated condition is true.

Breakpoint

2. Watchpoints (field watch)

A watchpoint suspends execution when a field is read or written. Use it to track when a shared/static/class-level field changes.

How to set:

  • Right-click a field declaration → “Toggle Watchpoint” (or add in the Debug tool window under Watches).
  • You can add conditions to watchpoints too (e.g., pause only when counter == 5).

Note: watchpoints work at the field level (class members). Local variables are visible in the Variables pane while stopped, but you can’t set a watchpoint on a local variable.

3. Exception breakpoints

If an exception is thrown anywhere, you may want the debugger to stop immediately where it originates.

How to set:

  • Run → View Breakpoints (or Ctrl+Shift+F8) → + → Java Exception Breakpoint → choose exception(s) and whether to suspend on “Thrown” and/or “Uncaught”.

This is invaluable to find the exact place an exception is raised (instead of chasing stack traces).

Here’s an expanded and more practical version of those sections. It keeps your tone consistent and adds real-world examples, common use cases, and small code snippets where helpful.

4. Evaluate Expression & Watches (Practical usage)

While the debugger is paused, these tools help you experiment and verify behavior without editing code.

Evaluate Expression (Alt+F8 / ⌥F8)

You can run expressions in the current stack frame. This is useful for checking logic, testing small fixes, or calling helper methods.

Example:

int discount = priceCalculator.applyDiscount(originalPrice);

While paused, evaluate:

priceCalculator.applyDiscount(500)

This lets you confirm whether the logic works without rerunning the app.
You can also check null-safety:

user.getAddress().getCity()

If this throws a NullPointerException inside Evaluate Expression, you instantly know which object is missing.

Modify runtime values:
You can temporarily change variables to see how the code behaves:

  • i = 10
  • currentUser = new User(“test-user”);

This helps you test alternate paths without restarting.

Watches

Watches let you track variables or expressions as you step.

Examples:

  • Watching a filtered list
items.stream().filter(x -> x.isActive()).count()
  • Watching a computed value:
totalAmount * 1.18
  • Watching nested fields:
order.customer.address.city

Watches update on every step, so you can see how values evolve inside loops or during recursive calls.

Great when debugging loops:
If you add:

  • numbers[i]
  • i

you can quickly spot when a loop behaves incorrectly.

5. Stepping: Step Over / Into / Out / Smart Step Into (Practical usage)

Stepping helps you walk through logic precisely.

Step Over (F8)

Use when you want to execute the current line but don’t need to go inside the method call.

Example:
You trust validateUser() and only care about the next few lines:

  • validateUser(user); // Step Over
  • processPayment(user); 

Step Into (F7)

Use when you need to inspect what’s happening inside a method.

Example:
You’re getting wrong totals:

double finalAmount = billingService.calculateTotal(cart);

Step Into calculateTotal() to check discounts, taxes, and rounding errors.

Smart Step Into (Shift+F7)

Perfect when multiple method calls are on the same line:

String result = helper.clean(input.trim().toLowerCase());

Smart Step Into lets you choose whether to enter trim(), toLowerCase(), or clean().

Step Out (Shift+F8)

If you accidentally stepped too deep into a method, Step Out returns you to the caller quickly.

6. Step Filtering (Practical usage)

When you step into code, IntelliJ might enter library methods you’re not interested in. Step Filtering avoids this.

To skip stepping into certain packages/classes (e.g., java.*, third-party libs): 

Settings → Build, Execution, Deployment → Debugger → Stepping. 

Add classes or package patterns (you can use com.mycompany.* or java.*).

 Apply → OK.

7. HotSwap / Redeploy code during debugging (Practical usage)

HotSwap lets IntelliJ reload small code changes without restarting the app.

What you can change without restart:

  • Method body logic
  • Local variables
  • Simple refactors within a method

Example:
You changed:

  • return price * 0.9;

to:

  • return price * 0.85;

Recompile, click “Reload classes,” and the debugger uses the updated logic immediately.

Great for:

  • Tuning formulas
  • Fixing off-by-one errors
  • Tweaking conditions
  • Adjusting log messages

Not supported:

  • Adding new methods
  • Adding fields
  • Changing class hierarchy

For bigger changes you’ll need a restart.

8. Remote debugging (attach to JVM) (Practical usage)

Remote debugging helps when your app runs in environments like:

  • Docker containers
  • Staging servers
  • Background JVMs
  • Microservices
  • Local processes started by another script

Example JVM arg for a Spring Boot app:

  • -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

You can connect IntelliJ to port 5005 and debug as if the app were local.

Common use case:
Your REST API behaves differently inside Docker.
Attach debugger → Set breakpoints in your service → Reproduce the issue → Inspect environment-specific behavior.

9. Debugging unit tests (Practical usage)

Right-click a test and run in debug mode.
Useful for:

  • Verifying mocks and stubbing
  • Tracking unexpected NPEs inside tests
  • Checking the correctness of assertions
  • Understanding why a particular test is flaky

Example:
Your test fails:

assertEquals(100, service.calculateTotal(cart));

Set a breakpoint inside calculateTotal() and run the test in debug mode.
You instantly see where values diverge.

10. Logs vs Breakpoints: when to use which (Practical usage)

Use both together depending on the situation.

Use logs when:

  • You need a history of events.
  • The issue happens only sometimes.
  • You want long-term telemetry.
  • It’s a production or staging environment.

Use breakpoints when:

  • You need to inspect exact values at runtime
  • You want to experiment with Evaluate Expression
  • You want to track control flow step-by-step

Log Message Breakpoints (super useful)

These let you print useful info without editing code.

Example:
Instead of adding:

System.out.println("i = " + i);

You can configure a breakpoint to log:

"Loop index: " + i

and continue execution without stopping.
This is ideal for debugging loops or repeated method calls without cluttering code.

Debugging Java code in IntelliJ

Example walkthrough (putting the pieces together)

  1. Open DebugExample.java in IntelliJ.
  2. Toggle a breakpoint at System.out.println(“Processing number: ” + numbers[i]);.
  3. Right-click breakpoint → add condition: numbers[i] == 40.
  4. Start debug (Shift+F9). Program runs and pauses when numbers[i] is 40.
  5. Inspect variables in the Variables pane, add a watch for i and for numbers[i].
  6. Use Evaluate Expression to compute numbers[i] * 2 or call helper methods.
  7. If you change a method body and compile, accept HotSwap when IntelliJ prompts to reload classes

Common pitfalls & tips

  • Method/exception breakpoints can be slow if used everywhere — prefer line or conditional breakpoints for hotspots.
  • Conditional expressions should be cheap; expensive conditions slow down program execution during debugging.
  • Watchpoints are only for fields; for locals, use a breakpoint and the Variables pane.
  • HotSwap is limited — don’t rely on it for structural changes.
  • Remote debugging over public networks: Be careful exposing JDWP ports publicly — use SSH tunnels or secure networking.
  • Avoid changing production behavior (don’t connect a debugger to critical production systems without safeguards).

Handy keyboard shortcuts (Windows/Linux | macOS)

  • Toggle breakpoint: Ctrl+F8 | ⌘F8
  • Start debug: Shift+F9 | Shift+F9
  • Resume: F9 | F9
  • Step Over: F8 | F8
  • Step Into: F7 | F7
  • Smart Step Into: Shift+F7 | Shift+F7
  • Evaluate Expression: Alt+F8 | ⌥F8
  • View Breakpoints dialog: Ctrl+Shift+F8 | ⌘⇧F8

(Shortcuts can be mapped differently if you use an alternate Keymap.)

Key Takeaways

  • Debugging is essential because it helps you understand and fix unexpected behavior in your Java code beyond what logging or tests can reveal.
  • IntelliJ IDEA offers powerful debugging tools like breakpoints, conditional breakpoints, watchpoints, and exception breakpoints, which allow you to pause and inspect your code precisely.
  • Use features like Evaluate Expression and Watches to interactively test and verify your code’s logic while paused in the debugger.
  • Stepping through code (Step Over, Step Into, Step Out) helps uncover issues by following program flow in detail.
  • HotSwap allows quick code changes without restarting, therefore speeding up the debugging cycle.
  • Remote debugging lets you troubleshoot apps running in containers, servers, or other environments thereby, enabling seamless investigation.
  • Combine logs and breakpoints strategically depending on the situation, therefore, to maximize insight.
  • Familiarize yourself with keyboard shortcuts and IntelliJ’s debugging settings ultimately, for an efficient workflow.

Conclusion

In fact, IntelliJ’s debugger is powerful — from simple line breakpoints to remote attachment, watches, exception breakpoints, and HotSwap. As a result, practicing these workflows will make you faster at diagnosing issues and understanding complex code paths. Debugging Java code in IntelliJ. Start small: set a couple of targeted conditional breakpoints, step through the logic, use Evaluate Expression, and gradually add more advanced techniques like remote debugging or thread inspection.

Click here to read more blogs like this.

10 Prompting Secrets Every QA Should Know to Get Smarter, Faster, and Better Results

10 Prompting Secrets Every QA Should Know to Get Smarter, Faster, and Better Results

The Testing Skill Nobody Taught You

Here’s a scenario that plays out in QA teams everywhere:

A tester spends 45 minutes manually writing test cases for a new feature. Another tester, working on the same type of feature, finishes in 12 minutes with better coverage, clearer scenarios, and more edge cases identified.

What’s the difference? Experience isn’t the deciding factor, and tools alone don’t explain it either. The real advantage comes from how they communicate with intelligent systems using effective QA Prompting Tips.

The testing world is changing more rapidly than we realise. Today, every QA engineer interacts with AI-powered tools, whether generating test cases, validating user stories, analysing logs, or debugging complex issues. But here’s the uncomfortable truth: most testers miss out on 80% of the value simply because they don’t know how to ask the right questions—especially when applying the right QA Prompting Tips.

That’s where prompting comes in.

Prompting isn’t about typing fancy commands or memorising templates. It’s about asking the right questions, in the right context, at the right time. It’s a skill that multiplies your testing expertise rather than replacing it.

Think of it this way: You wouldn’t write a bug report that just says “Login broken.” You’d provide steps to reproduce, expected vs. actual results, environment details, and severity. The same principle applies to prompting—specificity and structure determine quality, particularly when creating tests with QA Prompting Tips.

In this article, we’ll break down 10 simple yet powerful prompting secrets that can transform your day-to-day testing from reactive to strategic, from time-consuming to efficient, and from good to exceptional.

1. Context Is Everything

QA Prompting Tips

If you ask something vague, you’ll get vague answers. It’s that simple.

Consider these two prompts:

❌ Bad Prompt: “Write test cases for login.”

✅ Good Prompt: “You are a QA engineer for a healthcare application that handles sensitive patient data and must comply with HIPAA regulations. Write 10 test cases for the login module, focusing on data privacy, security vulnerabilities, session management, and multi-factor authentication.”

The difference? Context transforms generic output into actionable testing artifacts.

The first prompt might give you basic username/password validation scenarios. The second gives you security-focused test cases that consider regulatory compliance, session timeout scenarios, MFA edge cases, and data encryption validation, exactly what a healthcare app needs.

Why Context Matters

When you provide real-world details, AI tools can:

  • Align responses with your specific domain (fintech, healthcare, e-commerce)
  • Consider relevant compliance requirements (GDPR, HIPAA, PCI-DSS)
  • Prioritise appropriate risk areas
  • Use industry-specific terminology

Key Takeaway: Always include the “where” and “why” before the “what.” Context makes your prompts intelligent, not just informative, and serves as the foundation for effective QA Prompting Tips.

2. Define the Role Before the Task

QA Prompting Tips

Before you ask for anything, define what the system should think like. This single technique can elevate responses from junior-level to expert-level instantly.

✅ Effective Role Definition: “You are a senior QA engineer with 8 years of experience in exploratory testing and API validation. Review this user story and identify potential edge cases, security vulnerabilities, and performance bottlenecks.”

By assigning a role, you’re setting the expertise level, perspective, and focus area. The response shifts from surface-level observations to nuanced, experience-driven insights.

Role Examples for Different Testing Needs

  • For test case generation: “You are a detail-oriented QA analyst specializing in boundary value analysis…”
  • For bug analysis: “You are a senior test engineer experienced in root cause analysis…”
  • For automation: “You are a test automation architect with expertise in framework design…”
  • For performance: “You are a performance testing specialist, an expert in load testing methodologies and tools.”

Key Takeaway: Assign a role first, then give the task. It fundamentally changes the quality and depth of what you receive.

3. Structure the Output

QA Prompting Tips

QA engineers thrive on structured tables, columns, and clear formats. So ask for it explicitly.

✅ Structured Prompt: “Generate 10 test cases for the password reset feature in a table format with columns for: Test Case ID, Test Scenario, Pre-conditions, Test Steps, Expected Result, Actual Result, and Priority (High/Medium/Low).”

This gives you something that’s immediately copy-ready for Jira, TestRail, Zephyr, SpurQuality, or any test management tool. No reformatting. No cleanup. Just actionable test documentation.

Structure Options

Depending on your need, you can request:

  • Tables for test cases and test data
  • Numbered lists for test execution steps
  • Bullet points for quick scenario summaries
  • JSON/XML for API test data
  • Markdown for documentation
  • Gherkin syntax for BDD scenarios

Key Takeaway: Structured prompts produce structured results. Define the format, and you’ll save hours of manual reformatting.

4. Add Clear Boundaries

QA Prompting Tips

Boundaries create focus and prevent scope creep in your results.

✅ Bounded Prompt: “Generate exactly 8 test cases for the search functionality: 3 positive scenarios, 3 negative scenarios, and 2 edge cases. Focus only on the basic search feature, excluding advanced filters.”

This approach ensures you get:

  • The exact quantity you need (no overwhelming lists)
  • Balanced coverage (positive, negative, edge cases)
  • Focused scope (no feature creep)

Types of Boundaries to Set

  • Quantity: “Generate exactly 5 scenarios”
  • Scope: “Focus only on the checkout process, not the entire cart.”
  • Test types: “Only functional tests, no performance scenarios”
  • Priority: “High and medium priority only”
  • Platforms: “Web application only, exclude mobile”

Key Takeaway: Constraints keep your output precise, relevant, and actionable. They prevent information overload and maintain focus.

5. Build Step by Step (Prompt Chaining)

QA Prompting Tips

Just as QA processes are iterative, effective prompting follows a similar pattern. Instead of asking for everything at once, break it into logical steps.

Example Prompt Chain

Step 1:

“Analyze this user story and summarize the key functional requirements in 3-4 bullet points.”

Step 2:

“Based on those requirements, create 5 high-level test scenarios covering happy path, error handling, and edge cases.”

Step 3:

“Expand the second scenario into detailed test steps with expected results.”

Step 4:

“Identify potential automation candidates from these scenarios and explain why they’re suitable for automation.”

This layered approach produces clear, logical, and well-thought-out results. Each step builds on the previous one, creating a coherent testing strategy rather than disconnected outputs.

Key Takeaway: Prompt chaining mirrors your testing mindset. It’s iterative, logical, and produces higher-quality results than single-shot prompts.

6. Use Prompts for Reviews, Not Just Creation

QA Prompting Tips

Don’t limit AI tools to creation tasks; leverage them as your review partner.

Review Prompt Examples

✅ Test Case Review: “Review these 10 test cases for the payment gateway. Identify any missing scenarios, redundant steps, or unclear expected results.”

✅ Bug Report Quality Check: “Analyze this bug report and suggest improvements to make it clearer for developers. Focus on reproducibility, clarity, and completeness.”

✅ Test Summary Comparison: “Compare these two test execution summary reports and highlight which one communicates results more effectively to stakeholders.”

✅ Documentation Review: “Review this test plan and identify sections that lack clarity or need more detail.”

This transforms your workflow from one-directional (you create, you review) to collaborative (AI assists in both creation and quality assurance).

Key Takeaway: Use AI as your review partner, not just your assistant. It catches what you might miss and improves overall quality.

7. Use Real Scenarios and Data

use real scenarios and data

Generic prompts produce generic results. Feed real test data, actual API responses, or specific scenarios for practical insights.

✅ Real-Data Prompt: “Here’s the actual API response from our login endpoint: {‘status’: 200, ‘token’: null, ‘message’: ‘Success’}. Even though the status is 200 and the message is success, this is causing authentication failures. What could be the root cause, and what test scenarios should I add to catch this in the future?”

This gives you:

  • Specific debugging insights based on actual data
  • Relevant test scenarios tied to real issues
  • Actionable recommendations, not theoretical advice

When to Use Real Data

  • Debugging: Paste actual logs, error messages, or API responses
  • Test data generation: Provide sample data formats
  • Scenario validation: Share actual user workflows
  • Regression analysis: Include historical bug patterns

Key Takeaway: Realistic inputs produce realistic testing insights. The more specific your input, the more valuable your output.

Note: Be cautious about the data you send to the AI model; it might be used for their training purpose. Always prefer a purchased subscription with a data privacy policy.

8. Set the Quality Bar

Quality Bar

If you want a particular tone, standard, or level of professionalism, specify it upfront.

✅ Quality-Defined Prompts:

“Write concise, ISTQB-style test scenarios for the mobile registration flow using standard testing terminology.”

“Generate a bug report following IEEE 829 standards with proper severity classification and detailed reproduction steps.”

“Create BDD scenarios in Gherkin syntax following best practices for Given-When-Then structure.”

This instantly elevates the tone, structure, and professionalism of the output. You’re not getting casual descriptions, you’re getting industry-standard documentation.

Quality Standards to Reference

  • ISTQB for test case terminology
  • IEEE 829 for test documentation
  • Gherkin/BDD for behaviour-driven scenarios
  • ISO 25010 for quality characteristics
  • OWASP for security testing

Key Takeaway: Define the tone and quality standard upfront. It ensures outputs align with professional testing practices.

9. Refine and Iterate

Just like debugging, your first prompt won’t be perfect. And that’s okay.

After getting an initial result, refine it with follow-up prompts:

Initial Prompt: “Generate test cases for user registration.”

Refinement Prompts:

  • ✅ “Add data validation scenarios for email format and password strength.”
  • ✅ “Rank these test cases by priority based on business impact.”
  • ✅ “Include estimated effort for each test case (Small/Medium/Large).”
  • ✅ “Add a column for automation feasibility.”

Each iteration moves you from good to great. You’re sculpting the output to match your exact needs.

Iteration Strategies

  • Add missing elements: “Include security test scenarios”
  • Adjust scope: “Remove low-priority cases and add more edge cases”
  • Change format: “Convert this to Gherkin syntax”
  • Enhance detail: “Expand test steps with more specific actions”

Key Takeaway: Refinement is where you move from good to exceptional. Don’t settle for the first output iteration until it’s exactly what you need.

10. Ask for Prompt Feedback

Here’s a meta-technique: You can ask AI to improve your own prompts.

✅ Meta-Prompt Example: “Here’s the prompt I’m using to generate API test cases: [your prompt]. Analyze it and suggest how to make it more specific, QA-focused, and likely to produce better test scenarios.”

The system will reword, optimize, and enhance your prompt automatically. It’s like having a prompt coach.

What to Ask For

  • “How can I make this prompt more specific?”
  • “What context am I missing that would improve the output?”
  • “Rewrite this prompt to be more structured and clear.”
  • “What role definition would work best for this testing task?”

Key Takeaway: Always review and optimize your own prompts just like you’d review your test cases. Continuous improvement applies to prompting, too.

The QA Prompting Pyramid: A Framework for Mastery

Think of effective prompting as a pyramid. Each level builds on the previous one, creating a foundation for expert-level results.

LevelPrincipleFocusImpact
🧱 BaseContextRelevanceEnsures outputs match your domain and needs
🎭 Level 2Role DefinitionPerspectiveElevates expertise level of responses
📋 Level 3StructureClarityMakes outputs immediately usable
🎯 Level 4ConstraintsPrecisionPrevents scope creep and information overload
🪜 Level 5IterationRefinementTransforms good outputs into exceptional ones
🧠 ApexSelf-ImprovementMasteryContinuously optimizes your prompting skills

Start at the base and work your way up. Master each level before moving to the next. By the time you reach the apex, prompting becomes second nature, a natural extension of your testing expertise.

Real-World Impact: How Prompting Transforms QA Work

Let’s look at practical scenarios where these techniques deliver measurable results:

Test Case Generation

A QA team at a fintech company used structured prompting to generate test cases for a new payment feature. By providing context (PCI-DSS compliance), defining roles (security-focused QA), and setting boundaries (20 test cases covering security, functionality, and edge cases), they reduced test case creation time from 3 hours to 25 minutes while improving coverage by 40%. This type of improvement becomes even more powerful when teams apply effective QA Prompting Tips in their workflows.

Bug Analysis and Root Cause Investigation

A tester struggling with an intermittent bug used real API response data in their prompt, asking for potential root causes and additional test scenarios. Within minutes, they identified a race condition that would have taken hours to debug manually.

Test Automation Strategy

An automation engineer used prompt chaining to develop a framework strategy starting with requirements analysis, moving to tool selection, then architecture design, and finally implementation priorities. The structured approach created a comprehensive automation roadmap in one afternoon.

Documentation Review

A QA lead used review prompts to analyze test plans before stakeholder presentations. The AI identified unclear sections, missing risk assessments, and inconsistent terminology issues that would have surfaced during the actual presentation.

The Competitive Advantage: Why This Matters Now

Here’s the reality: AI won’t replace testers, but testers who know how to prompt will replace those who don’t.

This isn’t about job security, it’s about effectiveness. The QA engineers who master prompting will:

  • Deliver faster without sacrificing quality
  • Think more strategically by offloading routine tasks
  • Catch more issues through comprehensive scenario generation
  • Communicate better with clearer documentation and reports
  • Stay relevant as testing evolves

Prompting is becoming as fundamental to QA as writing test cases or understanding requirements. It’s not a nice-to-have skill; it’s a must-have multiplier.

Getting Started: Your First Steps

You don’t need to master all 10 techniques overnight. Start small and build momentum:

First Week: Foundation

  • Practice adding context to every prompt
  • Define roles before tasks
  • Track the difference in output quality

Second Week: Structure

  • Request structured outputs (tables, lists)
  • Set clear boundaries on scope and quantity
  • Compare structured vs. unstructured results

Third Week: Advanced

  • Try prompt chaining for complex tasks
  • Use prompts for review and feedback
  • Experiment with real data and scenarios

Fourth Week: Mastery

  • Set quality standards in your prompts
  • Iterate and refine outputs
  • Ask for feedback on your own prompts

The key is consistency. Use these techniques daily, even for small tasks. Over time, they become instinctive.

Conclusion: Prompting as a Core QA Skill

Smart prompting is quickly becoming a core competency for QA professionals. It doesn’t replace your testing expertise; it multiplies it, especially when you use the right QA Prompting Tips.

When you apply these 10 techniques, you’ll notice how your test cases become more comprehensive, your bug reports clearer, your scenario planning sharper, and your overall productivity significantly higher. These improvements happen faster when you incorporate effective QA Prompting Tips into your daily workflow.

Remember this simple truth:

“The best testers aren’t those who work harder; they’re those who work smarter by asking better questions.”

So start today. Pick one or two of these techniques and apply them to your next testing task. Notice the difference. Refine your approach. And watch as your testing workflow transforms from reactive to strategic with the help of QA Prompting Tips.

The future of QA isn’t about replacing human intelligence with artificial intelligence. It’s about augmenting human expertise with intelligent tools, and prompting is the bridge between the two.

Your Next Steps

If you found these techniques valuable:

  • Share this article with your QA team and start a conversation about prompting best practices
  • Bookmark this guide and reference it when crafting your next prompt
  • Try one technique today, pick the easiest one, and apply it to your current task
  • Drop a comment below. What’s your go-to prompt that saves you time? What challenges do you face with prompting?
  • Follow for more. We’ll be publishing guides on advanced prompt patterns, AI-driven test automation, and QA productivity hacks

Your prompting journey starts with a single, well-crafted question. Make it count.

Click here to read more blogs like this.

7 Common Software Testing Mistakes and How to Fix Them Using AI

7 Common Software Testing Mistakes and How to Fix Them Using AI

Software testing mistakes to fix using AI — software testing isn’t just about finding bugs — it’s about ensuring that the product delivers value, reliability, and confidence to both the business and the end-users. Yet, even experienced QA engineers and teams fall into common traps that undermine the effectiveness of their testing efforts, which include Software testing mistakes to fix using AI.

If you’ve ever felt like you’re running endless test cycles but still missing critical defects in production, chances are one (or more) of these mistakes is happening in your process. Let’s break down the 7 most common software testing mistakes to fix using AI.

1. Treating Testing as a Last-Minute Activity

Software Testing Mistake - Last Minute Activity

The mistake:

In many organizations, testing still gets pushed to the very end of the development lifecycle. The team develops features for weeks or months, and once deadlines are looming, QA is told to “quickly test everything.” This leaves little time for proper planning, exploratory testing, or regression checks. Rushed testing almost always results in overlooked bugs.

How to avoid it:

  • Adopt a shift-left testing mindset: bring QA into the earliest stages of development. Testers can review requirements, user stories, and wireframes to identify issues before code is written.
  • Integrate testing into each sprint if you’re following Agile. Don’t wait until the release phase — test incrementally.
  • Encourage developers to write unit tests and practice TDD (Test-Driven Development), so defects are caught as early as possible.

Early involvement means fewer surprises at the end and a smoother release process.

Fix this with AI:

AI-powered requirement analysis tools can review user stories and design docs to automatically highlight ambiguities or missing edge cases. Generative AI can also generate preliminary test cases as soon as requirements are written, helping QA get started earlier without waiting for code. Predictive analytics can forecast potential high-risk areas of the codebase so testers prioritize them early in the sprint.

2. Lack of Clear Test Objectives

Software Testing Mistake to fix using AI- Lack of Clear Test Objective

The mistake:

Testing without defined goals is like shooting in the dark. Some teams focus only on “happy path” tests that check whether the basic workflow works, but skip edge cases, negative scenarios, or business-critical paths. Without clarity, QA may spend a lot of time running tests that don’t actually reduce risk.

How to avoid it:

  • Define testing objectives for each cycle: Are you validating performance? Checking for usability? Ensuring compliance.
  • Collaborate with product owners and developers to write clear acceptance criteria for user stories.
  • Maintain a test strategy document that outlines what kinds of tests are required (unit, integration, end-to-end, performance, security).

Having clear objectives ensures testing isn’t just about “checking boxes” but about delivering meaningful coverage that aligns with business priorities.

Fix this with AI:

Use NLP-powered tools to automatically analyze user stories and acceptance criteria, flagging ambiguous or missing requirements. This ensures QA teams can clarify intent before writing test cases, reducing gaps caused by unclear objectives. AI-driven dashboards can also track coverage gaps in real time, so objectives don’t get missed.

3. Over-Reliance on Manual Testing

Software Testing Mistake - Over-Reliance on Manual Testing

The mistake:

Manual testing is valuable, but if it’s the only approach, teams end up wasting effort on repetitive tasks. Regression testing, smoke testing, and large datasets are prone to human error when done manually. Worse, it slows down releases in fast-paced CI/CD pipelines.

How to avoid it:

  • Identify repetitive test cases that can be automated and start small — login flows, form submissions, and critical user journeys.
  • Use frameworks like Selenium, Cypress, Playwright, Appium, or Pytest for automation, depending on your tech stack.
  • Balance automation with manual exploratory testing. Automation gives speed and consistency, while human testers uncover usability issues and unexpected defects.

Think of automation as your assistant, not your replacement. The best testing strategy combines the efficiency of automation with the creativity of manual exploration.

Fix this with AI:

AI-driven test automation tools can generate, maintain, and even self-heal test scripts automatically when the UI changes, reducing maintenance overhead. Machine learning models can prioritize regression test cases based on historical defect data and usage analytics, so you test what truly matters.

4. Poor Test Data and Environment Management

Software Testing Mistake - Poor Test Data

The mistake:

It’s common to hear: “The bug doesn’t happen in staging but appears in production.” This usually happens because test environments don’t mimic production conditions or because test data doesn’t reflect real-world complexity. Incomplete or unrealistic data leads to false confidence in test results.

How to avoid it:

  • Create production-like environments for staging and QA. Use containerization (Docker, Kubernetes) to replicate conditions consistently.
  • Use synthetic but realistic test data that covers edge cases (e.g., very large inputs, special characters, boundary values).
  • Refresh test data regularly, and anonymize sensitive customer data if you use production datasets.

Remember, if your test environment doesn’t reflect reality, your tests won’t either.

Fix this with AI:

AI-driven test data generators can automatically craft rich, production-like datasets that simulate real user behavior and edge cases without exposing sensitive data. Machine learning models can identify missing coverage areas by analyzing historical production incidents and system logs, ensuring your tests anticipate future issues—not just past ones.

5. Ignoring Non-Functional Testing

Software Testing Mistake to fix using AI - Ignoring Non-Functional Testing

The mistake:

Too many teams stop at “the feature works.” But does it scale when thousands of users log in at once? Does it remain secure under malicious attacks? Does it deliver a smooth experience on low network speeds? Ignoring non-functional testing creates systems that “work fine” in a demo but fail in the real world.

How to avoid it:

  • Integrate performance testing into your pipeline using tools like JMeter or Locust to simulate real-world traffic.
  • Run security tests (SQL injection, XSS, broken authentication) regularly — don’t wait for a penetration test once a year. ZAP Proxy passive and active scans can help!
  • Conduct usability testing with actual users or stakeholders to validate that the software isn’t just functional, but intuitive.

A product that functions correctly but performs poorly or feels insecure still damages user trust. Non-functional testing is just as critical as functional testing.

Fix this with AI:

AI can elevate non-functional testing from reactive to predictive. Machine learning models can simulate complex user patterns across diverse devices, geographies, and network conditions—pinpointing performance bottlenecks before they appear in production.

AI-driven security testing tools constantly evolve with new threat intelligence, automatically generating attack scenarios that mirror real-world exploits such as injection attacks, authentication bypasses, and API abuse.

For usability, AI-powered analytics and vision models can evaluate screen flows, identify confusing layouts, and detect design elements that slow user interaction. Instead of waiting for manual feedback cycles, development teams get continuous, data-backed insights to refine performance, security, and experience in tandem.

6. Inadequate Test Coverage and Documentation

Software Testing Mistake to fix using AI - Inadequate Test Coverage

The mistake:

Incomplete or outdated test cases often lead to critical gaps. Some QA teams also skip documentation to “save time,” but this creates chaos later — new team members don’t know what’s been tested, bugs get repeated, and regression cycles lose effectiveness.

How to avoid it:

  • Track test coverage using tools that measure which parts of the codebase are covered by automated tests.
  • Keep documentation lightweight but structured: test charters, bug reports, acceptance criteria, and coverage reports. Avoid bloated test case repositories that nobody reads.
  • Treat documentation as a living artifact. Update it continuously, not just during release crunches.

Good documentation doesn’t have to be lengthy — it has to be useful and easy to maintain.

Fix this with AI:

AI can transform documentation and coverage management from a manual chore into a continuous, intelligent process. By analyzing code commits, test execution results, and requirements, AI tools can automatically generate and update test documentation, keeping it synchronized with the evolving product.

Machine learning models can assess coverage depth, correlate it with defect history, and flag untested or high-risk code paths before they cause production issues. AI-powered assistants can also turn static documentation into dynamic knowledge engines, allowing testers to query test cases, trace feature impacts, or uncover reusable scripts instantly.

This ensures documentation stays accurate, context-aware, and actionable — supporting faster onboarding and more confident releases.

7. Not Learning from Production Defects

Software Testing Mistake - Not Learning from Production Defects

The mistake:

Bugs escaping into production are inevitable. But the bigger mistake is when teams only fix the bug and move on, without analyzing why it slipped through. This leads to the same categories of defects reappearing release after release.

How to avoid it:

  • Run root cause analysis for every critical production defect. Was it a missed requirement? An incomplete test case? An environment mismatch?
  • Use post-mortems not to blame but to improve processes. For example, if login bugs frequently slip through, strengthen test coverage around authentication.
  • Feed learnings back into test suites, automation, and requirements reviews. developers to write unit tests and practice TDD (Test-Driven Development), so defects are caught as early as possible.

Great QA teams don’t just find bugs — they learn from them, so they don’t happen again.

Fix this with AI:

AI can turn every production defect into a learning opportunity for continuous improvement. By analyzing production logs, telemetry, and historical bug data, AI systems can uncover hidden correlations—such as which modules, code changes, or dependencies are most prone to introducing similar defects.
Predictive analytics models can forecast which areas of the application are most at risk in upcoming releases, guiding QA teams to focus their regression tests strategically. AI-powered Root Cause Analysis tools can automatically cluster related issues, trace them to their originating commits, and even propose preventive test cases or test data refinements to avoid repeating past mistakes.

Instead of reacting to production failures, AI helps teams proactively strengthen their QA process with data-driven intelligence and faster feedback loops.

Conclusion: Building a Smarter QA Practice with AI

Software testing is not just a phase in development — it’s a mindset. It requires curiosity, discipline, and continuous improvement. Avoiding these seven mistakes can transform your QA practice from a bottleneck into a true enabler of quality and speed.

Software testing mistakes to fix using AI. Here’s the truth: quality doesn’t happen by accident. It’s the result of planning, collaboration, and constant refinement. By involving QA early, setting clear objectives, balancing manual and automated testing, managing data effectively, and learning from past mistakes, your team can deliver not just working software, but software that delights users and stands the test of time.

AI takes this one step further — with predictive analytics to catch risks earlier, self-healing test automation that adapts to change, intelligent test data generation, and AI-powered RCA (Root Cause Analysis) that learns from production. Instead of chasing bugs, QA teams can focus on engineering intelligent, resilient, and user-centric quality.

Strong QA isn’t about finding more bugs — it’s about building more confidence. And with AI, that confidence scales with every release.

Click here to read more blogs like this.

Building a Complete API Automation Testing Framework with Java, Rest Assured, Cucumber, and Playwright 

Building a Complete API Automation Testing Framework with Java, Rest Assured, Cucumber, and Playwright 

API Automation Testing Framework – In Today’s fast-paced digital ecosystem, almost every modern application relies on APIs (Application Programming Interfaces) to function seamlessly. Whether it’s a social media integration pulling live updates, a payment gateway processing transaction, or a data service exchanging real-time information, APIs act as the invisible backbone that connects various systems together. 

Because APIs serve as the foundation of all interconnected software, ensuring that they are reliable, secure, and high performing is absolutely critical. Even a minor API failure can impact multiple dependent systems; consequently, it may cause application downtime, data mismatches, or even financial loss.

That’s where API automation testing framework comes in. Unlike traditional UI testing, API testing validates the core business logic directly at the backend layer, which makes it faster, more stable, and capable of detecting issues early in the development cycle — even before the frontend is ready. 

In this blog, we’ll walk through the process of building a complete API Automation Testing Framework using a combination of: 

  • Java – as the main programming language 
  • Maven – for project and dependency management 
  • Cucumber – to implement Behavior Driven Development (BDD) 
  • RestAssured – for simplifying RESTful API automation 
  • Playwright – to handle browser-based token generation 

The framework you’ll learn to build will follow a BDD (Behavior-Driven Development) approach, enabling test scenarios to be written in simple, human-readable language. This not only improves collaboration between developers, testers, and business analysts but also makes test cases easier to understand, maintain, and extend

Additionally, the API automation testing framework will be CI/CD-friendly, meaning it can be seamlessly integrated into automated build pipelines for continuous testing and faster feedback. 

By the end of this guide, you’ll have a scalable, reusable, and maintainable API testing framework that brings together the best of automation, reporting, and real-time token management — a complete solution for modern QA teams. 

What is API?

An API (Application Programming Interface) acts as a communication bridge between two software systems, allowing them to exchange information in a standardized way. In simpler terms, it defines how different software components should interact — through a set of rules, protocols, and endpoints

Think of an API as a messenger that takes a request from one system, delivers it to another system, and then brings back the response. This interaction, therefore, allows applications to share data and functionality without exposing their internal logic or database structure.

Let’s take a simple example: 
When you open a weather application on your phone, it doesn’t store weather data itself. Instead, it sends a request to a weather server API, which processes the request and sends back a response — such as the current temperature, humidity, or forecast. 
This request-response cycle is what makes APIs so powerful and integral to almost every digital experience we use today. 

Most modern APIs follow the REST (Representational State Transfer) architectural style. REST APIs use the HTTP protocol and are designed around a set of standardized operations, including: 

HTTP MethodDescriptionExample Use
GETRetrieve data from the serverFetch a list of users
POSTCreate new data on the serverAdd a new product
PUTUpdate existing dataedit user details
DELETERemove dataDelete a record

The responses returned by API’s are typically in JSON (JavaScript Object Notation) format – a lightweight, human-readable, and machine-friendly data format that’s easy to parse and validate.

In essence, API’s are the digital glue that holds modern applications together — enabling smooth communication, faster integrations, and a consistent flow of information across systems. 

What is API Testing?

API Testing is the process of verifying that an API functions correctly and performs as expected — ensuring that all its endpoints, parameters, and data exchanges behave according to defined business rules. 

In simple terms, it’s about checking whether the backend logic of an application works properly — without needing a graphical user interface (UI). Since APIs act as the communication layer between different software components, testing them helps ensure that the entire system remains reliable, secure, and efficient. 

API testing typically focuses on four main aspects: 

  1. Functionality – Does the API perform the intended operation and return the correct response for valid requests? 
  2. Reliability – Does it deliver consistent results every time, even under different inputs and conditions? 
  3. Security – Is the API protected from unauthorized access, data leaks, or token misuse? 
  4. Performance – Does it respond quickly and remain stable under heavy load or high traffic? 

Unlike traditional UI testing, which validates the visual and interactive parts of an application, API testing operates directly at the business logic layer. This makes it: 

  • Faster – Since it bypasses the UI, execution times are much shorter. 
  • More Stable – UI changes (like a button name or layout) don’t affect API tests. 
  • Proactive – Tests can be created and run even before the front-end is developed. 

In essence, API testing ensures the heart of your application is healthy. By validating responses, performance, and security at the API level, teams can detect defects early, reduce costs, and deliver more reliable software to users. 

Why is API Testing Important?

API Testing plays a vital role in modern software development because APIs form the backbone of most applications. A failure in an API can affect multiple systems and impact overall functionality. 

Here’s why API testing is important: 

  1. Ensures Functionality: Verifies that endpoints return correct responses and handle errors properly. 
  2. Enhances Security: Detects vulnerabilities like unauthorized access or token misuse. 
  3. Validates Data Integrity: Confirms that data remains consistent across APIs and databases. 
  4. Improves Performance: Checks response time, stability, and behavior under load. 
  5. Detects Defects Early: Allows early testing right after backend development, saving time and cost
  6. Supports Continuous Integration: Easily integrates with CI/CD pipelines for automated validation. 

In short, API testing ensures your system’s core logic is reliable, secure, and ready for real-world use. 

Tools for Manual API Testing

Before jumping into automation, it’s essential to explore and understand APIs manually. Manual testing helps you validate endpoints, check responses, and get familiar with request structures. 

Here are some popular tools used for manual API testing: 

  • Postman: The most widely used tool for sending API requests, validating responses, and organizing test collections [refer link – https://www.postman.com/.
  • SoapUI: Best suited for testing both SOAP and REST APIs with advanced features like assertions and mock services. 
  • Insomnia: A lightweight and user-friendly alternative to Postman, ideal for quick API exploration. 
  • cURL: A command-line tool perfect for making fast API calls or testing from scripts. 
  • Fiddler: Excellent for capturing and debugging HTTP/HTTPS traffic between client and server. 

Using these tools helps testers understand API behavior, request/response formats, and possible edge cases — forming a strong foundation before moving to API automation

Tools for API Automation Testing 

After verifying APIs manually, the next step is to automate them using reliable tools and libraries. Automation helps improve test coverage, consistency, and execution speed. 

Here are some popular tools used for API automation testing: 

  • RestAssured: A powerful Java library designed specifically for testing and validating RESTful APIs. 
  • Cucumber: Enables writing test cases in Gherkin syntax (plain English), making them easy to read and maintain. 
  • Playwright: Automates browser interactions; in our framework, it will be used for token generation or authentication flows. 
  • Postman + Newman: Allows you to run Postman collections directly from the command line — ideal for CI/CD integration. 
  • JMeter: A robust tool for performance and load testing of APIs under different conditions. 

In this blog, our focus will be on building a framework using RestAssured, Cucumber, and Playwright — combining functional, BDD, and authentication automation into one cohesive setup. 

Framework Overview 

We’ll build a Behavior-Driven API Automation Testing Framework that combines multiple tools for a complete testing solution. Here’s how each component fits in: 

  • Cucumber – Manages the BDD layer, allowing test scenarios to be written in simple, readable feature files
  • RestAssured – Handles HTTP requests and responses for validating RESTful APIs. 
  • Playwright – Automates browser-based actions like token generation or authentication. 
  • Maven – Manages project dependencies, builds, and plugins efficiently. 
  • Cucumber HTML Reports – Automatically generates detailed execution reports after each run. 

The framework follows a modular structure, with separate packages for step definitions, utilities, configurations, and feature files — ensuring clean organization, easy maintenance, and scalability. 

Step 1: Prerequisites

Before starting, ensure you have: 

Add the required dependencies to your pom.xml file: 

<?xml version="1.0" encoding="UTF-8"?> 
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 
    <modelVersion>4.0.0</modelVersion> 
 
    <groupId>org.Spurqlabs</groupId> 
    <artifactId>SpurQLabs-Test-Automation</artifactId> 
    <version>1.0-SNAPSHOT</version> 
    <properties> 
        <maven.compiler.source>11</maven.compiler.source> 
        <maven.compiler.target>11</maven.compiler.target> 
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
    </properties> 
    <dependencies> 
        <!-- Playwright for UI automation --> 
        <dependency> 
            <groupId>com.microsoft.playwright</groupId> 
            <artifactId>playwright</artifactId> 
            <version>1.50.0</version> 
        </dependency> 
        <!-- Cucumber for BDD --> 
        <dependency> 
            <groupId>io.cucumber</groupId> 
            <artifactId>cucumber-java</artifactId> 
            <version>7.23.0</version> 
        </dependency> 
        <dependency> 
            <groupId>io.cucumber</groupId> 
            <artifactId>cucumber-testng</artifactId> 
            <version>7.23.0</version> 
        </dependency> 
        <!-- TestNG for test execution --> 
        <dependency> 
            <groupId>org.testng</groupId> 
            <artifactId>testng</artifactId> 
            <version>7.11.0</version> 
            <scope>test</scope> 
        </dependency> 
        <!-- Rest-Assured for API testing --> 
        <dependency> 
            <groupId>io.rest-assured</groupId> 
            <artifactId>rest-assured</artifactId> 
            <version>5.5.5</version> 
        </dependency> 
        <!-- Apache POI for Excel support --> 
        <dependency> 
            <groupId>org.apache.poi</groupId> 
            <artifactId>poi-ooxml</artifactId> 
            <version>5.4.1</version> 
        </dependency> 
        <!-- org.json for JSON parsing --> 
        <dependency> 
            <groupId>org.json</groupId> 
            <artifactId>json</artifactId> 
            <version>20250517</version> 
        </dependency> 
        <dependency> 
            <groupId>org.seleniumhq.selenium</groupId> 
            <artifactId>selenium-devtools-v130</artifactId> 
            <version>4.26.0</version> 
            <scope>test</scope> 
        </dependency> 
        <dependency> 
            <groupId>com.sun.mail</groupId> 
            <artifactId>jakarta.mail</artifactId> 
            <version>2.0.1</version> 
        </dependency> 
        <dependency> 
            <groupId>com.sun.activation</groupId> 
            <artifactId>jakarta.activation</artifactId> 
            <version>2.0.1</version> 
        </dependency> 
    </dependencies> 
    <build> 
        <plugins> 
            <plugin> 
                <groupId>org.apache.maven.plugins</groupId> 
                <artifactId>maven-compiler-plugin</artifactId> 
                <version>3.14.0</version> 
                <configuration> 
                    <source>11</source> 
                    <target>11</target> 
                </configuration> 
            </plugin> 
        </plugins> 
    </build> 
</project> 

Step 2: Creating Project

Create a Maven project with the following folder structure:

loanbook-api-automation 

│ 
├── .idea 
│ 
├── src 
│   └── test 
│       └── java 
│           └── org 
│               └── Spurlabs 
│                   ├── Core 
│                   │   ├── Hooks.java 
│                   │   ├── Main.java 
│                   │   ├── TestContext.java 
│                   │   └── TestRunner.java 
│                   │ 
│                   ├── Steps 
│                   │   └── CommonSteps.java 
│                   │ 
│                   └── Utils 
│                       ├── APIUtility.java 
│                       ├── FrameworkConfigReader.java 
│                       └── TokenManager.java 
│ 
├── resources 
│   ├── Features 
│   ├── headers 
│   ├── Query_Parameters 
│   ├── Request_Bodies 
│   ├── Schema 
│   └── cucumber.properties 
│ 
├── target 
│ 
├── test-output 
│ 
├── .gitignore 
├── bitbucket-pipelines.yml 
├── DealDetails.json 
├── FrameworkConfig.json 
├── pom.xml 
├── README.md 
└── token.json 

Step 3: Creating a Feature File

In this, we will be creating a feature file for API Automation Testing Framework. A feature file consists of steps. These steps are mentioned in the gherkin language. The feature is easy to understand and can be written in the English language so that a non-technical person can understand the flow of the test scenario. In this framework we will be automating the four basic API request methods i.e. POST, PUT, GET and DELETE. 
 
We can assign tags to our scenarios mentioned in the feature file to run particular test scenarios based on the requirement. The key point you must notice here is the feature file should end with .feature extension. We will be creating four different scenarios for the four different API methods.  

Feature: All Notes API Validation 
 
  @api 
 
  Scenario Outline: Validate POST Create Notes API Response for "<scenarioName>" Scenario 
    When User sends "<method>" request to "<url>" with headers "<headers>" and query file "<queryFile>" and requestDataFile  "<bodyFile>" 
    Then User verifies the response status code is <statusCode> 
    And User verifies the response body matches JSON schema "<schemaFile>" 
    Then User verifies fields in response: "<contentType>" with content type "<fields>" 
    Examples: 
      | scenarioName       | method | url                                                             | headers | queryFile | bodyFile             | statusCode | schemaFile | contentType | fields | 
      | Valid create Notes | POST   | /api/v1/loan-syndications/{dealId}/investors/{investorId}/notes | NA      | NA        | Create_Notes_Request | 200        | NA         | NA          | NA     | 
 
  Scenario Outline: Validate GET Notes API Response for "<scenarioName>" Scenario 
    When User sends "<method>" request to "<url>" with headers "<headers>" and query file "<queryFile>" and requestDataFile "<bodyFile>" 
    Then User verifies the response status code is <statusCode> 
    And User verifies the response body matches JSON schema "<schemaFile>" 
    Then User verifies fields in response: "<contentType>" with content type "<fields>" 
    Examples: 
      | scenarioName    | method | url                                                             | headers | queryFile | bodyFile | statusCode | schemaFile       | contentType | fields              | 
      | Valid Get Notes | GET    | /api/v1/loan-syndications/{dealId}/investors/{investorId}/notes | NA      | NA        | NA       | 200        | Notes_Schema_200 | json        | note=This is Note 1 | 
 
  Scenario Outline: Validate Update Notes API Response for "<scenarioName>" Scenario 
    When User sends "<method>" request to "<url>" with headers "<headers>" and query file "<queryFile>" and requestDataFile "<bodyFile>" 
    Then User verifies the response status code is <statusCode> 
    And User verifies the response body matches JSON schema "<schemaFile>" 
    Then User verifies fields in response: "<contentType>" with content type "<fields>" 
    Examples: 
      | scenarioName       | method | url                                                                                   | headers | queryFile | bodyFile             | statusCode | schemaFile | contentType | fields | 
      | Valid update Notes | PUT    | /api/v1/loan-syndications/{dealId}/investors/{investorId}/notes/{noteId}/update-notes | NA      | NA        | Update_Notes_Request | 200        | NA         | NA          | NA     | 
 
  Scenario Outline: Validate DELETE Create Notes API Response for "<scenarioName>" Scenario 
    When User sends "<method>" request to "<url>" with headers "<headers>" and query file "<queryFile>" and requestDataFile "<bodyFile>" 
    Then User verifies the response status code is <statusCode> 
    And User verifies the response body matches JSON schema "<schemaFile>" 
    Then User verifies fields in response: "<contentType>" with content type "<fields>" 
    Examples: 
      | scenarioName | method | url                                                                      | headers | queryFile | bodyFile | statusCode | schemaFile | contentType | fields | 
      | Valid delete | DELETE | /api/v1/loan-syndications/{dealId}/investors/{investorId}/notes/{noteId} | NA      | NA        | NA       | 200        | NA         | NA          | NA     | 

Step 4: Creating a Step Definition File

Unlike the automation framework which we have built in the previous blog, we will be creating a single-step file for all the feature files. In the BDD framework, the step files are used to map and implement the steps described in the feature file. Rest Assured library is very accurate to map the steps with the steps described in the feature file. We will be describing the same steps in the step file as they have described in the feature file so that behave will come to know the step implementation for the particular steps present in the feature file.  

package org.Spurqlabs.Steps; 
 
import io.cucumber.java.en.Then; 
import io.cucumber.java.en.When; 
import io.restassured.response.Response; 
import org.Spurqlabs.Core.TestContext; 
import org.Spurqlabs.Utils.*; 
import org.json.JSONArray; 
import org.json.JSONObject; 
 
import java.io.File; 
import java.io.IOException; 
import java.nio.charset.StandardCharsets; 
import java.nio.file.Files; 
import java.nio.file.Paths; 
import java.util.HashMap; 
import java.util.Map; 
 
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 
import static org.Spurqlabs.Utils.DealDetailsManager.replacePlaceholders; 
import static org.hamcrest.Matchers.equalTo; 
public class CommonSteps extends TestContext { 
    private Response response; 
 
    @When("User sends {string} request to {string} with headers {string} and query file {string} and requestDataFile {string}") 
    public void user_sends_request_to_with_query_file_and_requestDataFile (String method, String url, String headers, String queryFile, String bodyFile) throws IOException { 
        String jsonString = Files.readString(Paths.get(FrameworkConfigReader.getFrameworkConfig("DealDetails")), StandardCharsets.UTF_8); 
        JSONObject storedValues = new JSONObject(jsonString); 
 
        String fullUrl = FrameworkConfigReader.getFrameworkConfig("BaseUrl") + replacePlaceholders(url); 
 
        Map<String, String> header = new HashMap<>(); 
        if (!"NA".equalsIgnoreCase(headers)) { 
            header = JsonFileReader.getHeadersFromJson(FrameworkConfigReader.getFrameworkConfig("headers") + headers + ".json"); 
        } else { 
            header.put("cookie", TokenManager.getToken()); 
        } 
        Map<String, String> queryParams = new HashMap<>(); 
        if (!"NA".equalsIgnoreCase(queryFile)) { 
            queryParams = JsonFileReader.getQueryParamsFromJson(FrameworkConfigReader.getFrameworkConfig("Query_Parameters") + queryFile + ".json"); 
            for (String key : queryParams.keySet()) { 
                String value = queryParams.get(key); 
                for (String storedKey : storedValues.keySet()) { 
                    value = value.replace("{" + storedKey + "}", storedValues.getString(storedKey)); 
                } 
                queryParams.put(key, value); 
            } 
        } 
 
        Object requestBody = null; 
        if (!"NA".equalsIgnoreCase(bodyFile)) { 
            String bodyTemplate = JsonFileReader.getJsonAsString( 
                    FrameworkConfigReader.getFrameworkConfig("Request_Bodies") + bodyFile + ".json"); 
 
            for (String key : storedValues.keySet()) { 
                String placeholder = "{" + key + "}"; 
                if (bodyTemplate.contains(placeholder)) { 
                    bodyTemplate = bodyTemplate.replace(placeholder, storedValues.getString(key)); 
                } 
            } 
 
            requestBody = bodyTemplate; 
        } 

        response = APIUtility.sendRequest(method, fullUrl, header, queryParams, requestBody); 
        response.prettyPrint(); 
        TestContextLogger.scenarioLog("API", "Request sent: " + method + " " + fullUrl); 
 
        if (scenarioName.contains("GET Notes") && response.getStatusCode() == 200) { 
            DealDetailsManager.put("noteId", response.path("[0].id")); 
        } 
         
    } 
 
    @Then("User verifies the response status code is {int}") 
    public void userVerifiesTheResponseStatusCodeIsStatusCode(int statusCode) { 
        response.then().statusCode(statusCode); 
        TestContextLogger.scenarioLog("API", "Response status code: " + statusCode); 
    } 
 
    @Then("User verifies the response body matches JSON schema {string}") 
    public void userVerifiesTheResponseBodyMatchesJSONSchema(String schemaFile) { 
        if (!"NA".equalsIgnoreCase(schemaFile)) { 
            String schemaPath = "Schema/" + schemaFile + ".json"; 
            response.then().assertThat().body(matchesJsonSchemaInClasspath(schemaPath)); 
            TestContextLogger.scenarioLog("API", "Response body matches schema"); 
        } else { 
            TestContextLogger.scenarioLog("API", "Response body does not have schema to validate"); 
        } 
    } 
 
    @Then("User verifies field {string} has value {string}") 
    public void userVerifiesFieldHasValue(String jsonPath, String expectedValue) { 
        response.then().body(jsonPath, equalTo(expectedValue)); 
        TestContextLogger.scenarioLog("API", "Field " + jsonPath + " has value: " + expectedValue); 
    } 
 
    @Then("User verifies fields in response: {string} with content type {string}") 
    public void userVerifiesFieldsInResponseWithContentType(String contentType, String fields) throws IOException { 
        // If NA, skip verification 
        if ("NA".equalsIgnoreCase(contentType) || "NA".equalsIgnoreCase(fields)) { 
            return; 
        } 
        String responseStr = response.getBody().asString().trim(); 
 
        try { 
            if ("text".equalsIgnoreCase(contentType)) { 
                // For text, verify each expected value is present in response 
                for (String expected : fields.split(";")) { 
                    expected = replacePlaceholders(expected.trim()); 
                    if (!responseStr.contains(expected)) { 
                        throw new AssertionError("Expected text not found: " + expected); 
                    } 
                    TestContextLogger.scenarioLog("API", "Text found: " + expected); 
                } 
            } else if ("json".equalsIgnoreCase(contentType)) { 
                // For json, verify key=value pairs 
                JSONObject jsonResponse; 
                if (responseStr.startsWith("[")) { 
                    JSONArray arr = new JSONArray(responseStr); 
                    jsonResponse = !arr.isEmpty() ? arr.getJSONObject(0) : new JSONObject(); 
                } else { 
                    jsonResponse = new JSONObject(responseStr); 
                } 
                for (String pair : fields.split(";")) { 
                    if (pair.trim().isEmpty()) continue; 
                    String[] kv = pair.split("=", 2); 
                    if (kv.length < 2) continue; 
                    String keyPath = kv[0].trim(); 
                    String expected = replacePlaceholders(kv[1].trim()); 
                    Object actual = JsonFileReader.getJsonValueByPath(jsonResponse, keyPath); 
                    if (actual == null) { 
                        throw new AssertionError("Key not found in JSON: " + keyPath); 
                    } 
                    if (!String.valueOf(actual).equals(String.valueOf(expected))) { 
                        throw new AssertionError("Mismatch for " + keyPath + ": expected '" + expected + "', got '" + actual + "'"); 
                    } 
                    TestContextLogger.scenarioLog("API", "Validated: " + keyPath + " = " + expected); 
                } 
            } else { 
                throw new AssertionError("Unsupported content type: " + contentType); 
            } 
        } catch (AssertionError | Exception e) { 
            TestContextLogger.scenarioLog("API", "Validation failed: " + e.getMessage()); 
            throw e; 
        } 
    } 

Step 5: Creating API

Till now we have successfully created a feature file and a step file now in this step we will be creating a utility file. Generally, in Web automation, we have page files that contain the locators and the actions to perform on the web elements but in this framework, we will be creating a single utility file just like the step file. The utility file contains the API methods and the endpoints to perform the specific action like, POST, PUT, GET, or DELETE. The request body i.e. payload and the response body will be captured using the methods present in the utility file. So the reason these methods are created in the utility file is that we can use them multiple times and don’t have to create the same method over and over again.

package org.Spurqlabs.Utils; 
 
import io.restassured.RestAssured; 
import io.restassured.http.ContentType; 
import io.restassured.response.Response; 
import io.restassured.specification.RequestSpecification; 
 
import java.io.File; 
import java.util.Map; 
 
public class APIUtility { 
    public static Response sendRequest(String method, String url, Map<String, String> headers, Map<String, String> queryParams, Object body) { 
        RequestSpecification request = RestAssured.given(); 
        if (headers != null && !headers.isEmpty()) { 
            request.headers(headers); 
        } 
        if (queryParams != null && !queryParams.isEmpty()) { 
            request.queryParams(queryParams); 
        } 
        if (body != null && !method.equalsIgnoreCase("GET")) { 
            if (headers == null || !headers.containsKey("Content-Type")) { 
                request.header("Content-Type", "application/json"); 
            } 
            request.body(body); 
        } 
        switch (method.trim().toUpperCase()) { 
            case "GET": 
                return request.get(url); 
            case "POST": 
                return request.post(url); 
            case "PUT": 
                return request.put(url); 
            case "PATCH": 
                return request.patch(url); 
            case "DELETE": 
                return request.delete(url); 
            default: 
                throw new IllegalArgumentException("Unsupported HTTP method: " + method); 
        } 
    } 

Step 6: Create a Token Generation using Playwright

In this step, we automate the process of generating authentication tokens using Playwright. Many APIs require login-based tokens (like cookies or bearer tokens), and managing them manually can be difficult — especially when they expire frequently. 

The TokenManager class handles this by: 

  • Logging into the application automatically using Playwright. 
  • Extracting authentication cookies (OauthHMAC, OauthExpires, BearerToken). 
  • Storing the token in a local JSON file for reuse. 
  • Refreshing the token automatically when it expires. 

This ensures that your API tests always use a valid token without manual updates, making the framework fully automated and CI/CD ready. 

package org.Spurqlabs.Utils; 
 
import java.io.*; 
import java.nio.file.*; 
import java.time.Instant; 
import java.util.HashMap; 
import java.util.Map; 
import com.google.gson.Gson; 
import com.google.gson.reflect.TypeToken; 
import com.microsoft.playwright.*; 
import com.microsoft.playwright.options.Cookie; 
 
public class TokenManager { 
    private static final ThreadLocal<String> tokenThreadLocal = new ThreadLocal<>(); 
    private static final ThreadLocal<Long> expiryThreadLocal = new ThreadLocal<>(); 
    private static final String TOKEN_FILE = "token.json"; 
    private static final long TOKEN_VALIDITY_SECONDS = 30 * 60; // 30 minutes 
 
    public static String getToken() { 
        String token = tokenThreadLocal.get(); 
        Long expiry = expiryThreadLocal.get(); 
        if (token == null || expiry == null || Instant.now().getEpochSecond() >= expiry) { 
            // Try to read from a file (for multi-JVM/CI) 
            Map<String, Object> fileToken = readTokenFromFile(); 
            if (fileToken != null) { 
                token = (String) fileToken.get("token"); 
                expiry = ((Number) fileToken.get("expiry")).longValue(); 
            } 
            // If still null or expired, fetch new 
            if (token == null || expiry == null || Instant.now().getEpochSecond() >= expiry) { 
                Map<String, Object> newToken = generateAuthTokenViaBrowser(); 
                token = (String) newToken.get("token"); 
                expiry = (Long) newToken.get("expiry"); 
                writeTokenToFile(token, expiry); 
            } 
            tokenThreadLocal.set(token); 
            expiryThreadLocal.set(expiry); 
        } 
        return token; 
    } 
 
    private static Map<String, Object> generateAuthTokenViaBrowser() { 
        String bearerToken; 
        long expiry = Instant.now().getEpochSecond() + TOKEN_VALIDITY_SECONDS; 
        int maxRetries = 2; 
        int attempt = 0; 
        Exception lastException = null; 
        while (attempt < maxRetries) { 
            try (Playwright playwright = Playwright.create()) { 
                Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true)); 
                BrowserContext context = browser.newContext(); 
                Page page = context.newPage(); 
 
                // Robust wait for login page to load 
                page.navigate(FrameworkConfigReader.getFrameworkConfig("BaseUrl"), new Page.NavigateOptions().setTimeout(60000)); 
                page.waitForSelector("#email", new Page.WaitForSelectorOptions().setTimeout(20000)); 
                page.waitForSelector("#password", new Page.WaitForSelectorOptions().setTimeout(20000)); 
                page.waitForSelector("button[type='submit']", new Page.WaitForSelectorOptions().setTimeout(20000)); 
 
                // Fill a login form 
                page.fill("#email", FrameworkConfigReader.getFrameworkConfig("UserEmail")); 
                page.fill("#password", FrameworkConfigReader.getFrameworkConfig("UserPassword")); 
                page.waitForSelector("button[type='submit']:not([disabled])", new Page.WaitForSelectorOptions().setTimeout(10000)); 
                page.click("button[type='submit']"); 
 
                // Wait for either dashboard element or flexible URL match 
                boolean loggedIn; 
                try { 
                    page.waitForSelector(".dashboard, .main-content, .navbar, .sidebar", new Page.WaitForSelectorOptions().setTimeout(20000)); 
                    loggedIn = true; 
                } catch (Exception e) { 
                    // fallback to URL check 
                    try { 
                        page.waitForURL(url -> url.startsWith(FrameworkConfigReader.getFrameworkConfig("BaseUrl")), new Page.WaitForURLOptions().setTimeout(30000)); 
                        loggedIn = true; 
                    } catch (Exception ex) { 
                        // Both checks failed 
                        loggedIn = false; 
                    } 
                } 
                if (!loggedIn) { 
                    throw new RuntimeException("Login did not complete successfully: dashboard element or expected URL not found"); 
                } 
 
                // Extract cookies 
                String oauthHMAC = null; 
                String oauthExpires = null; 
                String token = null; 
                for (Cookie cookie : context.cookies()) { 
                    switch (cookie.name) { 
                        case "OauthHMAC": 
                            oauthHMAC = cookie.name + "=" + cookie.value; 
                            break; 
                        case "OauthExpires": 
                            oauthExpires = cookie.name + "=" + cookie.value; 
                            if (cookie.expires != null && cookie.expires > 0) { 
                                expiry = cookie.expires.longValue(); 
                            } 
                            break; 
                        case "BearerToken": 
                            token = cookie.name + "=" + cookie.value; 
                            break; 
                    } 
                } 
                if (oauthHMAC != null && oauthExpires != null && token != null) { 
                    bearerToken = oauthHMAC + ";" + oauthExpires + ";" + token + ";"; 
                } else { 
                    throw new RuntimeException("❗ One or more cookies are missing: OauthHMAC, OauthExpires, BearerToken"); 
                } 
                browser.close(); 
                Map<String, Object> map = new HashMap<>(); 
                map.put("token", bearerToken); 
                map.put("expiry", expiry); 
                return map; 
            } catch (Exception e) { 
                lastException = e; 
                System.err.println("[TokenManager] Login attempt " + (attempt + 1) + " failed: " + e.getMessage()); 
                attempt++; 
                try { Thread.sleep(2000); } catch (InterruptedException ignored) {} 
            } 
        } 
        throw new RuntimeException("Failed to generate auth token after " + maxRetries + " attempts", lastException); 
    } 
 
    private static void writeTokenToFile(String token, long expiry) { 
        try { 
            Map<String, Object> map = new HashMap<>(); 
            map.put("token", token); 
            map.put("expiry", expiry); 
            String json = new Gson().toJson(map); 
            Files.write(Paths.get(TOKEN_FILE), json.getBytes()); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    } 
 
    private static Map<String, Object> readTokenFromFile() { 
        try { 
            Path path = Paths.get(TOKEN_FILE); 
            if (!Files.exists(path)) return null; 
            String json = new String(Files.readAllBytes(path)); 
            return new Gson().fromJson(json, new TypeToken<Map<String, Object>>() {}.getType()); 
        } catch (IOException e) { 
            return null; 
        } 
    } 
} 

Step 7: Create Framework Config File

A good tester is one who knows the use and importance of config files. In this framework, we are also going to use the config file. Here, we are just going to put the base URL in this config file and will be using the same in the utility file over and over again. The config file contains more data than just of base URL when you start exploring the framework and start automating the new endpoints then at some point, you will realize that some data can be added to the config file.  

Additionally, the purpose of the config files is to make tests more maintainable and reusable. Another benefit of a config file is that it makes the code more modular and easier to understand as all the configuration settings are stored in a separate file and it makes it easier to update the configuration settings for all the tests at once.  

{ 
  "BaseUrl": "https://app.sample.com", 
  "UserEmail": "************.com", 
  "UserPassword": "#############", 
  "ExecutionBrowser": "chromium", 
  "Resources": "/src/test/resources/", 
  "Query_Parameters": "src/test/resources/Query_Parameters/", 
  "Request_Bodies": "src/test/resources/Request_Bodies/", 
  "Schema": "src/test/resources/Schema/", 
  "TestResultsDir": "test-output/", 
  "headers": "src/test/resources/headers/", 
  "DealDetails": "DealDetails.json", 
  "UploadDocUrl": "/api/v1/documents" 
} 

Step 8: Execute and Generate Cucumber Report

At this stage, we create the TestRunner class, which serves as the entry point to execute all Cucumber feature files. It uses TestNG as the test executor and integrates Cucumber for running BDD-style test scenarios. 

The @CucumberOptions annotation defines: 

  • features → Location of all .feature files. 
  • glue → Packages containing step definitions and hooks. 
  • plugin → Reporting options like JSON and HTML reports. 

After execution, Cucumber automatically generates: 

  • Cucumber.json → For CI/CD and detailed reporting. 
  • Cucumber.html → A user-friendly HTML report showing test results. 

This setup makes it easy to run all API tests and view clean, structured reports for quick analysis. 

package org.Spurqlabs.Core; 
import io.cucumber.testng.AbstractTestNGCucumberTests; 
import io.cucumber.testng.CucumberOptions; 
import org.testng.annotations.AfterSuite; 
import org.testng.annotations.BeforeSuite; 
import org.testng.annotations.DataProvider; 
import org.Spurqlabs.Utils.CustomHtmlReport; 
import org.Spurqlabs.Utils.ScenarioResultCollector; 
 
@CucumberOptions( 
        features = {"src/test/resources/Features"}, 
        glue = {"org.Spurqlabs.Steps", "org.Spurqlabs.Core"}, 
        plugin = {"pretty", "json:test-output/Cucumber.json","html:test-output/Cucumber.html"} 
) 
 
public class TestRunner {} 

Running your test

Once the framework is set up, you can execute your API automation suite directly from the command line using Maven. Maven handles compiling, running tests, and generating reports automatically. 

Run All Tests – 

To run all Cucumber feature files: 

mvn clean test 
  • clean → Deletes old compiled files and previous reports for a fresh run. 
  • test → Executes all test scenarios defined in your project. 

After running this command, Maven will trigger the Cucumber TestRunner, execute all scenarios, and generate reports in the test-output folder. 

Run Tests by Tag – 

Tags allow you to selectively run specific test scenarios or features. 
You can add tags like @api1, @smoke, or @regression in your .feature files to categorize tests. 

Example: 

@api1 
Scenario: Verify POST API creates a record successfully 
  Given User sends "POST" request to "/api/v1/create" ... 
  Then User verifies the response status code is 201 

To execute only scenarios with a specific tag, use: 

mvn clean test -Dcucumber.filter.tags="@api1" 
  • The framework will run only those tests that have the tag @api1. 
  • You can combine tags for more flexibility: 
  • @api1 or @api2 → Runs tests with either tag. 
  • @smoke and not @wip → Runs smoke tests excluding work-in-progress scenarios. 

This is especially useful when running specific test groups in CI/CD pipelines. 

View Test Reports 

API Automation Testing Framerwork Report – After the execution, Cucumber generates detailed reports automatically in the test-output directory: 

  • Cucumber.html → User-friendly HTML report showing scenario results and logs. 
  • Cucumber.json → JSON format report for CI/CD integrations or analytics tools. 

You can open the report in your browser: 

project-root/test-output/Cucumber.html 
 

This section gives testers a clear understanding of how to: 

  • Run all or specific tests using tags, 
  • Filter executions during CI/CD, and 
  • Locate and view the generated reports. 
API Automation Testing Framework Report

Reference Framework GitHub Link – https://github.com/spurqlabs/APIAutomation_RestAssured_Cucumber_Playwright

Conclusion

API automation testing framework ensures that backend services are functioning properly before the application reaches the end user. 
Therefore, by integrating Cucumber, RestAssured, and Playwright, we have built a flexible and maintainable test framework that: 

  • Supports BDD style scenarios. 
  • Handles token-based authentication automatically. 
  • Provides reusable utilities for API calls. 
  • Generates rich HTML reports for easy analysis. 

This hybrid setup helps QA engineers achieve faster feedback, maintain cleaner code, and enhance the overall quality of the software.