Two MoTacon attendees are on the left. The MoTaacon logo is in the center, and to the right a prompt to Get Your Ticket.

Data-Driven Testing Patterns to Keep Your Suite Clean at Scale

06 Aug 2026

In this moment: Komal chowdhary
 
Once we classified our Test data types, the next question was:

How should tests consume this data?

This is where data-driven testing patterns helped.

The data type tells us what kind of data we are dealing with.

The pattern tells us how to use it in tests.

Pattern 1: Named Datasets
 
A named dataset is a complete data object for a specific scenario.
 
// src/data/orders.data.ts
export const orderIndia = {
  product: 'Blue Denim Jacket',
  countryCode: 'ind',
  country: 'India',
};

export const orderAustralia = {
  product: 'Blue Denim Jacket',
  countryCode: 'aus',
  country: 'Australia',
};
 
Usage:

const ctx = buildCtx(app, users.maya, orderIndia);
await appFlow.loginFlow.execute(ctx);
await appFlow.placeOrderFlow.execute(ctx);
await appFlow.verifyOrderFlow.execute(ctx);
 
What problem does this solve?
 
It gives business meaning to your test data.
Instead of random values, you now have reusable scenarios.

Pattern 2: Array Iteration / Parameterized Tests
 

Use this when the same test needs to run with multiple inputs. 

export const multiProductOrders = [
  {
    product: 'Blue Denim Jacket',
    countryCode: 'ind',
    country: 'India',
    testName: 'denim jacket order',
  },
  {
    product: 'Road Runner Shoes',
    countryCode: 'ind',
    country: 'India',
    testName: 'shoes order',
  },
  {
    product: 'Wave Sound Headphones',
    countryCode: 'ind',
    country: 'India',
    testName: 'headphones order',
  },
];
 
Usage:

for (const orderData of multiProductOrders) {
  test(`place order for ${orderData.testName}`, async ({ app, appFlow }) => {
    const ctx = buildCtx(app, users.maya, orderData);
    await appFlow.loginFlow.execute(ctx);
    await appFlow.placeOrderFlow.execute(ctx);
    await appFlow.verifyOrderFlow.execute(ctx);
  });
}
 
What problem does this solve?
 
You avoid writing the same test again and again.
One flow multiple dataset. You get clean test results.

Pattern 3: Separate Describe Blocks for Different Workflows
 

Parameterized tests are useful when the workflow is the same. 

But what if the workflow itself is different. 

Example: 

  • Full checkout flow
  • Cart-only flow
  • Wishlist flow

test.describe('Full checkout - India', () => {
  test('place and verify order', async ({ app, appFlow }) => {
    const ctx = buildCtx(app, users.maya, orderIndia);
    await appFlow.loginFlow.execute(ctx);
    await appFlow.placeOrderFlow.execute(ctx);
    await appFlow.verifyOrderFlow.execute(ctx);
  });
});

test.describe('Cart only - Headphones', () => {
  test('add product to cart and verify', async ({ app }) => {
    await app.loginPage.validLogin(
      users.maya.username,
      users.maya.password
    );
    await app.dashboardPage.searchProductAddCart(
      products.wirelessHeadphones
    );
    await app.cartPage.verifyProductIsDisplayed(
      products.wirelessHeadphones
    );
  });
});
 
What problem does this solve?
 
It keeps different workflows clean. 
You need not force everything into one overloaded test flow.  

Pattern 4: Spread + Override
 

Use this when a scenario is mostly the same, but one or two values are different. 

export const orderDefault = {
  product: 'Blue Denim Jacket',
  countryCode: 'ind',
  country: 'India',
};

export const headphoneOrder = {
  ...orderDefault,
  product: 'Wave Sound Headphones',
};
 
Usage:
 
const ctx = buildCtx(app, users.maya, headphoneOrder);
await appFlow.loginFlow.execute(ctx);
await appFlow.placeOrderFlow.execute(ctx);
await appFlow.verifyOrderFlow.execute(ctx);
 
What problem does this solve?
 
You avoid duplicating large datasets.
 
Only the changed fields are mentioned.
 
Everything else comes from the base dataset.
 
Pattern 5: Type Constraints 

Type constraints prevent invalid values before the test even runs. 

export type ProductOption = 'Blue Denim Jacket' | 'Road Runner Shoes' | Headphones';
export type CountryCode = 'ind' | 'aus' | 'us' | 'uk';

export interface OrderConfig {
  product: ProductOption;
  countryCode: CountryCode;
  country: string;
}
 
Now this gives a TypeScript error:
 
const invalidOrder: OrderConfig = {
  product: 'Blue Denim Jaket',
  countryCode: 'xyz',
};
 
What problem does this solve?

Typos are caught in the editor. You do not discover them after you have executed your CI pipeline.

Pattern 6: Builder Pattern for Complex Test Data
 
Sometimes a test data object becomes too large.
 
Example:
 
const customer = {
  firstName: 'Maya',
  lastName: 'Sharma',
  country: 'India',
  plan: 'Premium',
  hasSavedAddress: true,
  hasSavedCard: true,
};
 
When many tests need variations of this object, a builder can help.

type Plan = 'Basic' | 'Premium' | 'Enterprise';

interface CustomerData {
  firstName: string;
  lastName: string;
  country: string;
  plan: Plan;
  hasSavedAddress: boolean;
  hasSavedCard: boolean;
}

export class CustomerBuilder {
  private customer: CustomerData = {
    firstName: 'Test',
    lastName: 'User',
    country: 'India',
    plan: 'Basic',
    hasSavedAddress: false,
    hasSavedCard: false,
  };

  withName(firstName: string, lastName: string) {
    this.customer.firstName = firstName;
    this.customer.lastName = lastName;
    return this;
  }

  withPlan(plan: Plan) {
    this.customer.plan = plan;
    return this;
  }

  withSavedAddress() {
    this.customer.hasSavedAddress = true;
    return this;
  }

  withSavedCard() {
    this.customer.hasSavedCard = true;
    return this;
  }
}

build(): CustomerData {
  return this.customer;
}
}
 
Usage:

const premiumCustomer = new CustomerBuilder()
  .withName('Maya', 'Sharma')
  .withPlan('Premium')
  .withSavedAddress()
  .withSavedCard()
  .build();
 
What problem does this solve?

It makes complex test data easier to read and reuse.

Instead of copying large objects everywhere, tests describe only the important differences.


Pattern 7: Contract-Based Test Data
 
Type constraints protect individual fields.
 
Contract-based data protects the full shape of the dataset.

type CustomerTestData = {
  email: string;
  country: 'India' | 'Australia' | 'United States';
  plan: 'Basic' | 'Premium' | 'Enterprise';
  hasSavedCard: boolean;
};
 
Now the dataset must satisfy this structure:

const premiumCustomer = {
  email: 'maya.customer@example.com',
  country: 'India',
  plan: 'Premium',
  hasSavedCard: true,
} satisfies CustomerTestData;
 
If a required field is missing:

const invalidCustomer = {
  email: 'maya.customer@example.com',
  country: 'India',
  plan: 'Premium',
} satisfies CustomerTestData;
 
TypeScript will complain because hasSavedCard is missing.
 
What problem does this solve?
 
This is useful when multiple people contribute test data.
It prevents incomplete or wrongly shaped datasets from entering the framework.
 
 This piece is a companion to an article on the  types of test data, which classifies the data itself. The type tells you what a piece of data is; the pattern tells you how a test should use it. 
 
Komal chowdhary profile image
Komal chowdhary
QA Specialist

I blend strategy, creativity, and a growth mindset, while engaging with the testing community

Open To
Write
Speak
Attending MoTaCon 🤝
Sign in to comment
Explore MoT
Influence, from the other side of the table image
What I learned about influence by becoming a stakeholder
MoT Software Testing Essentials Certificate image
Boost your career in software testing with the MoT Software Testing Essentials Certificate. Learn essential skills, from basic testing techniques to advanced risk analysis, crafted by industry experts.
Into The Motaverse image
Into the MoTaverse is a podcast by Ministry of Testing, hosted by Rosie Sherry, exploring the people, insights, and systems shaping quality in modern software teams.
Subscribe to our newsletter