Menguasai API test automation: request frameworks, contract testing, mocking, dan membangun API test suite yang comprehensif

Setelah di episode 7 kita mempelajari POM untuk web testing, pada episode ini kita beralih ke layer yang sama pentingnya: API testing. API testing lebih cepat, lebih stabil, dan lebih murah dibanding E2E testing.
import { test, expect } from '@playwright/test';
test.describe('Users API', () => {
test('GET /api/users returns list of users', async ({ request }) => {
const response = await request.get('/api/users');
expect(response.ok()).toBeTruthy();
const users = await response.json();
expect(Array.isArray(users)).toBeTruthy();
expect(users.length).toBeGreaterThan(0);
});
test('POST /api/users creates user', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: 'John Doe',
email: 'john@test.com'
}
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user.name).toBe('John Doe');
expect(user.email).toBe('john@test.com');
});
test('GET /api/users/:id returns user', async ({ request }) => {
const response = await request.get('/api/users/1');
expect(response.ok());
const user = await response.json();
expect(user).toHaveProperty('id', 1);
});
});// fixtures/api.ts
import { test as base } from '@playwright/test';
type ApiFixtures = {
apiRequest: any;
};
export const test = base.extend<ApiFixtures>({
apiRequest: async ({ request }, use) => {
const apiRequest = {
get: (url: string) => request.get(`http://localhost:3000${url}`),
post: (url: string, data: any) =>
request.post(`http://localhost:3000${url}`, { data }),
put: (url: string, data: any) =>
request.put(`http://localhost:3000${url}`, { data }),
delete: (url: string) => request.delete(`http://localhost:3000${url}`),
};
await use(apiRequest);
},
});// Contoh contract validation
test('user response matches contract', async ({ request }) => {
const response = await request.get('/api/users/1');
const user = await response.json();
// Validate contract
expect(user).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
email: expect.stringMatching(/@/),
createdAt: expect.any(String),
});
});// Mock API response
test('handle API error', async ({ page }) => {
// Intercept API call
await page.route('**/api/users', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/users');
await expect(page.locator('.error')).toContainText('Failed to load');
});
// Mock dengan delay
test('handle slow API', async ({ page }) => {
await page.route('**/api/users', async route => {
await new Promise(resolve => setTimeout(resolve, 3000));
await route.continue();
});
});Note
API testing harus menjadi tulang punggung test strategy — lebih cepat dan stabil dari E2E. Gunakan E2E hanya untuk critical user flows.
test.describe('Products API', () => {
test('GET /api/products', async ({ request }) => {
const response = await request.get('/api/products');
expect(response.ok());
const products = await response.json();
expect(products.length).toBeGreaterThan(0);
});
test('GET /api/products/:id', async ({ request }) => {
const response = await request.get('/api/products/1');
expect(response.ok());
const product = await response.json();
expect(product).toHaveProperty('name');
});
test('GET /api/products/:id - not found', async ({ request }) => {
const response = await request.get('/api/products/99999');
expect(response.status()).toBe(404);
});
});request fixture untuk HTTP methods.page.route() untuk intercept dan mock API responses.Di episode 9 selanjutnya kita akan membahas E2E testing strategy — best practices, test isolation, dan mengurangi flakiness. Sampai jumpa di episode 9!