SNAPSHOT-TESTING API-TESTING DATA-CONTRACTS BACKEND-TESTING API-DEVELOPMENT TESTING-STRATEGY MICROSERVICES CI-CD DEVELOPER-EXPERIENCE DATA-INTEGRITY QUALITY-ASSURANCE

Snapshot Testing untuk API dan Data Contracts: Memastikan Konsistensi Backend Anda

⏱️ 12 menit baca
👨‍💻

Snapshot Testing untuk API dan Data Contracts: Memastikan Konsistensi Backend Anda

1. Pendahuluan

Sebagai developer web, kita semua tahu betapa krusialnya API dan data contracts. Mereka adalah “jembatan” yang menghubungkan frontend dengan backend, antar-microservices, atau bahkan dengan sistem eksternal. Namun, seiring berjalannya waktu dan evolusi fitur, API kita cenderung berubah. Perubahan kecil pada respons JSON, urutan field, atau bahkan tipe data bisa jadi mimpi buruk jika tidak terdeteksi sejak dini. Frontend bisa error, integrasi antar-layanan bisa putus, dan pengalaman pengguna pun terganggu.

Di sinilah Snapshot Testing hadir sebagai penyelamat. Mungkin kamu sudah familiar dengan snapshot testing untuk komponen UI, di mana kita membandingkan output rendering komponen dengan “snapshot” yang tersimpan. Nah, konsep yang sama persis bisa kita terapkan untuk API dan data contracts. Ini adalah cara yang efisien untuk memastikan bahwa respons API Anda tetap konsisten dan sesuai harapan, mencegah breaking changes yang tidak disengaja, dan menjaga integritas data di seluruh sistem.

Artikel ini akan membawa kamu menyelami bagaimana snapshot testing dapat menjadi perisai tambahan untuk API dan data contracts di backend Anda. Kita akan membahas mengapa ini penting, cara mengimplementasikannya, dan strategi terbaik untuk menjadikannya bagian tak terpisahkan dari workflow pengembangan Anda.

2. Apa Itu Snapshot Testing (dalam Konteks API)?

Pada dasarnya, snapshot testing adalah metode pengujian di mana output dari sebuah fungsi atau komponen (dalam kasus ini, respons API) dibandingkan dengan sebuah “snapshot” atau referensi yang sudah tersimpan sebelumnya.

📌 Konsep Dasar:

  1. Eksekusi Tes: Anda menjalankan tes yang memanggil API Anda.
  2. Pembuatan Snapshot: Pada eksekusi pertama, respons API akan disimpan sebagai file snapshot (biasanya dalam format JSON atau teks). Ini adalah “kondisi yang diharapkan”.
  3. Perbandingan di Masa Depan: Pada eksekusi tes berikutnya, respons API yang baru akan dibandingkan dengan snapshot yang sudah ada.
    • Jika sama, tes lulus.
    • Jika berbeda, tes gagal, menandakan ada perubahan. Anda kemudian dapat meninjau perubahan tersebut dan memutuskan apakah itu perubahan yang disengaja (dan memperbarui snapshot) atau bug (dan memperbaikinya).

Kenapa ini berbeda dari Unit/Integration Testing biasa? Unit atau integration testing biasanya berfokus pada asersi spesifik (expect(response.status).toBe(200), expect(response.body.data.name).toBe('John Doe')). Snapshot testing, di sisi lain, menangkap seluruh struktur dan konten dari respons API. Ini sangat berguna untuk:

Contoh Kasus Nyata: Bayangkan Anda memiliki API /users/{id} yang mengembalikan detail pengguna. Seiring waktu, tim lain menambahkan field lastLoginAt atau mengubah tipe phoneNumber dari string menjadi objek. Tanpa snapshot testing, perubahan ini mungkin lolos dari pengujian unit yang hanya memeriksa name dan email. Dengan snapshot testing, setiap perubahan pada struktur respons akan langsung memicu kegagalan tes, memaksa developer untuk meninjau dan menyetujui perubahan tersebut.

3. Persiapan: Tools yang Dibutuhkan

Untuk mengimplementasikan snapshot testing di backend, Anda memerlukan framework testing yang mendukung fitur ini. Yang paling populer di ekosistem JavaScript/TypeScript adalah Jest atau Vitest. Keduanya memiliki dukungan snapshot testing bawaan yang sangat baik.

Contoh dengan Jest (Node.js/TypeScript):

Pertama, pastikan Anda memiliki Jest terinstal di proyek Anda:

npm install --save-dev jest supertest
# atau
yarn add --dev jest supertest

supertest adalah library yang sangat berguna untuk menguji HTTP endpoint di Node.js.

Jika Anda menggunakan TypeScript, Anda juga perlu:

npm install --save-dev @types/jest @types/supertest ts-jest
# atau
yarn add --dev @types/jest @types/supertest ts-jest

Dan konfigurasikan jest.config.js atau jest.config.ts Anda:

// jest.config.js
module.exports = {
  preset: 'ts-jest', // Jika menggunakan TypeScript
  testEnvironment: 'node',
  // Direktori di mana snapshot akan disimpan
  snapshotResolver: './snapshotResolver.js',
};

Untuk snapshotResolver.js (opsional, tapi bagus untuk struktur):

// snapshotResolver.js
const path = require('path');

module.exports = {
  // resolves from test to snapshot path
  resolveSnapshotPath: (testPath, snapshotExtension) =>
    testPath.replace('__tests__', '__snapshots__') + snapshotExtension,

  // resolves from snapshot to test path
  resolveTestPath: (snapshotPath, snapshotExtension) =>
    snapshotPath.replace('__snapshots__', '__tests__').slice(0, -snapshotExtension.length),

  // example of how to use `parsePath`
  parsePath: (snapshotPath, snapshotExtension) => ({
    dirname: path.dirname(snapshotPath),
    basename: path.basename(snapshotPath, snapshotExtension),
    extension: snapshotExtension,
  }),
};

Ini akan membuat snapshot tersimpan di folder __snapshots__ yang terpisah, bukan di samping file tes. Lebih rapi!

4. Implementasi Dasar: Langkah demi Langkah

Mari kita buat contoh sederhana. Misalkan kita punya aplikasi Express.js:

// src/app.ts
import express from 'express';

const app = express();
app.use(express.json());

interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

const users: User[] = [
  { id: 1, name: 'Budi Santoso', email: 'budi@example.com', createdAt: new Date().toISOString() },
  { id: 2, name: 'Siti Aminah', email: 'siti@example.com', createdAt: new Date().toISOString() },
];

app.get('/api/users/:id', (req, res) => {
  const userId = parseInt(req.params.id, 10);
  const user = users.find(u => u.id === userId);

  if (user) {
    res.json(user);
  } else {
    res.status(404).json({ message: 'User not found' });
  }
});

app.get('/api/users', (req, res) => {
  res.json(users);
});

export default app;

Sekarang, kita buat file tesnya:

// __tests__/user.api.test.ts
import request from 'supertest';
import app from '../src/app'; // Asumsikan app.ts diekspor sebagai default

describe('User API Snapshot Tests', () => {
  // Test untuk mengambil satu user
  test('GET /api/users/:id should match snapshot for a specific user', async () => {
    const response = await request(app).get('/api/users/1');

    expect(response.statusCode).toBe(200);
    // ✅ Ini dia magic-nya: membandingkan body respons dengan snapshot
    // Penting: Hapus field dinamis seperti 'createdAt' atau 'id' jika selalu berubah
    // atau gunakan teknik serialisasi kustom (lihat di bagian Best Practices)
    const { createdAt, ...userWithoutDynamicFields } = response.body;
    expect(userWithoutDynamicFields).toMatchSnapshot();
  });

  // Test untuk mengambil semua user
  test('GET /api/users should match snapshot for all users', async () => {
    const response = await request(app).get('/api/users');

    expect(response.statusCode).toBe(200);
    // Untuk array, kita mungkin perlu memfilter field dinamis di setiap item
    const usersWithoutDynamicFields = response.body.map((user: any) => {
      const { createdAt, ...rest } = user;
      return rest;
    });
    expect(usersWithoutDynamicFields).toMatchSnapshot();
  });

  // Test untuk error response
  test('GET /api/users/:id for non-existent user should match snapshot', async () => {
    const response = await request(app).get('/api/users/999');

    expect(response.statusCode).toBe(404);
    expect(response.body).toMatchSnapshot();
  });
});

Cara Menjalankan Tes:

npx jest

Pada eksekusi pertama, Jest akan membuat file snapshot di __snapshots__/user.api.test.ts.snap dengan konten seperti ini:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`User API Snapshot Tests GET /api/users/:id for non-existent user should match snapshot 1`] = `
Object {
  "message": "User not found",
}
`;

exports[`User API Snapshot Tests GET /api/users/:id should match snapshot for a specific user 1`] = `
Object {
  "email": "budi@example.com",
  "id": 1,
  "name": "Budi Santoso",
}
`;

exports[`User API Snapshot Tests GET /api/users should match snapshot for all users 1`] = `
Array [
  Object {
    "email": "budi@example.com",
    "id": 1,
    "name": "Budi Santoso",
  },
  Object {
    "email": "siti@example.com",
    "id": 2,
    "name": "Siti Aminah",
  },
]
`;

🎉 Selamat! Anda baru saja membuat snapshot testing pertama Anda untuk API backend.

5. Mengelola Snapshot: Kapan Update, Kapan Fail

Setelah snapshot dibuat, setiap kali Anda menjalankan npx jest, Jest akan membandingkan respons API yang baru dengan snapshot yang tersimpan.

⚠️ Pentingnya Review Snapshot: Ketika tes gagal karena perbedaan snapshot, Anda memiliki dua pilihan:

  1. Perbaiki Kode: Jika perubahan pada API bukan disengaja (misalnya, bug, salah ketik, atau perubahan yang tidak seharusnya), Anda harus memperbaiki kode API Anda agar kembali sesuai dengan snapshot yang diharapkan.

  2. Perbarui Snapshot: Jika perubahan pada API adalah disengaja (misalnya, penambahan field baru yang memang dibutuhkan, perbaikan data contract), Anda harus meninjau diff yang ditampilkan Jest. Jika Anda setuju dengan perubahannya, Anda bisa memperbarui snapshot dengan menjalankan:

    npx jest --updateSnapshot
    # atau singkatnya
    npx jest -u

    Ini akan menulis ulang file snapshot dengan respons API yang baru. Pastikan untuk selalu meninjau perubahan snapshot sebelum mengcommitnya. Memperbarui snapshot secara “buta” sama saja dengan tidak menguji sama sekali!

🎯 Kapan Tidak Menggunakan Snapshot Testing: Snapshot testing tidak cocok untuk semua skenario:

6. Strategi Lanjutan dan Best Practices

Untuk memaksimalkan manfaat snapshot testing pada API dan data contracts, terapkan beberapa strategi lanjutan ini:

a. Filtering dan Normalisasi Data Dinamis

Seperti yang terlihat pada contoh createdAt, field yang berubah setiap kali tes dijalankan akan membuat snapshot selalu gagal. Anda bisa memfilter field ini atau menggantinya dengan nilai statis ('MOCK_DATE') sebelum membuat snapshot.

// __tests__/user.api.test.ts (contoh dengan filtering yang lebih canggih)
import request from 'supertest';
import app from '../src/app';

// Fungsi utilitas untuk membersihkan objek atau array objek
const cleanDynamicFields = (data: any) => {
  if (Array.isArray(data)) {
    return data.map(item => cleanDynamicFields(item));
  }
  if (typeof data === 'object' && data !== null) {
    const newData = { ...data };
    delete newData.createdAt; // Hapus field dinamis
    // Jika ada ID yang dihasilkan otomatis dan tidak relevan untuk snapshot
    // if (newData.id && typeof newData.id === 'number' && newData.id > 100) {
    //   newData.id = 'DYNAMIC_ID';
    // }
    return newData;
  }
  return data;
};

describe('User API Snapshot Tests - Advanced', () => {
  test('GET /api/users/:id should match snapshot after cleaning dynamic fields', async () => {
    const response = await request(app).get('/api/users/1');
    expect(response.statusCode).toBe(200);
    expect(cleanDynamicFields(response.body)).toMatchSnapshot();
  });

  test('GET /api/users should match snapshot after cleaning dynamic fields for all users', async () => {
    const response = await request(app).get('/api/users');
    expect(response.statusCode).toBe(200);
    expect(cleanDynamicFields(response.body)).toMatchSnapshot();
  });
});

💡 Tips: Anda juga bisa menggunakan custom serializer Jest untuk menangani ini secara global.

b. Struktur Snapshot yang Jelas

Pastikan nama tes Anda deskriptif sehingga snapshot yang dihasilkan mudah dipahami. Gunakan blok describe dan test untuk mengelompokkan tes secara logis. Ini akan membantu saat meninjau diff atau mencari snapshot tertentu.

c. Integrasi CI/CD

Ini adalah poin terpenting! Snapshot testing paling efektif jika diintegrasikan ke dalam pipeline CI/CD Anda.

# .github/workflows/ci.yml (Contoh GitHub Actions)
name: CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test # Atau npx jest
      - name: Check for uncommitted snapshot changes
        run: git diff --exit-code

Dengan git diff --exit-code, pipeline akan gagal jika ada perubahan snapshot yang belum di-commit. Ini memaksa developer untuk memperbarui dan mereview snapshot secara lokal sebelum push.

d. Menggabungkan dengan Contract Testing

Snapshot testing sangat baik untuk memastikan internal consistency dari API Anda. Namun, untuk memastikan API Anda sesuai dengan harapan klien (frontend atau microservice lain), Anda bisa menggabungkannya dengan Consumer-Driven Contract Testing (CDC).

CDC berfokus pada kontrak yang disepakati antara produsen dan konsumen, sementara snapshot testing lebih pada “apa yang saat ini dikembalikan” oleh produsen. Keduanya saling melengkapi.

e. Hati-hati dengan Ukuran Snapshot

Jika respons API sangat besar atau memiliki banyak variasi, file snapshot bisa menjadi sangat besar dan sulit dikelola atau direview. Pertimbangkan untuk:

Kesimpulan

Snapshot testing adalah senjata ampuh dalam arsenal developer untuk menjaga kualitas dan konsistensi API serta data contracts. Dengan otomatisasi deteksi perubahan pada respons API, Anda dapat:

Meskipun bukan peluru perak untuk semua masalah testing, ketika diterapkan dengan bijak, snapshot testing dapat menjadi fondasi yang kokoh untuk membangun backend yang lebih stabil, andal, dan mudah di-maintain. Jadi, tunggu apa lagi? Integrasikan snapshot testing ke dalam workflow Anda dan rasakan manfaatnya!

🔗 Baca Juga