Saat tim menyederhanakan pipeline gambar dengan memindahkan resize, crop, format conversion, atau caching ke CDN/image service, risiko utama bukan hanya bug visual. Regresi sering muncul sebagai URL transformasi yang berubah diam-diam, fallback yang salah, header cache yang tidak konsisten, atau perilaku berbeda antara environment test dan produksi.

Strategi uji pipeline gambar untuk cegah regresi CDN dan cache harus memperlakukan image delivery sebagai kontrak yang bisa diverifikasi. Artinya, Anda tidak cukup menguji bahwa gambar “muncul”, tetapi juga memastikan URL transformasi stabil, respons fallback benar, dimensi dan format sesuai ekspektasi, cache header valid, dan dependency eksternal tidak membuat test suite menjadi flaky.

Konteks ini relevan ketika tim mengadopsi CDN/image service untuk menggantikan pipeline internal yang lebih kompleks. Keuntungan operasionalnya besar, tetapi pengujian perlu didesain ulang. Artikel ini berfokus pada pola implementasi yang praktis untuk tim web/backend.

Apa yang Berubah Saat Pipeline Gambar Dipindah ke CDN

Pada pipeline lama, aplikasi mungkin mengontrol hampir semua tahap: upload, normalisasi, resize, penyimpanan turunan gambar, invalidasi cache, dan delivery. Saat transformasi dipindah ke CDN atau image service, aplikasi biasanya hanya bertanggung jawab atas:

  • membentuk URL transformasi,
  • menentukan parameter default,
  • memilih fallback saat origin bermasalah,
  • dan memverifikasi hasil delivery di level HTTP.

Konsekuensinya, banyak bug tidak lagi terlihat sebagai exception aplikasi. Bug bisa muncul sebagai:

  • rasio gambar salah karena parameter crop berubah urutan atau nama,
  • WebP/AVIF tidak terkirim meski browser mendukung,
  • cache miss berulang karena query string tidak stabil,
  • placeholder 404 ter-cache terlalu lama,
  • atau test integration gagal sesekali karena service eksternal lambat.

Karena itu, desain pengujian perlu dibagi berdasarkan lapisan tanggung jawab, bukan hanya berdasarkan endpoint aplikasi.

Prinsip Dasar: Uji Kontrak, Bukan Hanya Tampilan

Kesalahan umum adalah mengandalkan screenshot visual atau pemeriksaan manual. Itu berguna untuk regresi UI, tetapi kurang presisi untuk pipeline gambar. Untuk mencegah regresi, yang lebih penting adalah menguji kontrak berikut:

  • Kontrak URL transformasi: input tertentu harus menghasilkan URL yang stabil dan terduplikasi dengan cara yang sama.
  • Kontrak fallback: saat sumber asli hilang, timeout, atau tidak valid, sistem harus mengembalikan placeholder atau jalur fallback yang disepakati.
  • Kontrak output: dimensi, format, atau content-type harus sesuai aturan.
  • Kontrak cache: header seperti Cache-Control, ETag, Vary, atau indikasi cache hit/miss harus terverifikasi.
  • Kontrak reliabilitas test: test tidak boleh bergantung penuh pada availability jaringan publik untuk validasi yang seharusnya lokal.

Dengan pendekatan ini, Anda bisa memisahkan test yang cepat dan deterministik dari test yang memang perlu memukul service nyata.

Pemisahan Jenis Test: Unit, Integration, dan Smoke Test Pascadeploy

1. Unit test: validasi pembentukan URL dan aturan bisnis

Unit test harus menjadi lapisan utama karena cepat, murah, dan stabil. Fokusnya bukan memanggil CDN, tetapi memastikan aplikasi membangun URL dengan benar.

Yang diuji di level ini:

  • mapping parameter internal ke parameter transformasi,
  • default width/height/quality,
  • normalisasi urutan query string atau path segment,
  • penghilangan parameter kosong atau tidak valid,
  • pemilihan format berdasarkan capability atau policy aplikasi,
  • dan logika fallback.

Contoh fungsi pembentuk URL:

function buildImageUrl({ baseUrl, path, width, height, format, quality }) {
  const params = new URLSearchParams();

  if (width) params.set('w', String(width));
  if (height) params.set('h', String(height));
  if (format) params.set('fmt', format);
  if (quality) params.set('q', String(quality));

  const query = params.toString();
  return query ? `${baseUrl}/${path}?${query}` : `${baseUrl}/${path}`;
}

Contoh unit test kontrak URL:

describe('buildImageUrl', () => {
  it('membentuk URL transformasi secara stabil', () => {
    const url = buildImageUrl({
      baseUrl: 'https://img.example-cdn.com',
      path: 'uploads/avatar.jpg',
      width: 200,
      height: 200,
      format: 'webp',
      quality: 80,
    });

    expect(url).toBe(
      'https://img.example-cdn.com/uploads/avatar.jpg?w=200&h=200&fmt=webp&q=80'
    );
  });

  it('tidak menyertakan parameter kosong', () => {
    const url = buildImageUrl({
      baseUrl: 'https://img.example-cdn.com',
      path: 'uploads/banner.png',
      width: 1200,
    });

    expect(url).toBe('https://img.example-cdn.com/uploads/banner.png?w=1200');
  });
});

Jika tim Anda sering mengubah provider, simpan kontrak ini di satu modul adapter. Hindari menyebar pembentukan URL di template, controller, serializer, dan frontend sekaligus. Semakin banyak titik pembentuk URL, semakin sulit mencegah regresi.

2. Integration test: verifikasi perilaku HTTP dan output nyata

Integration test dipakai untuk memastikan service image benar-benar merespons sesuai ekspektasi. Di sini Anda boleh memukul service nyata, tetapi cakupannya kecil dan terarah.

Yang layak diuji di integration test:

  • respons sukses untuk aset fixture yang stabil,
  • status code untuk aset tidak ditemukan,
  • Content-Type hasil transformasi,
  • header cache penting,
  • dimensi gambar output,
  • dan fallback saat origin gagal.

Contoh test sederhana berbasis HTTP:

import fetch from 'node-fetch';
import sizeOf from 'image-size';

it('mengembalikan gambar webp 400x300 dengan cache header', async () => {
  const url = 'https://img.example-cdn.com/fixtures/hero.jpg?w=400&h=300&fmt=webp';
  const res = await fetch(url);

  expect(res.status).toBe(200);
  expect(res.headers.get('content-type')).toContain('image/webp');
  expect(res.headers.get('cache-control')).toBeTruthy();

  const buffer = Buffer.from(await res.arrayBuffer());
  const dimensions = sizeOf(buffer);

  expect(dimensions.width).toBe(400);
  expect(dimensions.height).toBe(300);
});

Penting: gunakan fixture gambar yang stabil, kecil, dan berada pada origin yang Anda kontrol. Jangan bergantung pada URL gambar acak dari internet karena itu memperbesar flaky test tanpa memberi nilai tambahan.

3. Smoke test pascadeploy: validasi jalur kritikal di environment nyata

Smoke test pascadeploy bertujuan mendeteksi masalah yang hanya muncul setelah konfigurasi deploy aktif, misalnya rule cache yang salah, domain CDN belum terpasang benar, atau header berubah karena reverse proxy di depan CDN.

Jalankan smoke test pada beberapa URL representatif:

  • gambar publik yang paling sering diakses,
  • satu gambar dengan transform resize,
  • satu gambar fallback,
  • dan satu gambar dengan format negosiasi atau transformasi yang sensitif.

Smoke test sebaiknya memverifikasi:

  • status code,
  • Content-Type,
  • ukuran respons masuk akal,
  • cache header ada,
  • dan tidak terjadi redirect tak terduga.

Jangan membuat smoke test terlalu banyak. Tujuannya bukan menggantikan integration test, melainkan menangkap kegagalan konfigurasi produksi secepat mungkin.

Kontrak URL Transformasi: Lapisan Pertama Pencegahan Regresi

Regresi paling murah untuk dicegah adalah regresi pembentukan URL. Jika aplikasi menghasilkan URL yang salah, semua lapisan di bawahnya akan gagal meskipun CDN berfungsi normal.

Apa yang harus masuk ke test kontrak URL

  • kombinasi parameter umum: width, height, crop, quality, format, dpr, fit, gravity, dan sebagainya sesuai kebutuhan Anda,
  • nilai default bila parameter tidak diberikan,
  • perilaku untuk input tidak valid,
  • encoding path dengan karakter khusus,
  • stabilitas urutan parameter jika memengaruhi cache key,
  • dan kompatibilitas lintas consumer, misalnya backend API dan frontend helper.

Pola yang disarankan

Buat satu adapter internal seperti ImageUrlBuilder dan jadikan itu satu-satunya jalur pembentukan URL. Jika provider diganti atau aturan transformasi berubah, Anda cukup mengubah adapter dan memperbarui snapshot kontrak.

Catatan: snapshot test berguna untuk string URL, tetapi tetap tambahkan assertion eksplisit untuk bagian penting. Snapshot besar sering lolos saat perubahan kecil sebenarnya berbahaya.

Validasi Fallback: Jangan Asumsikan Origin Selalu Sehat

Banyak tim baru menyadari pentingnya fallback setelah migrasi ke image service. Begitu origin image timeout, mengembalikan 404, atau file rusak, pengalaman pengguna bisa anjlok jika tidak ada perilaku fallback yang jelas.

Skenario fallback yang perlu diuji

  • origin mengembalikan 404,
  • origin lambat atau timeout,
  • file ada tetapi bukan gambar valid,
  • transformasi tidak didukung untuk format tertentu,
  • dan path kosong atau null dari aplikasi.

Contoh aturan fallback yang baik

  • avatar pengguna tanpa gambar memakai placeholder lokal/CDN yang stabil,
  • thumbnail artikel yang gagal dimuat kembali ke gambar default,
  • halaman detail tetap menerima URL gambar valid meskipun aset utama hilang,
  • error upstream tidak menghasilkan redirect loop.

Unit test cukup untuk memastikan aplikasi memilih fallback yang benar. Integration test dipakai untuk memastikan URL fallback itu benar-benar hidup dan memiliki header cache yang sesuai.

it('menggunakan placeholder saat imagePath kosong', () => {
  const url = resolveArticleImageUrl({ imagePath: null });
  expect(url).toBe('https://img.example-cdn.com/placeholders/article-default.png');
});

Snapshot Dimensi dan Format: Verifikasi Hasil, Bukan Sekadar 200 OK

Status 200 tidak menjamin transformasi benar. Gambar bisa terkirim dengan dimensi salah, format berbeda, atau ukuran file membesar karena transformasi gagal diterapkan.

Apa yang sebaiknya disnapshot

  • dimensi output,
  • Content-Type,
  • opsional: ukuran file dalam rentang yang masuk akal,
  • opsional: hash untuk fixture yang sangat stabil.

Untuk sebagian besar tim, snapshot metadata lebih aman daripada snapshot biner penuh. Snapshot biner rentan berubah karena optimisasi encoder, metadata EXIF, atau update kecil di provider meski hasil visual masih dapat diterima.

Contoh pendekatan metadata snapshot:

it('output thumbnail tetap sesuai kontrak', async () => {
  const res = await fetch('https://img.example-cdn.com/fixtures/card.jpg?w=320&h=180&fmt=webp');
  const buffer = Buffer.from(await res.arrayBuffer());
  const dimensions = sizeOf(buffer);

  expect({
    status: res.status,
    contentType: res.headers.get('content-type'),
    width: dimensions.width,
    height: dimensions.height,
  }).toMatchInlineSnapshot(`
    {
      "contentType": "image/webp",
      "height": 180,
      "status": 200,
      "width": 320,
    }
  `);
});

Jika provider melakukan format negotiation berdasarkan header Accept, kirim header itu secara eksplisit di test. Jika tidak, hasil bisa berbeda antar runner atau environment.

Verifikasi Cache Header: Sumber Regresi yang Sering Terlewat

Salah satu manfaat utama CDN adalah caching. Namun banyak tim hanya menguji gambar dapat diakses, tanpa memverifikasi apakah cache benar-benar bekerja seperti yang diharapkan.

Header yang umum diperiksa

  • Cache-Control untuk kebijakan TTL,
  • ETag atau validator serupa bila dipakai,
  • Vary jika format bergantung pada Accept,
  • header indikasi cache hit/miss dari CDN jika tersedia,
  • dan kadang Age untuk observasi, bukan assertion keras.

Karena implementasi header bisa berbeda antar provider, uji hal yang benar-benar Anda andalkan secara operasional. Misalnya, daripada memaksa nilai persis semua header, Anda bisa memastikan:

  • Cache-Control ada dan mengandung direktif yang Anda butuhkan,
  • respons fallback tidak ter-cache terlalu lama,
  • gambar publik punya TTL lebih panjang daripada placeholder error sementara.

Contoh assertion yang tidak terlalu rapuh:

it('menyertakan kebijakan cache untuk asset publik', async () => {
  const res = await fetch('https://img.example-cdn.com/fixtures/logo.png');
  const cacheControl = res.headers.get('cache-control') || '';

  expect(res.status).toBe(200);
  expect(cacheControl).toContain('max-age=');
});

Hindari assertion yang terlalu spesifik pada header observasional seperti nilai exact Age, karena itu mudah berubah dan tidak selalu merefleksikan bug.

Mengurangi Flaky Test dari Dependency Eksternal

Begitu test memukul CDN atau image service sungguhan, Anda berurusan dengan latensi jaringan, rate limit, warm/cold cache, dan sesekali gangguan provider. Tanpa strategi yang tepat, suite akan menjadi tidak stabil.

Tanda test Anda terlalu bergantung pada faktor eksternal

  • gagal hanya di CI, tetapi lolos di lokal,
  • gagal acak dengan timeout,
  • hasil berbeda antara run pertama dan kedua karena cache,
  • atau test harus di-retry berkali-kali untuk hijau.

Cara mengurangi flaky test

  • Batasi jumlah integration test nyata. Uji hanya jalur kritikal.
  • Gunakan fixture yang Anda kontrol. Simpan aset uji pada origin khusus test.
  • Pisahkan test warm-cache dan cold-cache. Jangan campur ekspektasinya.
  • Tambahkan timeout realistis. Jangan terlalu ketat, tetapi juga jangan membiarkan hang lama.
  • Jangan mengassert header yang tidak stabil.
  • Karantina test provider-spesifik. Jalankan terjadwal jika mahal atau rawan noise.

Kapan perlu mock vs hit service nyata

Gunakan mock bila yang diuji adalah logika aplikasi:

  • pembentukan URL,
  • pemilihan preset transformasi,
  • fallback berdasarkan kondisi domain bisnis,
  • perilaku komponen frontend ketika URL tersedia/tidak tersedia.

Hit service nyata bila yang diuji adalah kontrak lintas boundary:

  • format output aktual,
  • dimensi hasil transformasi,
  • cache header,
  • respons terhadap origin error,
  • dan konfigurasi domain/CDN setelah deploy.

Aturan praktisnya: jika bug bisa terjadi walau kode aplikasi benar, Anda memerlukan setidaknya sedikit test ke service nyata.

Contoh Matriks Test Case yang Layak Dimiliki

Berikut contoh test case minimal yang berguna untuk tim web/backend:

Unit test

  • URL resize tunggal: input width menghasilkan query/path yang benar.
  • URL resize + format: format default atau override bekerja.
  • Parameter kosong dihilangkan dari URL.
  • Path dengan spasi atau karakter khusus di-encode dengan benar.
  • Preset thumbnail artikel menghasilkan kontrak URL tetap.
  • Jika imagePath null, gunakan placeholder yang benar.

Integration test

  • Fixture JPEG di-transform menjadi WebP dan berdimensi sesuai.
  • Gambar yang tidak ada mengembalikan fallback atau status yang disepakati.
  • Header Cache-Control ada pada aset publik.
  • Jika negosiasi format dipakai, Accept: image/webp menghasilkan content-type yang sesuai.
  • Asset origin yang rusak memicu perilaku fallback, bukan 500 tak terduga.

Smoke test pascadeploy

  • URL CDN utama dapat diakses dari environment produksi.
  • Satu transformasi resize berjalan tanpa redirect loop.
  • Satu placeholder fallback tersedia.
  • Header cache hadir pada satu asset publik.

Checklist CI untuk Pipeline Gambar

Gunakan checklist ini saat menambahkan atau memodifikasi pipeline gambar:

  1. Unit test wajib lulus untuk adapter URL dan fallback resolver.
  2. Snapshot kontrak URL direview jika ada perubahan query/path transformasi.
  3. Fixture test stabil dan berada pada origin yang dikendalikan tim.
  4. Integration test terbatas ke skenario kritikal, bukan semua variasi ukuran.
  5. Assertion header tidak rapuh; fokus pada kontrak penting.
  6. Timeout test eksplisit untuk call eksternal.
  7. Retry dibatasi dan hanya untuk test eksternal tertentu, bukan menutupi bug logika.
  8. Smoke test pascadeploy otomatis dijalankan setelah perubahan konfigurasi CDN/origin.
  9. Alert observabilitas aktif untuk lonjakan 4xx/5xx dan cache miss.
  10. Dokumentasi kontrak URL diperbarui ketika preset atau policy berubah.

Metrik yang Perlu Dipantau Setelah Deploy

Testing saja tidak cukup. Karena pipeline gambar melibatkan sistem eksternal, Anda perlu observabilitas untuk mendeteksi regresi yang lolos dari test.

Metrik operasional yang berguna

  • rasio 2xx/4xx/5xx pada jalur image delivery,
  • cache hit ratio atau indikator hit/miss bila tersedia,
  • latensi respons per endpoint atau kelompok transformasi,
  • distribusi content-type untuk melihat apakah konversi format berhenti bekerja,
  • ukuran respons rata-rata untuk mendeteksi transformasi yang gagal mengecilkan file,
  • jumlah fallback yang dipakai bila aplikasi atau edge dapat mencatatnya.

Jika setelah migrasi ke CDN cache miss naik tajam, penyebabnya sering bukan performa provider, tetapi kontrak URL yang tidak stabil. Misalnya urutan parameter berubah antar service, query string berisi nilai default yang seharusnya dihilangkan, atau frontend dan backend membentuk URL yang sedikit berbeda untuk gambar yang sama.

Kesalahan Umum yang Sering Terjadi

  • Menguji screenshot saja. Masalah cache header dan content negotiation tidak akan terlihat.
  • Semua test memukul CDN nyata. Suite menjadi lambat dan flaky.
  • Tidak punya fixture origin sendiri. Test bergantung pada aset publik yang bisa berubah atau hilang.
  • Assertion terlalu spesifik pada header dinamis. Test gagal tanpa ada regresi nyata.
  • Menganggap 200 OK berarti benar. Dimensi atau format bisa tetap salah.
  • Tidak memisahkan smoke test produksi. Bug konfigurasi baru ketahuan dari laporan pengguna.

Rekomendasi Implementasi yang Realistis

Untuk kebanyakan tim, strategi berikut cukup seimbang:

  1. Buat satu adapter internal untuk semua URL gambar.
  2. Tulis unit test menyeluruh untuk kontrak URL dan fallback.
  3. Tambahkan beberapa integration test dengan fixture stabil untuk memverifikasi output HTTP nyata.
  4. Jalankan smoke test pascadeploy pada jalur kritikal.
  5. Pantau cache hit ratio, error rate, dan content-type distribution setelah rilis.

Pendekatan ini memberi perlindungan berlapis: unit test mencegah bug logika, integration test memverifikasi kontrak dengan service eksternal, dan smoke test menangkap kesalahan konfigurasi nyata di environment deploy.

Jika tim Anda sedang menyederhanakan image pipeline dengan CDN/image service, investasi terbaik bukan membuat test sebanyak mungkin, tetapi menempatkan test yang tepat di lapisan yang tepat. Di situlah strategi uji pipeline gambar benar-benar membantu mencegah regresi CDN dan cache sebelum berdampak ke pengguna.