TestForge

Otomasi QAPelajaran 8 dari 12

English

Otomasi API

Pengujian yang lebih cepat dan lebih mantap di bawah UI — dan memakai API untuk menyiapkan pengujian UI.

14 mntOtomasi QA

Lapisan yang paling kurang dimanfaatkan kebanyakan suite

Pengujian UI untuk "membuat case dengan judul kosong ditolak" memakan delapan detik, mengemudikan sebuah browser, dan bisa gagal karena sebuah tombol pindah. Aturan yang sama diuji terhadap endpoint-nya memakan 200 milidetik dan gagal hanya ketika aturannya sendiri rusak.

Itulah pertukaran yang menjadi pokok pelajaran pertama track ini, dibuat konkret: dorong setiap pengujian serendah mungkin selagi ia masih menyatakan sesuatu yang benar tentang apa yang diterima pengguna. Aturan validasi, hak akses, kode kesalahan, dan logika bisnis hampir selalu lebih rendah daripada browser.

Anda sebenarnya sudah memakai lapisan ini. Pelajaran sebelumnya menyiapkan data lewat API karena melakukannya lewat UI itu lambat dan rapuh. Pelajaran ini adalah alat yang sama, diarahkan ke hal yang sedang diuji alih-alih ke persiapannya.

Playwright menguji API tanpa browser

Tanpa dependensi baru, tanpa framework kedua:

import { test, expect } from "@playwright/test";

const PROJECT = process.env.TF_PROJECT!;   // slug proyek sandbox Anda

test("rejects a case with a blank title", async ({ request }) => {
  const res = await request.post(`/api/v1/projects/${PROJECT}/cases`, {
    data: { title: "" },
  });

  expect(res.status()).toBe(422);
  const { error } = await res.json();
  expect(error.code).toBe("validation_error");
  expect(error.details.map((d) => d.field)).toContain("title");
});

Setiap route tulis di API ini dilingkupi proyek — slug-nya bagian dari path-nya, dan tidak ada koleksi /api/v1/cases di atasnya. Membaca /api/v1/openapi sekali, sebelum menulis satu pun pengujian ini, lebih murah daripada menemukan bentuknya satu 404 demi satu 404.

Fixture request adalah klien HTTP dengan baseURL dari config-nya dan toples cookie-nya sendiri. Dua hal mengikutinya: ia tidak membuka browser, jadi pengujian ini berjalan dalam milidetik; dan ia bisa berbagi autentikasi dengan pengujian UI Anda alih-alih membutuhkan mekanisme login yang terpisah.

Perhatikan gaya asersinya. expect(res.status()) adalah perbandingan nilai biasa, bukan web-first assertion — tidak ada yang perlu dijajaki berulang, karena sebuah response HTTP entah tiba entah tidak. Aturan coba-ulang dari pelajaran asersi berlaku untuk locator; di sini bentuk biasanya-lah yang benar.

Melakukan autentikasi sekali

// playwright.config.ts
use: {
  baseURL: process.env.TF_BASE_URL,
  extraHTTPHeaders: {
    Authorization: `Bearer ${process.env.TF_API_KEY}`,
  },
},

API key dari environment adalah jawaban benar yang paling sederhana, dan itulah yang diharapkan TestForge sendiri. Ketika sebuah pengujian butuh identitas yang berbeda — memeriksa bahwa seorang viewer tidak bisa menghapus sebuah suite — bangunlah klien untuknya alih-alih mengubah yang bersama:

test("a viewer cannot delete a suite", async ({ playwright }) => {
  const viewer = await playwright.request.newContext({
    baseURL: process.env.TF_BASE_URL,
    extraHTTPHeaders: { Authorization: `Bearer ${process.env.TF_VIEWER_KEY}` },
  });

  const res = await viewer.delete(`/api/v1/projects/${PROJECT}/suites/${suiteId}`);
  expect(res.status()).toBe(403);

  await viewer.dispose();
});

Pengujian otorisasi adalah hal paling bernilai di lapisan ini, dan ia nyaris mustahil lewat UI yang sekadar menyembunyikan tombolnya. Tombol yang disembunyikan bukan pemeriksaan hak akses — endpoint-nya yang begitu — dan beginilah cara Anda mengetahui yang mana sebenarnya yang dimiliki aplikasi Anda. Track manual membuat argumen yang sama tentang memeriksa otorisasi lewat URL lebih dulu; ini bentuk otomatisnya.

Apa yang diasersikan pada sebuah response

Lebih dari kode statusnya, dan kurang dari segalanya:

const path = `/api/v1/projects/${PROJECT}/cases`;
const res = await request.post(path, { data: { title: "TC-12", suiteId } });

expect(res.status()).toBe(201);                       // 1. status
expect(res.headers()["content-type"]).toContain("application/json");

const { id, displayId } = await res.json();           // create hanya menjawab dengan id
expect(displayId).toMatch(/^TC-[A-Z0-9-]+-\d{3}$/);   // 2. bentuknya, bukan nilai persisnya

const created = await (await request.get(`${path}/${id}`)).json();
expect(created).toMatchObject({ title: "TC-12", suiteId, priority: "MEDIUM" });
expect(new Date(created.createdAt).getTime()).toBeGreaterThan(0);

Ada dua kebiasaan di cuplikan itu. toMatchObject adalah kuda bebannya: ia memeriksa kolom yang Anda sebutkan dan mengabaikan sisanya, sehingga kolom baru yang ditambahkan ke response tidak merusak empat puluh pengujian. Mengasersikan kesetaraan dalam terhadap seluruh payload adalah padanan API dari rantai selektor CSS — ia gagal karena perubahan yang bukan cacat. Perhatikan bahwa priority bukan sesuatu yang dikirim pengujiannya: mengasersikan nilai default dari server adalah cara Anda tahu ketika ada yang mengubahnya.

Dan asersikan bentuk untuk apa pun yang dibangkitkan server. displayId yang cocok dengan TC-<SLUG>-<nnn> adalah kontrak yang nyata, kontrak yang menjadi sandaran karya penutup JUnit; displayId === "TC-DEMO-012" adalah pencacah hari ini. id yang buram tidak layak diasersikan apa-apa selain berupa string yang tidak kosong.

Layak pula diperhatikan apa yang tidak dikembalikan endpoint ini. Create yang menjawab dengan identifier alih-alih seluruh catatannya itu lazim, dan artinya verifikasinya adalah request kedua — dan itu bukan hal buruk, karena membaca kembali sumber dayanya adalah pemeriksaan yang lebih kuat daripada memercayai response dari operasi tulisnya.

Kode status yang layak dicermati

Pengujian yang menerima "error apa pun" nyaris bukan pengujian. Perbedaan di antara semua ini biasanya berupa cacat yang nyata:

KodeArtinyaBug lazim yang ditangkapnya
400Request cacat bentukValidasi yang mengembalikan 500 alih-alih ini
401Belum terautentikasiEndpoint yang lupa mewajibkan auth
403Terautentikasi, tidak diizinkanYang besar — hak akses tidak ditegakkan di sisi server
404Tidak ditemukanMembocorkan keberadaan: mengembalikan 403 vs 404 untuk catatan milik orang lain
409KonflikPenanganan duplikat yang diam-diam menimpa
422Dipahami, maknanya tidak validAturan bisnis yang dilewati

401 versus 403 dan 403 versus 404 adalah dua pasangan yang layak diuji secara eksplisit. Yang kedua lebih halus daripada kelihatannya: mengembalikan 403 untuk catatan yang ada tapi milik orang lain memberi tahu penyerang bahwa ia ada. Apa pun yang dipilih aplikasi Anda, ia sebaiknya memilih secara konsisten, dan sebuah pengujian adalah cara itu tetap benar.

TestForge adalah contoh nyata dari pilihan itu, dan Anda bisa membuktikannya dalam satu request. Kirim sebuah case ke proyek yang bukan tempat Anda menjadi anggota, dan Anda mendapat 404, bukan 403 — API-nya menolak membenarkan bahwa proyek itu ada sama sekali:

test("a project you are not in is indistinguishable from one that does not exist", async ({ request }) => {
  const res = await request.post("/api/v1/projects/someone-elses-project/cases", {
    data: { title: "probe" },
  });
  expect(res.status()).toBe(404);
});

Menguji jalur kesalahan itulah intinya

Happy path biasanya sudah tercakup oleh pengujian UI. Nilai lapisan ini ada pada segala yang tidak mudah dijangkau UI:

const cases = [
  { data: {}, field: "title", why: "no fields at all" },
  { data: { title: "" }, field: "title", why: "blank title" },
  { data: { title: "TC-1", priority: "URGENT" }, field: "priority", why: "a priority off the list" },
  { data: { title: "TC-1", suiteId: "does-not-exist" }, field: "suiteId", why: "a suite from another project" },
];

for (const c of cases) {
  test(`rejects ${c.why}`, async ({ request }) => {
    const res = await request.post(`/api/v1/projects/${PROJECT}/cases`, { data: c.data });
    expect(res.status()).toBe(422);
    const { error } = await res.json();
    expect(error.details.map((d) => d.field)).toContain(c.field);
  });
}

Perhatikan apa yang diasersikan tabel itu. Keempatnya 422, jadi pengujian yang hanya memeriksa statusnya akan lulus di server yang menolak setiap satunya dengan alasan yang keliru — menyebut kolom yang bermasalah itulah yang menjadikan barisnya pengujian sungguhan. Baris keempat yang paling menarik: id suite milik proyek lain adalah kegagalan validasi, bukan 404, karena mengakui "suite itu ada, hanya tidak di sini" akan menjadi kebocoran yang sama seperti yang ditutup bagian sebelumnya.

Membangkitkan pengujian dari sebuah tabel itu sah di sini dengan cara yang tidak sah di UI: setiap case adalah satu request yang cepat, pesan kegagalannya menyebut baris mana yang gagal, dan menambahkan batas kelima belas berbiaya satu baris. Pertahankan sebagai panggilan test() yang terpisah alih-alih perulangan di dalam satu pengujian, supaya sebuah kegagalan melaporkan case yang spesifik dan satu baris yang gagal tidak menyembunyikan empat sesudahnya.

Pengujian hibrida adalah tempat ini paling membayar

test("TC-SHOP-31 a case created by API appears in the case list", async ({ page, request }) => {
  const title = `login ${Date.now()}`;
  const res = await request.post(`/api/v1/projects/${PROJECT}/cases`, {
    data: { title, suiteId },
  });
  const created = await res.json();

  await page.goto(`/projects/${PROJECT}/cases`);
  await expect(page.getByRole("row", { name: title })).toBeVisible();
  await expect(page.getByText(created.displayId)).toBeVisible();
});

Siapkan di bawah, bertindak dan asersikan di atas. Inilah bentuk yang akhirnya ditempati sebagian besar suite yang matang, dan itulah sebabnya pelajaran sebelumnya dan yang ini berpasangan: API sekaligus hal yang diuji dan alat yang membuat pengujian UI jadi cepat dan mandiri.

Arah sebaliknya juga layak diketahui — lakukan sebuah aksi di UI, lalu verifikasi lewat API bahwa state yang tersimpan sudah benar. Form yang tampak menyimpan tapi menulis kolom yang keliru adalah bug yang akan dengan senang hati disembunyikan layar dari Anda.

Apa yang tidak akan diberitahukan lapisan ini

Bersikap jujur tentang batasnya menjaga argumen piramidanya tetap jujur:

  • Bahwa fiturnya bekerja untuk seorang manusia. Setiap endpoint bisa benar sementara tombol yang memanggilnya nonaktif.
  • Apa pun tentang render, tata letak, atau aksesibilitas.
  • Bahwa klien-nya mengirim apa yang Anda kira ia kirim. Pengujian Anda membangun request-nya; aplikasi yang sungguhan membangun yang lain. Inilah celah yang menjadi alasan contract testing ada, dan tempatnya di track Beyond Functional.

Jadi pembagiannya bukan "pengujian API menggantikan pengujian UI". Melainkan: aturan, hak akses, dan jalur kesalahan di bawah; sejumlah kecil perjalanan yang benar-benar ditempuh pengguna di atas.

Di mana TestForge berperan

Karya penutupnya memakai perkakas pelajaran ini sungguh-sungguh: /api/v1/junit adalah sebuah endpoint, unggahan Anda adalah sebuah POST dengan body multipart, dan run yang dihasilkannya adalah sesuatu yang lalu bisa Anda baca kembali dan asersikan. Berlatih terhadap /api/v1/projects/<slug>/cases di proyek sandbox Anda sekarang persis otot yang dibutuhkan karya penutupnya — bentuk terlingkupi-proyek yang sama dengan yang Anda kirim dengan tangan di pelajaran API testing di T2.

GET    /api/v1/openapi                                 setiap route, terbaca mesin
GET    /api/v1/projects/<slug>/cases                   daftar
POST   /api/v1/projects/<slug>/cases                   buat → { id, displayId }
DELETE /api/v1/projects/<slug>/cases/<caseId>          hapus lunak
POST   /api/v1/projects/<slug>/suites                  buat → { id, name, parentId }
Header: Authorization: Bearer <API_KEY>

Layak dicoba sekali untuk merasakan bentuknya: buat sebuah case lewat API, unggah sebuah hasil JUnit yang nama pengujiannya membawa displayId case itu, lalu baca run-nya kembali untuk memastikan pencocokannya mendarat. Itu seluruh lingkaran produknya dalam tiga request, dan itulah yang dirangkai dengan benar oleh dua pelajaran terakhir track ini.

Selanjutnya: menjalankan suite-nya di CI dengan GitHub Actions — workflow, artifact, dan menjaga pipeline-nya di bawah sepuluh menit supaya orang sungguh-sungguh mau menunggunya.

Uji pemahaman Anda

3 pertanyaan. Tidak perlu akun, dan tidak ada yang dikirim ke mana pun selain ke pemeriksa jawaban.

  1. 1. Kenapa memeriksa bahwa seorang viewer mendapat 403 dari DELETE /api/v1/projects/<slug>/suites/<suiteId> lebih bernilai daripada memeriksa bahwa tombol hapusnya disembunyikan di UI?

  2. 2. Gaya asersi mana yang tepat untuk id yang dibangkitkan bagi sebuah catatan baru?

  3. 3. Mana di antara ini yang benar tentang posisi pengujian API berdampingan dengan pengujian UI?(pilih semua yang sesuai)

Jawab semua pertanyaan dulu.