Testing Kode Asinkron — Testing

Tantangan Testing Async Kode asinkron (async/await, Promise, setTimeout) membutuhkan penanganan khusus saat testing. Tanpa penanganan yang benar, test bisa…

Kode async adalah kode yang hasilnya datang belakangan: request ke server, timer setTimeout, membaca file. Di JavaScript, hasil yang belum datang dibungkus Promise. Bayangin pesan makanan di food court: kasir memberimu buzzer, makanannya datang nanti. Buzzer itu Promise-nya. Kalau kamu langsung memotret nampan begitu buzzer di tangan, nampannya masih kosong. Test async juga begitu: test harus menunggu buzzer bunyi dulu, baru memeriksa isinya.

🧪 Tebakan cepat: ambilData("/kosong") memberi data kosong. Test di bawah memeriksa datanya berisi dua nama, tapi lewat .then tanpa await. Vitest menandainya ✓ (lulus) atau × (gagal)? Tebak dulu, jawabannya ada di bagian "Lupa menunggu: test lulus palsu".

it("data /kosong berisi dua nama", () => {
  ambilData("/kosong").then((hasil) => {
    expect(hasil.data).toHaveLength(2)
  })
})

Promise: janji yang selesai belakangan

Ini function yang dipakai sepanjang materi ini. Dia pura-pura mengambil data dari server:

function ambilData(url) {
  return new Promise((resolve, reject) => {
    if (url.includes("users")) {
      resolve({ data: ["Budi", "Siti"] });
    } else if (url.includes("error")) {
      reject(new Error("Gagal mengambil data"));
    } else {
      resolve({ data: [] });
    }
  });
}
Pemanggilan Hasil Promise-nya
ambilData("/users") resolve { data: ["Budi", "Siti"] }
ambilData("/error") reject Error("Gagal mengambil data")
ambilData("/kosong") resolve { data: [] }

async dan await di dalam test

import { describe, it, expect } from "vitest"

describe("ambilData", () => {
  it("memberi dua nama untuk /users", async () => {
    const hasil = await ambilData("/users")
    expect(hasil.data).toEqual(["Budi", "Siti"])
  })
})

Kalau await-nya lupa, hasil berisi Promise-nya sendiri (buzzer-nya), bukan datanya. hasil.data jadi undefined, dan Vitest menulis:

AssertionError: expected undefined to deeply equal [ 'Budi', 'Siti' ]

resolves dan rejects: periksa Promise langsung

it("memberi dua nama untuk /users", async () => {
  await expect(ambilData("/users")).resolves.toEqual({ data: ["Budi", "Siti"] })
})

it("ditolak untuk /error", async () => {
  await expect(ambilData("/error")).rejects.toThrow("Gagal mengambil data")
})

Menangkap Promise yang ditolak dengan try/catch

Tanpa Vitest pun kamu bisa menguji Promise. Pola ini yang dipakai di latihan: hitung sendiri kasus yang lulus.

async function testAmbilData() {
  let passed = 0

  try {
    await ambilData("/error")
  } catch (e) {
    if (e.message === "Gagal mengambil data") passed++
  }

  // dua kasus lain: "/users" (data.length 2) dan url lain (data.length 0)
  return passed
}

testAmbilData().then((jumlah) => console.log(jumlah))

Lupa menunggu: test lulus palsu

Ini jawaban tebakannya: ✓, lulus, padahal datanya kosong. Vitest 4 menulis:

 ✓ src/ambilData.test.js > ambilData > data /kosong berisi dua nama 2ms

Callback-nya gak async dan gak mengembalikan apa-apa, jadi buat Vitest test-nya selesai begitu function-nya kembali. .then baru jalan sesudahnya, dan expect yang gagal di dalamnya sudah bukan bagian dari test. Karena ambilData di sini selesai seketika, Vitest kebetulan masih menangkap error nyasar itu di blok terpisah, Unhandled Rejection, dengan pesan This might cause false positive tests. Tapi kalau datanya dari server sungguhan yang butuh 300 ms, run-nya sudah selesai duluan dan gak ada yang melapor.

Obatnya: async di callback, lalu await Promise-nya (atau return-kan Promise-nya).

Fake timers: majukan jam tanpa menunggu

Kode dengan setTimeout gak perlu ditunggu sungguhan. Vitest bisa mengganti jam-nya dengan jam palsu yang kamu putar sendiri, seperti mempercepat timer microwave.

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"

function delayedHello(callback) {
  setTimeout(() => {
    callback("hello")
  }, 1000)
}

describe("delayedHello", () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  it("memanggil callback setelah 1 detik", () => {
    const callback = vi.fn()
    delayedHello(callback)
    expect(callback).not.toHaveBeenCalled()

    vi.advanceTimersByTime(1000)
    expect(callback).toHaveBeenCalledWith("hello")
  })
})
Langkah Jam palsu callback dipanggil?
delayedHello(callback) 0 ms belum, timer-nya dijadwalkan di 1.000 ms
vi.advanceTimersByTime(999) 999 ms belum
vi.advanceTimersByTime(1) 1.000 ms ya, dengan "hello"

Tanpa vi.useFakeTimers(), memajukan jam langsung ditolak:

Error: A function to advance timers was called but the timers APIs are not mocked. Call `vi.useFakeTimers()` in the test file first.

⚠️ Jebakan yang sering ketemu

TL;DR: Promise membawa hasil yang datang belakangan: resolve kalau berhasil, reject kalau gagal. Di test, tandai callback dengan async lalu await Promise-nya sebelum memeriksa. await expect(p).resolves.toEqual(...) memeriksa isi Promise yang berhasil, await expect(p).rejects.toThrow("...") memeriksa yang ditolak. Tanpa Vitest, tangkap penolakan dengan try/catch. expect di dalam .then tanpa await bikin test lulus palsu. Untuk setTimeout, pakai vi.useFakeTimers(), majukan jam dengan vi.advanceTimersByTime(ms), dan kembalikan dengan vi.useRealTimers() di afterEach.

Yang akan kamu pelajari