Test Runner
Struktur test, fixture dan hook, pola AAA, data-driven, tag, dan cara menjalankannya.
Test runner menyiapkan setiap test (setup atau fixture), menjalankannya, membereskannya (teardown), lalu memberi laporan lulus atau gagal. Kamu cukup fokus ke isi test-nya.
Test runner mirip test management tool (TestRail, Zephyr, atau spreadsheet test case). Ia tahu daftar test case, menyiapkan precondition, menjalankan semuanya, lalu menulis status passed atau failed untuk setiap baris.
Dua alat, satu konsep
| pytest + pytest-playwright | Playwright Test | |
|---|---|---|
| Nama file test | test_*.py | *.spec.ts |
| Bentuk test | fungsi def test_…(page): | test('judul', async ({ page }) => { … }) |
| Persiapan bersama | fixture (@pytest.fixture) | hook (beforeEach) dan fixture (test.extend) |
| Mengelompokkan test | per file atau class | test.describe() |
| Satu test, banyak data | @pytest.mark.parametrize | perulangan for |
| Label atau tag | @pytest.mark.smoke | { tag: '@smoke' } |
| Tempat pengaturan | pyproject.toml + perintah terminal | playwright.config.ts |
Fixture bawaan yang tinggal dipakai
Tulis namanya sebagai parameter test, dan test runner menyiapkannya:
| Fixture | Isinya |
|---|---|
page | Tab baru di browser context baru, bersih untuk setiap test |
context | Browser context milik test itu |
browser | Browser yang dipakai bersama (jarang perlu dipakai langsung) |
browser_name / browserName | "chromium", "firefox", atau "webkit" |
playwright (Py) / request (TS) | Untuk API test (Modul 13) |
base_url (Py) | Base URL dari konfigurasi |
Setiap test dapat context baru: cookies kosong, penyimpanan kosong, belum login. Kalau test A login, test B tidak ikut login, jadi test boleh jalan dalam urutan apa pun, bahkan paralel.
Pola AAA: siapkan, lakukan, periksa
Pola yang membuat test mudah dibaca:
- Arrange (siapkan): buka halaman, siapkan data, login.
- Act (lakukan): satu aksi utama yang mau diuji.
- Assert (periksa): pastikan hasilnya sesuai harapan.
def test_login_berhasil(page): # fungsi berawalan test_ = satu test
# Arrange
page.goto("/login-toko.html")
# Act
page.get_by_label("Username").fill("customer")
page.get_by_label("Password").fill("rahasia123") # isi Password
page.get_by_role("button", name="Masuk").click() # klik Masuk
# Assert
expect(page).to_have_url(re.compile(r"shop\.htmlMemuat materi…quot;))
expect(page.get_by_test_id("greeting")).to_have_text("Halo, Rina Wulandari") # sapaan menyebut nama customertest('login berhasil', async ({ page }) => { // satu test
// Arrange
await page.goto('/login-toko.html');
// Act
await page.getByLabel('Username').fill('customer');
await page.getByLabel('Password').fill('rahasia123'); // isi Password
await page.getByRole('button', { name: 'Masuk' }).click(); // klik Masuk
// Assert
await expect(page).toHaveURL(/shop\.html$/);
await expect(page.getByTestId('greeting')).toHaveText('Halo, Rina Wulandari'); // sapaan menyebut nama customer
});Beri nama test sesuai perilaku yang diuji. test_login_gagal_saat_password_salah langsung memberi tahu apa yang rusak, test_login_2 tidak.
Persiapan yang bisa dipakai berulang
Di pytest, persiapan bersama ditulis sebagai fixture. Kode sebelum yield adalah setup, setelah yield adalah teardown.
import pytest # pustaka pytest
@pytest.fixture # tandai fungsi di bawah sebagai fixture
def halaman_login(page): # fixture: persiapan yang dipakai ulang
page.goto("/login-toko.html") # setup
yield page # test berjalan di sini
# teardown (opsional): misalnya hapus data yang dibuat test
def test_form_login_tampil(halaman_login): # test meminta fixture lewat nama parameternya
expect(halaman_login.get_by_role("button", name="Masuk")).to_be_visible() # tombol Masuk terlihat- Fixture boleh memakai fixture lain (
halaman_loginmemakaipage). - Fixture yang dipakai banyak file taruh di
conftest.py, pytest menemukannya tanpaimport. - Scope mengatur seberapa sering fixture dibuat:
function(bawaan),module, atausession.
Di Playwright Test, cara paling sederhana adalah memakai hook:
test.describe('Login TokoQA', () => { // kelompokkan test yang sejenis
test.beforeEach(async ({ page }) => {
await page.goto('/login-toko.html'); // jalan sebelum SETIAP test di grup ini
});
test.afterEach(async ({ page }, testInfo) => { // dijalankan setelah setiap test
console.log(`${testInfo.title} → ${testInfo.status}`);
});
test('form login tampil', async ({ page }) => { // satu test di dalam kelompok
await expect(page.getByRole('button', { name: 'Masuk' })).toBeVisible();
});
});beforeAll/afterAll jalan sekali per file atau grup. Untuk persiapan yang dipakai banyak file, buat fixture sendiri dengan test.extend() (Modul 10).
Satu test, banyak data
Tulis test login gagal sekali, lalu berikan datanya:
@pytest.mark.parametrize( # jalankan test yang sama dengan banyak data
("username", "password", "pesan_error"),
[
("customer", "salah", "Username atau password salah"),
("terkunci", "rahasia123", "Akun kamu terkunci. Hubungi admin."),
("", "", "Username dan password wajib diisi"),
],
ids=["password-salah", "akun-terkunci", "form-kosong"],
)
def test_login_gagal(halaman_login, username, password, pesan_error): # satu test, dijalankan sekali per baris data
halaman_login.get_by_label("Username").fill(username) # username dari data
halaman_login.get_by_label("Password").fill(password) # password dari data
halaman_login.get_by_role("button", name="Masuk").click() # klik Masuk
expect(halaman_login.get_by_role("alert")).to_have_text(pesan_error) # pesan error sesuai dataconst kasusGagal = [ // tabel data: satu baris = satu test
{ judul: 'password salah', username: 'customer', password: 'salah', pesan: 'Username atau password salah' },
{ judul: 'akun terkunci', username: 'terkunci', password: 'rahasia123', pesan: 'Akun kamu terkunci. Hubungi admin.' },
{ judul: 'form kosong', username: '', password: '', pesan: 'Username dan password wajib diisi' },
];
for (const { judul, username, password, pesan } of kasusGagal) { // buat satu test untuk setiap baris
test(`login gagal: ${judul}`, async ({ page }) => {
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password); // password dari data
await page.getByRole('button', { name: 'Masuk' }).click(); // klik Masuk
await expect(page.getByRole('alert')).toHaveText(pesan); // pesan error sesuai data
});
}Hasilnya tiga test terpisah di laporan, jadi data penyebab gagal langsung terlihat.
File asli Modul 7
Memuat tests/academy/test_m07_runner.py…
uv run pytest tests/academy/test_m07_runner.py -vMemuat tests/academy/m07-runner.spec.ts…
npx playwright test m07-runner --project=chromiumMenjalankan test sesuai kebutuhan
Menampilkan Python. Ganti bahasa di bagian atas untuk melihat TypeScript.Menampilkan TypeScript. Ganti bahasa di bagian atas untuk melihat Python.
| Mau apa | pytest | Playwright Test |
|---|---|---|
| Jalankan semua test | uv run pytest | npx playwright test |
| Satu file saja | uv run pytest tests/academy/test_m07_runner.py | npx playwright test m07-runner |
| Cari berdasarkan nama test | -k "login and gagal" | -g "login gagal" |
| Hanya yang punya tag smoke | -m smoke | --grep @smoke |
| Semua kecuali tanda tertentu | -m "not smoke" | --grep-invert @smoke |
| Tampilkan hasil per test | -v | --reporter=list |
| Ulangi yang tadi gagal saja | --lf (last failed) | --last-failed |
| Jalankan paralel | -n 4 (plugin pytest-xdist) | --workers=4 (sudah bawaan) |
| Ulangi otomatis kalau gagal | --reruns 2 (plugin pytest-rerunfailures) | --retries=2 |
| Berhenti di kegagalan pertama | -x | -x |
Melewati test dan sejenisnya
@pytest.mark.skip(reason="Fitur voucher belum rilis") # lewati test ini, alasannya tampil di laporan
def test_voucher(page): ... # badan test belum ditulis
@pytest.mark.skipif(sys.platform == "win32", reason="Tidak relevan di Windows") # lewati hanya kalau kondisinya benar
def test_khusus_linux(page): ... # badan test
@pytest.mark.xfail(reason="Bug #123: total salah hitung") # diharapkan gagal
def test_total_dengan_diskon(page): ... # test yang diharapkan gagal (bug yang sudah diketahui)
def test_khusus_chromium(page, browser_name): # browser_name dari pytest-playwright
if browser_name != "chromium": # bukan Chromium?
pytest.skip("Hanya relevan di Chromium") # lewati dari dalam testtest.skip('voucher', async ({ page }) => { /* fitur belum rilis */ }); // lewati test ini
test('khusus linux', async ({ page }) => { // test yang dilewati di kondisi tertentu
test.skip(process.platform === 'win32', 'Tidak relevan di Windows');
});
test.fail('total dengan diskon', async ({ page }) => { /* Bug #123, diharapkan gagal */ }); // diharapkan gagal karena bug yang sudah diketahui
test('khusus chromium', async ({ page, browserName }) => { // browserName dari Playwright Test
test.skip(browserName !== 'chromium', 'Hanya relevan di Chromium');
});
// test.only(...) → HANYA test ini yang jalan. Jangan sampai ter-commit!Kalau test.only lupa dihapus, CI hanya menjalankan satu test dan semuanya terlihat hijau. Pengaturan forbidOnly: !!process.env.CI (sudah ada di config buatan wizard) mencegahnya.
Lebih jauh (opsional)Trik komunitas
- Menjalankan paralel di Python:
pytest-xdistdengan-n automemakai semua inti prosesor laptopmu. Mengulang test yang gagal:pytest-rerunfailuresdengan--reruns 2. test.step()(TypeScript) membagi test panjang menjadi langkah-langkah bernama di laporan. Di Python, pecah test menjadi fungsi kecil dengan nama yang jelas.--only-changed(TypeScript, v1.46+) hanya menjalankan file test yang berubah sejak commit terakhir. Hemat waktu saat kamu sedang menulis test.- Fixture otomatis:
autouse=True(pytest) atau{ auto: true }(Playwright Test) untuk hal yang selalu dibutuhkan, misalnya mencatat error di setiap test. - Annotation menautkan test ke tiket bug, sehingga langsung terlihat di laporan.
uv add --dev pytest-xdist pytest-rerunfailures # plugin untuk paralel dan ulang otomatis
uv run pytest -n auto --reruns 2 # paralel sesuai jumlah CPU, test gagal diulang 2 kalitest('checkout dengan kupon', { // test dengan tag dan anotasi
tag: '@regression',
annotation: { type: 'issue', description: 'https://github.com/org/repo/issues/123' },
}, async ({ page }) => { // lanjutan: badan test
await test.step('login', async () => { /* ... */ });
await test.step('isi keranjang dan bayar', async () => { /* ... */ }); // langkah bernama, tampil di laporan
});
// npx playwright test --only-changedLatihan
Kerjakan di file test baru, jalankan, lalu bandingkan dengan jawaban.
Tambah kasus login gagal tanpa menulis test baru
Salin test login gagal di atas, tambahkan satu kasus (username admin, password salah), lalu jalankan dengan -v (Python) atau reporter list (TypeScript).
Hasil4 test lulus, masing-masing dengan nama kasusnya
Memuat tests/latihan/test_m07_latihan.py…
Memuat tests/latihan/m07-latihan.spec.ts…
Buat fixture atau hook yang membuka Arena
Buat fixture (Python) atau beforeEach (TypeScript) yang membuka Arena. Pakai di 2 test: satu mengklik "Klik aku", satu menekan Escape di kolom keyboard.
Hasilkedua test lulus tanpa goto di dalam test-nya
Memuat tests/latihan/test_m07_latihan.py…
Memuat tests/latihan/m07-latihan.spec.ts…
Tulis test dengan pola AAA
Login sebagai customer (Arrange), tambahkan Mouse Wireless dan Lampu Meja LED ke keranjang (Act), lalu pastikan jumlah keranjang 2 dan totalnya Rp 305.000 (Assert). Beri komentar penanda di setiap bagian.
Hasiltest lulus dan mudah dibaca dari atas ke bawah
Memuat tests/latihan/test_m07_latihan.py…
Memuat tests/latihan/m07-latihan.spec.ts…
Jalankan hanya test smoke
Jalankan hanya test dengan tag smoke di seluruh project.
Hasilhanya beberapa test yang jalan, sisanya dilewati
uv run pytest -m smoke -v # hanya test bertanda smokenpx playwright test --grep @smoke # hanya test yang judulnya mengandung @smokeDi project contoh Academy, tag smoke dipakai di contoh Modul 7 (login berhasil), Modul 12 (contoh laporan Allure), dan proyek akhir Modul 17 (alur belanja dari login sampai pesanan berhasil).