Otomasi QAPelajaran 5 dari 12
EnglishAsersi dan menunggu
Web-first assertion, auto-waiting, dan kenapa sleep() adalah bug.
13 mntOtomasi QA
Pengujian yang tidak pernah bisa gagal bukanlah pengujian yang lulus
Dua pengujian, fitur yang sama, keduanya hijau:
// A
await page.getByRole("button", { name: "Create project" }).click();
await page.waitForTimeout(3000);
// B
await page.getByRole("button", { name: "Create project" }).click();
await expect(page.getByRole("heading", { name: "New project" })).toBeVisible();
Pengujian A tidak mungkin gagal. Ia mengeklik, menunggu tiga detik, dan melapor berhasil apa pun yang dilakukan aplikasinya — termasuk tidak melakukan apa-apa. Ia pengujian dalam arti ia berjalan, dan tidak dalam arti yang lain.
Segala isi pelajaran ini kembali ke sana: asersi adalah satu-satunya bagian sebuah pengujian yang bisa menemukan bug, dan menunggu adalah mekanisme yang menentukan apakah asersi Anda sempat berjalan terhadap momen yang tepat.
Auto-waiting sudah mengerjakan sebagian besarnya
Pelajaran sebelumnya menyebut locator itu malas — tidak ada yang dicari sampai Anda bertindak. Paruh yang penting di sini adalah apa yang dilakukan Playwright sebelum ia bertindak. Setiap aksi menjalankan serangkaian pemeriksaan keteraksian (actionability) lebih dulu dan mencobanya ulang sampai lolos atau waktunya habis:
| Aksi | Menunggu elemennya |
|---|---|
click() | terpasang, terlihat, diam (tidak sedang beranimasi), aktif, tidak tertutup elemen lain |
fill() | terpasang, terlihat, aktif, bisa disunting |
check() | sama seperti click, plus benar-benar sebuah checkbox atau radio |
selectOption() | terpasang, terlihat, aktif |
hover() | terpasang, terlihat, diam, menerima peristiwa |
Itulah sebabnya pengujian Playwright yang ditulis dengan baik nyaris tidak butuh penungguan eksplisit. Klik pada tombol yang muncul setengah detik setelah halamannya dimuat begitu saja bekerja — bukan karena margin waktu yang beruntung, melainkan karena klik-nya dicoba ulang sampai tombolnya ada, terlihat, diam, dan bisa diklik.
"Tidak tertutup elemen lain" adalah yang paling layak bayarannya. Banner cookie, sebuah toast, atau latar modal di atas tombol Anda adalah sumber klasik klik yang menembak ke kehampaan dan pengujian yang gagal dua langkah kemudian dengan pesan yang membingungkan. Playwright menolak mengeklik menembusnya dan memberi tahu Anda apa yang menghalangi.
Web-first assertion juga mencoba ulang
expect() di Playwright bukan expect() dari unit testing. Diberi sebuah
locator, ia menjajaki berulang sampai kondisinya benar atau waktunya habis:
await expect(page.getByRole("alert")).toHaveText("Project created");
await expect(page.getByRole("row")).toHaveCount(4);
await expect(page.getByRole("button", { name: "Save" })).toBeEnabled();
await expect(page).toHaveURL(/\/projects\/[a-z0-9]+$/);
await expect(page.getByTestId("status-pill")).toHaveAttribute("data-state", "passed");
Bandingkan dengan bentuk yang tidak mencoba ulang, dan di situlah kelabilan ditulis:
// membaca sekali, di momen apa pun ketika baris ini kebetulan berjalan
expect(await page.getByRole("row").count()).toBe(4);
Versi itu mengambil sampel halamannya pada satu titik waktu. Kalau baris keempat
tiba 50md kemudian, ia gagal — di lokal tidak pernah, di runner CI yang sarat
kadang-kadang. Aturannya: serahkan locator-nya ke dalam expect, jangan
await nilainya keluar lebih dulu. Di mana pun Anda melihat
expect(await …) di sebuah pengujian UI, Anda sedang melihat sebuah perlombaan.
Dua lagi yang mengikuti bentuk yang sama:
await expect(locator).toBeVisible(); // mencoba ulang
expect(await locator.isVisible()).toBe(true); // potret — perlombaan
isVisible() dan kawan-kawannya untuk bercabang berdasarkan state yang memang
tidak Anda ketahui ("apakah banner-nya ada? kalau ya tutup"), bukan untuk
mengasersikan.
Asersi yang layak diketahui
// keberadaan dan state
await expect(l).toBeVisible();
await expect(l).toBeHidden();
await expect(l).toBeEnabled();
await expect(l).toBeChecked();
await expect(l).toBeFocused();
// isi
await expect(l).toHaveText("Exactly this");
await expect(l).toContainText("part of this");
await expect(l).toHaveValue("typed@example.com");
await expect(l).toHaveCount(3);
// atribut dan kelas
await expect(l).toHaveAttribute("aria-expanded", "true");
await expect(l).toHaveClass(/active/);
// tingkat halaman
await expect(page).toHaveTitle(/TestForge/);
await expect(page).toHaveURL("/projects");
toHaveText pada locator yang cocok dengan beberapa elemen membandingkan
terhadap seluruh daftarnya, dan itu cara yang rapi untuk mengasersikan sebuah
urutan:
await expect(page.getByRole("row")).toHaveText([/TC-10/, /TC-11/, /TC-12/]);
Dan .not membalik salah satunya — await expect(l).not.toBeVisible() —
dengan perilaku coba-ulang yang memang Anda inginkan: ia menunggu benda itu
menghilang, alih-alih mengasersikan bahwa ia tidak ada pada satu titik waktu.
Kenapa sleep() adalah bug dan bukan jalan pintas
waitForTimeout() keliru di dua arah sekaligus, dan itulah yang membuatnya jadi
kekeliruan yang begitu awet:
- Terlalu pendek di hari yang buruk. CI lebih lambat daripada laptop Anda ketika sedang sarat. Tiga detik yang bekerja sepanjang minggu gagal di pagi ketika semua orang mendorong kodenya.
- Terlalu panjang di setiap hari yang baik. Tiga puluh sleep tiga detik adalah sembilan puluh detik yang ditambahkan ke setiap run, selamanya, untuk apa-apa.
Perbaikannya tidak pernah berupa angka yang lebih besar. Perbaikannya adalah menyatakan apa yang sedang Anda tunggu:
| Alih-alih | Tulislah |
|---|---|
waitForTimeout(2000) setelah sebuah klik | await expect(page.getByRole("alert")).toBeVisible() |
waitForTimeout(1000) menunggu daftar dimuat | await expect(page.getByRole("row")).toHaveCount(4) |
waitForTimeout(500) menunggu spinner | await expect(page.getByTestId("spinner")).toBeHidden() |
waitForTimeout(3000) menunggu penyimpanan | await page.waitForResponse(r => r.url().includes("/api/cases") && r.ok()) |
Dokumentasi Playwright sendiri menyebut waitForTimeout tidak boleh dipakai di
pengujian produksi, dan API-nya ada untuk debugging. Perlakukan sebuah
waitForTimeout di dalam pull request sebagaimana Anda memperlakukan asersi yang
dikomentari.
Satu-satunya kekecualian yang jujur adalah jeda tetap yang dipaksakan dari luar — sebuah debounce yang tidak bisa Anda amati, widget pihak ketiga tanpa sinyal apa pun. Bahkan di situ pun, tuliskan alasannya di barisnya, karena kalau tidak orang berikutnya akan mengira itu ritual tanpa makna lalu menghapusnya atau menyalinnya.
Menunggu hal-hal yang bukan elemen
// sebuah response jaringan
const created = page.waitForResponse(r => r.url().endsWith("/api/cases") && r.status() === 201);
await page.getByRole("button", { name: "Save" }).click();
await created;
// navigasi yang dipicu sebuah aksi
await page.getByRole("link", { name: "Projects" }).click();
await expect(page).toHaveURL(/\/projects/);
// sebuah kondisi di dalam halamannya
await page.waitForFunction(() => document.querySelectorAll("[data-row]").length > 0);
Perhatikan bentuk yang pertama: mulai menunggu sebelum aksinya, await sesudahnya. Mendaftarkan penungguan setelah klik-nya adalah perlombaan — response-nya bisa jadi sudah tiba.
Sebagian besar waktu Anda tidak butuh satu pun dari ini. Lebih baik asersikan
konsekuensi yang terlihat pengguna: kalau penyimpanannya berhasil, ada sesuatu
di layar yang mengatakannya, dan itu sekaligus penungguan yang lebih baik dan
asersi yang lebih baik. Jangkau waitForResponse ketika konsekuensinya memang
tak terlihat — panggilan analitik latar, penulisan yang dilepas begitu saja —
atau ketika Anda butuh isi response-nya.
networkidle layak mendapat peringatan khusus: ia menunggu kesunyian jaringan,
yang tidak pernah dicapai aplikasi dengan polling atau koneksi hidup, dan yang
tidak mengatakan apa pun tentang apakah hal yang Anda pedulikan sudah ter-render.
Ia tidak dianjurkan persis karena itu.
Timeout, dan di mana menyetelnya
// playwright.config.ts
export default defineConfig({
timeout: 30_000, // per pengujian
expect: { timeout: 5_000 }, // per web-first assertion
use: { actionTimeout: 10_000 }, // per aksi
});
Timpa per asersi ketika satu hal memang lambat, alih-alih menaikkan angka globalnya:
await expect(page.getByText("Import complete")).toBeVisible({ timeout: 60_000 });
Menaikkan timeout global untuk membetulkan pengujian yang gagal hampir selalu langkah yang keliru. Ia mengubah merah yang cepat menjadi merah yang lambat, dan pada hari pengujiannya benar-benar rusak Anda menunggu satu menit untuk mengetahuinya. Penimpaan lokal di sebelah sebuah impor yang lambat adalah fakta terdokumentasi tentang aplikasinya; timeout global 120 detik adalah catatan yang berbunyi "kami berhenti memahami suite ini".
Soft assertion, secukupnya saja
await expect.soft(page.getByTestId("total")).toHaveText("4 cases");
await expect.soft(page.getByTestId("passed")).toHaveText("3 passed");
await expect(page.getByRole("heading", { name: "Run summary" })).toBeVisible();
Soft assertion mencatat kegagalannya lalu membiarkan pengujiannya lanjut, sehingga satu run memberi tahu Anda bahwa ketiga angkanya keliru alih-alih hanya yang pertama. Berguna ketika memeriksa beberapa fakta independen tentang layar yang sama. Tidak berguna sebagai kebiasaan umum: kegagalan keras di awal mencegah pengujiannya menghasilkan air terjun error susulan yang menyesatkan, dan biasanya itulah yang Anda inginkan.
Asersikan hal yang diterima pengguna
Asersi lemah yang paling umum bukan sebuah perlombaan — melainkan mengasersikan hal yang keliru. Setelah mengeklik Simpan, memeriksa bahwa tombolnya masih terlihat tidak membuktikan apa pun. Memeriksa bahwa barisnya kini muncul di tabel, atau bahwa konfirmasinya menyebutkan catatannya, membuktikan fiturnya bekerja.
Pelajaran test oracle di track manual adalah gagasan yang sama dalam medium yang lain: Anda harus sanggup menyatakan bagaimana Anda akan tahu ia berhasil, sebelum Anda menulis barisnya. Kalau Anda tidak bisa menyelesaikan kalimat itu, asersi yang hendak Anda tulis adalah hiasan.
Di mana TestForge berperan
Pengujian dengan asersi yang sungguhan menghasilkan hasil yang bisa Anda percaya; pengujian yang diganjal sleep menghasilkan riwayat hijau yang tidak berarti apa pun, dan pada hari ia akhirnya memerah tidak ada yang memercayainya. Ketika run Anda mendarat di TestForge, riwayat case-nya hanya sejujur asersinya.
Ada juga versi terukur dari hal ini. Suite yang digerakkan sleep muncul sebagai
run yang durasinya terus tumbuh sementara jumlah case-nya nyaris tidak bergerak
— pelajaran metrik di track manual menyebut angka semacam itu layak diawasi.
Sembilan puluh detik waitForTimeout per run, empat puluh run
sehari, adalah satu jam CI sehari yang tidak membeli apa pun.
Selanjutnya: page object — struktur yang menghentikan lima puluh pengujian mengulang enam baris yang sama, dan titik di mana struktur itu berubah menjadi aplikasi kedua yang harus dirawat.
Uji pemahaman Anda
3 pertanyaan. Tidak perlu akun, dan tidak ada yang dikirim ke mana pun selain ke pemeriksa jawaban.
1. Baris mana di antara ini yang merupakan perlombaan yang menunggu terjadi di runner CI yang sarat?
2. Seorang rekan membetulkan pengujian yang kadang gagal di CI dengan mengubah waitForTimeout(2000) menjadi waitForTimeout(5000). Kenapa itu perbaikan yang keliru meskipun pengujiannya sekarang lulus?
3. Pernyataan mana tentang auto-waiting Playwright yang benar?(pilih semua yang sesuai)