Membangun test framework: custom helpers, shared utilities, monorepo patterns, dan design patterns untuk framework yang scalable

Setelah di episode 23 kita menguji AI/LLM apps, pada episode ini kita membangun test framework — arsitektur yang membuat test suite maintainable, reusable, dan scalable.
test-framework/
├── fixtures/
│ ├── auth.fixture.ts
│ ├── database.fixture.ts
│ └── api.fixture.ts
├── pages/
│ ├── BasePage.ts
│ ├── LoginPage.ts
│ └── DashboardPage.ts
├── helpers/
│ ├── assertion.helper.ts
│ ├── api.helper.ts
│ └── data.helper.ts
├── utils/
│ ├── logger.ts
│ ├── reporter.ts
│ └── config.ts
├── tests/
│ ├── login.spec.ts
│ └── dashboard.spec.ts
├── playwright.config.ts
└── package.json// utils/config.ts
export const config = {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
apiURL: process.env.API_URL || 'http://localhost:3001',
testUser: {
email: process.env.TEST_USER_EMAIL,
password: process.env.TEST_USER_PASSWORD,
},
timeouts: {
short: 5000,
medium: 15000,
long: 30000,
},
};// utils/logger.ts
export class Logger {
static info(message: string) {
console.log(`[INFO] ${new Date().toISOString()}: ${message}`);
}
static error(message: string, error?: Error) {
console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);
if (error) console.error(error);
}
static step(step: number, message: string) {
console.log(`[STEP ${step}] ${message}`);
}
}// helpers/api.helper.ts
export class ApiHelper {
constructor(private request: APIRequestContext) {}
async login(email: string, password: string) {
const response = await this.request.post('/api/auth/login', {
data: { email, password },
});
return response.json();
}
async getProduct(id: number) {
const response = await this.request.get(`/api/products/${id}`);
return response.json();
}
}// fixtures/test.fixture.ts
import { test as base } from '@playwright/test';
import { ApiHelper } from '../helpers/api.helper';
type TestFixtures = {
apiHelper: ApiHelper;
authenticatedPage: any;
};
export const test = base.extend<TestFixtures>({
apiHelper: async ({ request }, use) => {
const apiHelper = new ApiHelper(request);
await use(apiHelper);
},
authenticatedPage: async ({ page, apiHelper }, use) => {
const { token } = await apiHelper.login(
process.env.TEST_USER_EMAIL,
process.env.TEST_USER_PASSWORD
);
await page.goto('/');
await page.evaluate(t => localStorage.setItem('token', t), token);
await use(page);
},
});
export { expect } from '@playwright/test';// packages/test-utils/src/index.ts
export { ApiHelper } from './helpers/api.helper';
export { Logger } from './utils/logger';
export { config } from './utils/config';{
"name": "monorepo",
"workspaces": ["packages/*", "apps/*"]
}Tip
Framework harus modular — setiap bagian bisa di-import secara individual. Jangan buat monolithic framework yang sulit dipelihara.
# 1. Create structure
mkdir -p test-framework/{fixtures,pages,helpers,utils,tests}
# 2. Create base files
touch test-framework/utils/config.ts
touch test-framework/utils/logger.ts
touch test-framework/helpers/api.helper.ts
touch test-framework/fixtures/test.fixture.ts
# 3. Update playwright.config.ts
# Import custom fixturesDi episode 25 selanjutnya kita akan membahas shift-left & quality engineering — test di development loop, contract testing, dan quality engineering practices. Sampai jumpa di episode 25!