Dokumentasi Catatan Teknik

Wawasan, tutorial, dan catatan mendalam seputar QA automation dan rekayasa perangkat lunak.

QA Automation Terakhir diperbarui: 14 Agustus 2026

Membangun Arsitektur E2E Test Automation dari Nol dengan Playwright & POM


Dalam dunia pengujian perangkat lunak modern, memiliki kerangka kerja (framework) otomasi yang handal, mudah dipelihara (maintainable), dan terstruktur adalah kunci sukses sebuah produk digital. Playwright telah menjadi salah satu standar industri terdepan untuk E2E testing.

1. Best Practice Struktur Direktori (Folder Structure)

Untuk proyek skala menengah hingga enterprise, pemisahan tanggung jawab (separation of concerns) sangat krusial. Berikut adalah struktur folder standar yang lebih matang:

my-playwright-framework/
├── tests/
│   └── login.spec.ts           # 3. Test Script (Eksekusi & Assertion)
├── pages/
│   └── LoginPage.ts            # 2. Page Class (Logic & Aksi Halaman)
├── selectors/
│   └── loginSelectors.ts       # 1. Selector / Locator Repository
├── utils/
│   └── dataHelper.ts           # 4. Utilities / Helpers (Fungsi Reusable)
├── playwright.config.ts        # Konfigurasi Global Playwright
└── package.json
2. Anatomi 3 Pilar POM (Selector, Page, & Test)

Penerapan Page Object Model yang ideal membagi komponen menjadi tiga lapisan yang jelas agar kode mudah dirawat:

A. Selector / Locator Layer

Menyimpan semua elemen DOM atau *selectors* di satu tempat terpusat. Jika UI berubah, Anda cukup mengubahnya di sini.

// selectors/loginSelectors.ts
export const LoginSelectors = {
  usernameInput: "#username",
  passwordInput: "#password",
  loginButton: 'button[type="submit"]',
  errorMessage: ".alert-danger"
};
B. Page Class Layer (Implementasi Getter)

Di sinilah Getter memegang peran penting. Menggunakan getter (`get`) untuk membungkus locator mengamankan elemen (enkapsulasi) dan membuat pemanggilan kode menjadi jauh lebih bersih.

// pages/LoginPage.ts
import { Page } from "@playwright/test";
import { LoginSelectors } from "../selectors/loginSelectors";

export class LoginPage {
  private page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  // Penggunaan Getter (Best Practice Enkapsulasi)
  get usernameInput() { return this.page.locator(LoginSelectors.usernameInput); }
  get passwordInput() { return this.page.locator(LoginSelectors.passwordInput); }
  get loginButton() { return this.page.locator(LoginSelectors.loginButton); }
  get errorMessage() { return this.page.locator(LoginSelectors.errorMessage); }

  async navigate() {
    await this.page.goto("/login");
  }

  async login(user: string, pass: string) {
    await this.usernameInput.fill(user);
    await this.passwordInput.fill(pass);
    await this.loginButton.click();
  }
}
C. Test Script Layer (Execution & Assertion)

Berkat penggunaan Getter di atas, kita dapat melakukan assertion (validasi ekspektasi) langsung ke elemen halaman dengan sintaks yang sangat natural dan elegan.

// tests/login.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";

test("Gagal login memunculkan pesan error", async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.navigate();
  await loginPage.login("wrong_user", "wrong_password");

  // Assertion memanfaatkan Getter (Sangat Clean!)
  await expect(loginPage.errorMessage).toBeVisible();
  await expect(loginPage.errorMessage).toHaveText("Invalid credentials");
});
3. Mengapa Menggunakan Getter Menjadi Best Practice Utama?

Dalam JavaScript/TypeScript modern, Getter bukan hanya sekadar sintaks gula (syntactic sugar), melainkan strategi arsitektur untuk menerapkan Lazy Evaluation dan Encapsulation. Mari kita lihat perbandingannya secara mendalam:

❌ Tanpa Getter (Deklarasi Properti Biasa) ✅ Dengan Getter (Best Practice)
this.btn = page.locator('.btn');
Kekurangan:
  • Stale Element Risk: Elemen dievaluasi dan "dikunci" sejak awal instansiasi *class*. Jika UI me-render ulang (DOM berubah) sebelum elemen di-klik, script akan gagal (*StaleElementReferenceException*).
  • Boros Memori: Semua *locator* di dalam *class* langsung dicari oleh Playwright saat *class* dipanggil, bahkan jika elemen itu tidak pernah dipakai dalam skenario tes tersebut.
  • Sintaks pemanggilan tidak bisa se-fleksibel getter.
get btn() { return page.locator('.btn'); }
Kelebihan:
  • Lazy Evaluation (Pencarian Dinamis): Elemen TIDAK dicari saat *class* diinstansiasi. Elemen baru dicari secara *real-time* di DOM tepat saat baris kode tersebut dipanggil/dieksekusi. Ini 100% menghilangkan isu *Stale Element*.
  • Hemat Memori: Jika sebuah *test case* tidak menyentuh tombol tertentu, Playwright tidak akan pernah membuang waktu dan memori untuk memproses *locator* tombol tersebut.
  • Sintaks Elegan: Memungkinkan penulisan Chaining dan Assertion yang sangat natural seperti membaca bahasa Inggris (contoh: expect(page.btn).toBeVisible()).
4. Memisahkan Logika dengan Helpers (DRY Principle)

Seringkali saat membuat *test automation*, kita butuh membuat email acak (random), memformat tanggal, atau logika kalkulasi lainnya. Jangan pernah menaruh fungsi ini di dalam Page Class! Page Class harus 100% fokus pada interaksi UI.

Gunakan fungsi independen di folder /utils atau /helpers. Mengapa? Agar fungsi tersebut bisa dipakai ulang (reusable) di file mana pun tanpa mengulangi penulisan kode (Don't Repeat Yourself).

// utils/dataHelper.ts
export function generateRandomEmail(): string {
  const timestamp = new Date().getTime();
  return `qa_tester_${timestamp}@example.com`;
}
Kesimpulan

Membangun otomasi bukan sekadar membuat skrip agar "bisa jalan". Dengan memisahkan Selector, menggunakan Getter (Lazy Evaluation) untuk performa dinamis yang tahan banting terhadap perubahan DOM, serta mendelegasikan logika generator data ke fungsi Helper, Anda menciptakan arsitektur kelas enterprise yang scalable, minim *flaky test*, dan sangat mudah dirawat oleh tim dalam jangka panjang.