Uji unit hitungPajak milik toko Budi lulus, uji terapkanKupon juga lulus — tetapi pembeli tetap mengeluh total akhirnya salah. Penyebabnya ada di sambungan: apakah pajak dihitung dari subtotal sebelum atau sesudah diskon? Masing-masing unit benar, namun urutan pemanggilannya keliru. Kasus seperti inilah alasan keberadaan uji integration (integrasi): memastikan beberapa unit bekerja sama menghasilkan perilaku yang benar secara keseluruhan.

Perbedaannya dengan uji unit terletak pada cakupan klaimnya. Uji unit mengatakan “fungsi ini benar ketika berdiri sendiri”; uji integration mengatakan “rangkaian unit ini menghasilkan keluaran akhir yang benar untuk skenario nyata”. Chapter ini membangun keduanya di atas satu modul alur pesanan: subtotal, kupon, lalu pajak.

Modul yang Diuji Bersama #

Modul pesanan.mjs memuat tiga unit kecil plus satu fungsi gabungan yang merangkainya. Perhatikan urutannya: diskon diterapkan lebih dulu, lalu pajak dihitung dari harga setelah diskon — keputusan yang justru sering salah dikodekan:

js pesanan.mjs
// pesanan.mjs — modul alur pesanan: subtotal, kupon, lalu pajak.

export function hitungSubtotal(item) {
  return item.reduce((total, baris) => total + baris.harga * baris.jumlah, 0);
}

const KUPON = {
  HEMAT10: 0.1,
  MEMBER15: 0.15,
};

export function terapkanKupon(subtotal, kode) {
  const persen = KUPON[kode] ?? 0;
  return Math.round(subtotal * (1 - persen));
}

export function hitungPajak(dasar) {
  return Math.round(dasar * 0.11);
}

// Fungsi gabungan: kerangka alur dari keranjang hingga total akhir.
export function hitungTotal(keranjang, kodeKupon = '') {
  const subtotal = hitungSubtotal(keranjang);
  const setelahDiskon = terapkanKupon(subtotal, kodeKupon);
  const pajak = hitungPajak(setelahDiskon);
  return {
    subtotal,
    diskon: subtotal - setelahDiskon,
    setelahDiskon,
    pajak,
    total: setelahDiskon + pajak,
  };
}

hitungTotal adalah jantung modul: ia memanggil ketiga unit lain sesuai urutan bisnis dan mengembalikan seluruh rincian perhitungan. Tiga unit lainnya berdiri sendiri-sendiri dan masing-masing pantas dapat uji unit. Sekarang berkas ujinya — bagian atas memuat data tiruan yang dipakai bersama, tengahnya uji unit, dan bagian akhirnya uji alur:

import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { hitungSubtotal, terapkanKupon, hitungPajak, hitungTotal } from './pesanan.mjs';

const keranjangBudi = [
  { nama: 'Buku Algoritma', harga: 85000, jumlah: 2 },
  { nama: 'Pulpen Gel', harga: 12000, jumlah: 3 },
  { nama: 'Buku Tulis', harga: 5000, jumlah: 5 },
];

describe('unit di dalam modul pesanan', () => {
  test('hitungSubtotal menjumlahkan seluruh baris', () => {
    assert.strictEqual(hitungSubtotal(keranjangBudi), 231000);
  });

  test('terapkanKupon HEMAT10 memotong sepuluh persen', () => {
    assert.strictEqual(terapkanKupon(231000, 'HEMAT10'), 207900);
  });

  test('terapkanKupon dengan kode asing tidak memotong apa pun', () => {
    assert.strictEqual(terapkanKupon(231000, 'DISKON99'), 231000);
  });

  test('hitungPajak menghitung PPN sebelas persen', () => {
    assert.strictEqual(hitungPajak(207900), 22869);
  });
});

test('alur lengkap dari keranjang hingga total akhir', () => {
  const rincian = hitungTotal(keranjangBudi, 'HEMAT10');

  assert.deepStrictEqual(rincian, {
    subtotal: 231000,
    diskon: 23100,
    setelahDiskon: 207900,
    pajak: 22869,
    total: 230769,
  });
});

test('alur tanpa kupon tetap berjalan', () => {
  const rincian = hitungTotal(keranjangBudi);

  assert.strictEqual(rincian.diskon, 0);
  assert.strictEqual(rincian.setelahDiskon, 231000);
  assert.strictEqual(rincian.total, 231000 + 25410);
});

test('alur dengan kupon MEMBER15 di keranjang Siti', () => {
  const rincian = hitungTotal(keranjangBudi, 'MEMBER15');

  assert.strictEqual(rincian.diskon, 34650);
  assert.strictEqual(rincian.setelahDiskon, 196350);
  assert.strictEqual(rincian.pajak, 21599);
  assert.strictEqual(rincian.total, 217949);
});

test('keranjang kosong menghasilkan total nol', () => {
  const rincian = hitungTotal([], 'HEMAT10');

  assert.deepStrictEqual(rincian, {
    subtotal: 0,
    diskon: 0,
    setelahDiskon: 0,
    pajak: 0,
    total: 0,
  });
});

Output yang Dihasilkan:

text
▶ unit di dalam modul pesanan
  ✔ hitungSubtotal menjumlahkan seluruh baris (0.603894ms)
  ✔ terapkanKupon HEMAT10 memotong sepuluh persen (0.131181ms)
  ✔ terapkanKupon dengan kode asing tidak memotong apa pun (0.249785ms)
  ✔ hitungPajak menghitung PPN sebelas persen (0.112754ms)
✔ unit di dalam modul pesanan (2.845907ms)
✔ alur lengkap dari keranjang hingga total akhir (0.746057ms)
✔ alur tanpa kupon tetap berjalan (0.119616ms)
✔ alur dengan kupon MEMBER15 di keranjang Siti (0.092794ms)
✔ keranjang kosong menghasilkan total nol (0.124569ms)
ℹ tests 8
ℹ suites 1
ℹ pass 8
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 61.352412

Membaca Skenario Alur Lengkap #

Uji alur lengkap dari keranjang hingga total akhir (baris 29–39) adalah uji integration sesungguhnya. Ia memanggil satu fungsi, tetapi fungsi itu menjalankan tiga unit di dalamnya, dan asersi deepStrictEqual menuntut kelima nilai sekaligus benar. Angkanya bisa diikuti seperti struk pembelian:

  1. subtotal: 231000 — hasil 85000×2 + 12000×3 + 5000×5 dari hitungSubtotal.
  2. diskon: 23100 — sepuluh persen dari subtotal, dipotong oleh kupon HEMAT10.
  3. setelahDiskon: 207900 — harga dasar bagi perhitungan berikutnya.
  4. pajak: 22869 — PPN 11 persen yang dihitung hitungPajak dari nilai setelah diskon, bukan dari subtotal.
  5. total: 230769 — jumlah setelahDiskon dan pajak, angka yang dibayar pembeli.

Kekuatan uji ini ada di klaim menyeluruhnya. Bila seseorang keliru menukar urutan — menghitung pajak dari subtotal penuh — angka pajak berubah menjadi 25410 dan total menjadi 233370; asersi langsung membedah kedua nilai yang menyimpang. Uji unit hitungPajak sendiri tidak akan sadar apa-apa, karena fungsi pajaknya tetap benar untuk input yang diberikan.

Skenario Batas di Sekeliling Alur Utama #

Tiga uji setelahnya mengawal alur utama dari sisi-sisi yang sering terlewat:

  • alur tanpa kupon tetap berjalan (baris 41–47) melindungi parameter bawaan ''. Lookup KUPON[''] menghasilkan undefined, dan ?? 0 memilih tanpa potongan — regresi yang muncul bila seseorang menghapus nilai bawaan itu tanpa sadar.
  • alur dengan kupon MEMBER15 di keranjang Siti (baris 49–56) menguji jalur data kedua dengan angka yang dihitung terpisah: diskon 34650, sisa 196350, pajak 21599, total 217949. Angka kedua yang benar-benar independen menutup celah klasik: uji yang hanya memakai satu set angka bisa lolos karena angka itu kebetulan bekerja.
  • keranjang kosong menghasilkan total nol (baris 58–68) memastikan seluruh rantai tetap konsisten pada data kosong — semua nilai nol, tanpa NaN yang menyusup dari pembagian atau pembulatan.

Kapan Uji Integration Ditambahkan #

Tiga pertanyaan pemandu sebelum menulis uji integration:

  • Apakah unit-unitnya benar-benar bergantung satu sama lain? Tiga fungsi berbeda yang tidak pernah saling dipanggil tidak perlu diuji bersama.
  • Apakah ada keputusan urutan atau pemakaian data bersama? Aturan “pajak dari harga setelah diskon” adalah keputusan urutan — tipe keputusan yang paling sering salah dan paling layak dikunci.
  • Apakah kegagalannya sulit dijelaskan oleh uji unit? Bila semua unit lulus tetapi gejala masih ada, celahnya ada di sambungan — di situlah uji integration berada.

Satu batasan juga perlu dipahami: uji integration yang gagal lebih sulit dilokalisasi. Pesan kegagalannya menyebut keluaran akhir yang salah, bukan unit pelakunya — karena itu uji unit tetap ditulis lebih dulu, dan uji integration mengikuti sebagai pengaman kedua. Ingat pula posisinya di piramide dari chapter pertama: lebih mahal daripada unit, lebih sedikit jumlahnya, dan tidak menggantikan uji unit di mana pun.

Ada satu lapisan lagi yang bahkan lebih tinggi: memastikan seluruh aplikasi berjalan di browser sungguhan, dari mengetik di kolom pencarian sampai halaman pembayaran. Lapisan E2E itulah topik chapter berikutnya.