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. Struktur yang terlalu datar akan menyulitkan skalabilitas. Berikut adalah struktur folder standar yang lebih matang dan terpusat:

my-playwright-framework/
├── .github/
│   └── workflows/
│       └── playwright.yml      # Konfigurasi Pipeline CI/CD
├── resources/                  # Direktori Pusat Resource Uji
│   ├── pages/                  # 2. Page Class (Logic & Aksi Halaman)
│   ├── selectors/              # 1. Selector / Locator Repository
│   └── test-data/              # Data Uji Statis (JSON, CSV)
├── tests/
│   └── login.spec.ts           # 3. Test Script (Eksekusi & Assertion)
├── utils/
│   └── dataHelper.ts           # 4. Utilities / Helpers (Fungsi Reusable)
├── Dockerfile                  # Konfigurasi Containerization
├── 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.

// resources/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.

// resources/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 "../resources/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.
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 membuang waktu dan memori untuk memproses *locator* tombol tersebut.
4. Playwright Fixtures: Cara "Native" Playwright (Tingkat Lanjut)

Meskipun contoh di atas menggunakan new LoginPage(page), best practice sejati di Playwright adalah menggunakan Fixtures (Dependency Injection). Ini menghindari instansiasi *class* berulang kali di setiap file *test*.

// utils/testFixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../resources/pages/LoginPage';

type MyFixtures = {
  loginPage: LoginPage;
};

export const test = base.extend<MyFixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

// Di file test Anda, cukup panggil fixture-nya:
// test('my test', async ({ loginPage }) => { ... })
5. 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).

6. Integrasi CI/CD & Pelaporan (Reporting) Real-time

Otomatisasi tidak lengkap jika hanya berjalan di lokal. Arsitektur yang matang harus siap dijalankan di pipeline CI/CD (seperti GitHub Actions) di dalam environment Docker untuk menjamin konsistensi OS. Selain itu, hasil pengujian (pass/fail/flaky) sebaiknya dihubungkan ke bot *messaging* (Slack atau MS Teams) menggunakan Playwright Reporters agar seluruh tim mendapatkan *feedback* secara *real-time*.

Kesimpulan

Membangun otomasi bukan sekadar membuat skrip agar "bisa jalan". Dengan memusatkan sumber daya di direktori resources/, menggunakan Getter (Lazy Evaluation) untuk performa dinamis, mengimplementasikan Fixtures untuk injeksi dependensi, serta mendelegasikan logika ke Helper, Anda menciptakan arsitektur kelas enterprise yang scalable, terintegrasi sempurna dengan pipeline CI/CD, dan sangat mudah dirawat oleh tim QA dalam jangka panjang.