Kegagalan Mock Statis dan Risiko Breaking Changes pada Mobile
Aplikasi mobile memiliki karakteristik distribusi yang berbeda dari web: klien tidak dapat diperbarui secara instan. Sekali binari APK atau IPA terpasang pada perangkat pengguna, versi tersebut akan terus mengonsumsi backend API sampai pengguna memilih untuk melakukan pembaruan via app store.
Pengujian unit standar di React Native umumnya mengandalkan jest.mock() atau fixture JSON statis. Pendekatan ini memiliki kelemahan mendasar: API drift. Jika tim backend mengubah nama field, menghapus properti, atau mengubah format tanggal, unit test pada sisi mobile tetap berstatus passed karena membaca mock statis lama. Hasilnya, breaking change baru terdeteksi saat runtime di production.
Consumer-Driven Contract Testing (CDCT) membalik paradigma integrasi. Aplikasi mobile (consumer) mendefinisikan kontrak eksplisit mengenai struktur payload dan status HTTP yang dibutuhkan. Kontrak ini diekspor ke dalam format standar (Pact JSON) untuk kemudian divalidasi oleh backend (provider) sebelum backend melakukan deployment.
Arsitektur Pact Consumer di React Native
Pact JS tidak memerlukan bridge native React Native, Metro bundler, Android emulator, maupun iOS simulator. Kontrak diuji pada level network client menggunakan Node.js runtime di dalam runner Jest.
Pisahkan konfigurasi Jest khusus contract testing untuk menghindari konflik dengan modul React Native preset (seperti mock native modules atau runtime JSX). Buat file konfigurasi terpisah:
// jest.pact.config.js
module.exports = {
testEnvironment: 'node',
testMatch: ['**/*.contract.test.ts'],
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
testTimeout: 30000,
};Instal dependensi yang dibutuhkan:
npm install --save-dev @pact-foundation/pact @types/jest ts-jest jestImplementasi Consumer Test Menggunakan PactV3
Gunakan class PactV3 dari library @pact-foundation/pact. Hindari pencocokan nilai literal (exact value matching) pada payload response. Gunakan Matchers agar pengujian fokus pada tipe data dan struktur skema.
1. Service Layer Klien Mobile
Berikut adalah service HTTP sederhana yang akan diuji:
// src/api/user.ts
export interface UserProfile {
id: string;
name: string;
email: string;
status: 'ACTIVE' | 'SUSPENDED';
}
export const fetchUserProfile = async (baseUrl: string, token: string): Promise<UserProfile> => {
const res = await fetch(`${baseUrl}/api/v1/users/me`, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
});
if (!res.ok) {
throw new Error(`Request failed with status ${res.status}`);
}
return res.json();
};2. Penulisan Contract Test
Test berikut menjalankan mock server lokal Pact, mengeksekusi fungsi klien ke mock server tersebut, dan memvalidasi interaksi HTTP.
// src/api/__tests__/user.contract.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
import { fetchUserProfile } from '../user';
const { like, uuid, string } = MatchersV3;
const provider = new PactV3({
consumer: 'MobileApp-ReactNative',
provider: 'UserBackendService',
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: 'warn',
});
describe('User API Contract', () => {
it('menerima payload profil user yang valid dari backend', async () => {
// Definisi ekspektasi interaksi
provider
.given('pengguna terautentikasi dan aktif')
.uponReceiving('permintaan GET untuk detail akun')
.withRequest({
method: 'GET',
path: '/api/v1/users/me',
headers: {
Authorization: 'Bearer valid-jwt-token',
Accept: 'application/json',
},
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: uuid('e9b5c3e0-6b6f-4a6c-9c3f-981a8ef1c2e4'),
name: string('Budi Santoso'),
email: string('[email protected]'),
status: like('ACTIVE'),
},
});
// Eksekusi request nyata ke mock server Pact
await provider.executeTest(async (mockServer) => {
const result = await fetchUserProfile(mockServer.url, 'valid-jwt-token');
expect(result.id).toBeDefined();
expect(result.name).toBe('Budi Santoso');
expect(result.status).toBe('ACTIVE');
});
});
});Catatan Teknis: Matcher like() memastikan provider mengirim tipe data yang sesuai (misal: string), tanpa memaksa backend mengembalikan nilai literal 'ACTIVE' saat verifikasi provider berjalan.Otomasi Pipeline CI: Publish dan can-i-deploy
Menjalankan test secara lokal hanya menghasilkan artefak JSON di folder pacts/. Untuk mengunci kompatibilitas antara mobile build dan backend, integrasikan artefak tersebut ke Pact Broker.
Tahapan CI/CD:
- Jalankan Test: Bentuk file contract JSON.
- Publish Contract: Unggah artefak dengan metadata Git SHA dan branch.
- Verifikasi Deployability: Jalankan perintah
can-i-deploysebelum melakukan build biner aplikasi (misal: via Fastlane atau EAS).
Berikut contoh implementasi GitHub Actions workflow:
# .github/workflows/pact-consumer.yml
name: Pact Consumer CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
GIT_COMMIT: ${{ github.sha }}
GIT_BRANCH: ${{ github.ref_name }}
jobs:
run-contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Run Pact Contract Tests
run: npx jest --config jest.pact.config.js
- name: Publish Contract to Pact Broker
run: |
npx @pact-foundation/pact-cli pact-broker publish pacts \
--consumer-app-version $GIT_COMMIT \
--branch $GIT_BRANCH \
--broker-base-url $PACT_BROKER_BASE_URL \
--broker-token $PACT_BROKER_TOKEN
- name: Check Deployability to Production
run: |
npx @pact-foundation/pact-cli pact-broker can-i-deploy \
--pacticipant MobileApp-ReactNative \
--version $GIT_COMMIT \
--to-environment production \
--broker-base-url $PACT_BROKER_BASE_URL \
--broker-token $PACT_BROKER_TOKENAturan Penerapan (Trade-offs & Best Practices)
- Hindari Testing Functional State di Pact: Contract testing bukan pengganti End-to-End (E2E) testing. Jangan uji validasi UI atau business logic kompleks di sini. Batasi hanya pada skema komunikasi HTTP.
- Gunakan State Provider Seperlunya: Klausul
provider.given()harus didelegasikan ke provider untuk menyiapkan test data (mocking database backend). Jaga deklarasi state sesederhana mungkin. - Pemisahan Environment Script: Jangan gabungkan runner Pact ke dalam script
npm testharian jika developer backend belum menerapkan Pact verifier di repo mereka, karenacan-i-deployakan memblokir pipeline jika provider belum memverifikasi kontrak tersebut.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!