Test Data Factories & Environment Config (Playwright + TypeScript, Ch.17)

Published: (June 8, 2026 at 01:15 PM EDT)
3 min read
Source: Dev.to

Source: Dev.to

Two kinds of constants have been creeping into our tests: inline data objects

({ title, description, body, tagList }) and URLs. Both want a single home.

This chapter gives them one — a data factory and a typed environment module — and

closes Part 4.

Code for this chapter is tagged ch-17 in the repo:

https://github.com/aktibaba/playwright-qa-course — see

src/fixtures-data/article.ts and src/utils/env.ts.

A data factory

Every test that makes an article was spelling out the same fields. A factory

centralizes “what a valid article looks like,” bakes in uniqueness, and lets a test

override only the part it’s testing:

// src/fixtures-data/article.ts
export interface ArticleInput {
  title: string;
  description: string;
  body: string;
  tagList: string[];
}

let seq = 0;

export function articleData(overrides: Partial = {}): ArticleInput {
  seq += 1;
  return {
    title: `Test Article ${Date.now()}-${seq}`,
    description: "Generated by the article factory",
    body: "Article body for automated tests.",
    tagList: [],                 // required by the API (Ch.13)
    ...overrides,
  };
}
Enter fullscreen mode


Exit fullscreen mode

Our provisioning util now just defers to it:

// src/utils/scenarios.ts
export async function createArticle(api, overrides: Partial = {}) {
  const res = await api.post("articles", { data: { article: articleData(overrides) } });
  // ...
}
Enter fullscreen mode


Exit fullscreen mode

So a test stays focused on intent — makeArticle({ tagList: ["integration"] }) — and

the unique title, valid defaults, and the tagList-is-required rule all live in one

place. Change the article shape once, and every test follows.

Why src/fixtures-data/ (the @data alias) and not a fixture? Because this is

pure data — no page, no lifecycle. Factories are plain functions; the

fixtures that use them own setup and teardown. Keeping them separate is the same

layer discipline from Chapter 10.

A typed environment module

URLs are the other scattered constant. env is the single source of truth, and now

it’s multi-environment: choose a target with TEST_ENV, override individual URLs

with WEB_URL / API_URL:

// src/utils/env.ts
export type EnvName = "local" | "ci" | "staging";

const ENVIRONMENTS: Record = {
  local:   { webURL: "http://localhost:3000", apiURL: "http://localhost:3001/api" },
  ci:      { webURL: "http://localhost:3000", apiURL: "http://localhost:3001/api" },
  staging: { webURL: "https://inkwell-staging.example.com", apiURL: "https://inkwell-staging.example.com/api" },
};

const name = (process.env.TEST_ENV as EnvName) || "local";
const base = ENVIRONMENTS[name] ?? ENVIRONMENTS.local;

export const env = {
  name,
  webURL: process.env.WEB_URL ?? base.webURL,
  apiURL: process.env.API_URL ?? base.apiURL,
} as const;
Enter fullscreen mode


Exit fullscreen mode

Now the same suite runs anywhere:

npm test                       # local (default)
TEST_ENV=staging npm test      # against the staging deployment
API_URL=http://host:4000/api npm test   # one-off override
Enter fullscreen mode


Exit fullscreen mode

The key discipline: only env.ts reads process.env. Tests, Page Objects, and

fixtures import env — never environment variables directly. That keeps

configuration in one auditable place (and is the layer rule from Chapter 10 applied

to config).

Part 4, done

The integration milestone is complete: auth once with storageState, seed via the

API and verify in the UI, and now clean factories and environment config. The suite

is fast, isolated, and portable across environments.

Next up — Part 5: Scaling, Config & CI

Chapter 18 — Multi-environment configuration takes the env module we just built

and wires it into Playwright’s project system, so a single config can target several

environments with the right base URLs, retries, and metadata. Tag: ch-18.

Following along? Star the repo

and tell me what your test-data factories generate most.

0 views
Back to Blog

Related posts

Read more »