DEV Community

kadir
kadir

Posted on

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

Two kinds of constant have been creeping into our tests: inline data objects (the
{ title, description, body, tagList } we keep typing) and URLs. Both deserve 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

A factory is a function that builds a valid object with sensible defaults, and lets
the caller override just the parts it cares about. It centralises "what a valid article
looks like" so no test has to remember every field:

// 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> = {}): 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 (Chapter 13)
    ...overrides,                // anything the caller passed wins
  };
}
Enter fullscreen mode Exit fullscreen mode

Two TypeScript bits worth naming:

  • Partial<ArticleInput> means "an object that may have some of the article fields" — so a caller can override only title, or nothing at all.
  • ...overrides is the spread operator: it copies the caller's fields on top of the defaults, so their values win. articleData({ tagList: ["x"] }) keeps every default but replaces tagList.

Our provisioning helper now just defers to the factory:

// src/utils/scenarios.ts
export async function createArticle(api, overrides: Partial<ArticleInput> = {}) {
  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, the valid defaults, and the "tagList is required" rule all live in
one place. Change the article shape once and every test follows.

Why put it in src/fixtures-data/ (the @data alias) and not in a fixture? Because
this is pure data — no page, no setup/teardown. Factories are plain functions;
fixtures that use them own the lifecycle. Keeping them separate is the layering
discipline from Chapter 10.

A typed environment module

URLs are the other scattered constant. env is our single source of truth, and now
it's multi-environment. First, a definition:

Environment variables (process.env.*) are values set outside your code — by
your shell or your CI system — like TEST_ENV=staging. They let you change
behaviour without editing files.

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

const ENVIRONMENTS: Record<EnvName, { webURL: string; apiURL: string }> = {
  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";  // pick a target, default local
const base = ENVIRONMENTS[name] ?? ENVIRONMENTS.local;

export const env = {
  name,
  webURL: process.env.WEB_URL ?? base.webURL,   // allow per-URL overrides
  apiURL: process.env.API_URL ?? base.apiURL,
} as const;
Enter fullscreen mode Exit fullscreen mode

Now the same suite runs anywhere, by changing only an environment variable:

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

The discipline that keeps this clean: only env.ts reads process.env. Tests,
Page Objects, and fixtures import env — never environment variables directly. All
configuration lives in one auditable place (the Chapter 10 layer rule, applied to
config).

Part 4, done

The integration milestone is complete: log in 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 targets 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.

Top comments (0)