API Testing & Hybrid Workflows
Here's a secret: the fastest UI test is one that doesn't use the UI when it doesn't have to. Playwright's API testing capabilities let you bypass slow UI operations for setup and teardown.
The Speed Problem
A typical checkout test through the UI:
1. Go to homepage → 2s
2. Search for product → 1s
3. Click product → 1s
4. Add to cart → 2s
5. Go to cart → 1s
6. Fill checkout form → 3s
7. Submit order → 2s
8. Verify confirmation → 1s
───────────────────────────────────
Total: 13s
What if you only care about testing step 7-8? The first 6 steps are just setup.
With API + UI hybrid:1. Create cart via API → 0.1s
2. Add items via API → 0.1s
3. Navigate to checkout → 1s
4. Submit order (UI) → 2s
5. Verify confirmation → 1s
───────────────────────────────────
Total: 4.2s (3x faster)
Playwright Request Context
Basic API Requests
import { test, expect } from '@playwright/test';
test('fetch products via API', async ({ request }) => {
const response = await request.get('https://api.shop.example/products');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const products = await response.json();
expect(products.length).toBeGreaterThan(0);
expect(products[0]).toHaveProperty('name');
expect(products[0]).toHaveProperty('price');
});
POST Requests
test('create order via API', async ({ request }) => {
const response = await request.post('https://api.shop.example/orders', {
data: {
items: [
{ productId: 'SKU-001', quantity: 2 },
{ productId: 'SKU-002', quantity: 1 },
],
shippingAddress: {
street: '123 Main St',
city: 'San Francisco',
state: 'CA',
zip: '94102',
},
},
});
expect(response.ok()).toBeTruthy();
const order = await response.json();
expect(order.orderId).toBeDefined();
expect(order.status).toBe('pending');
});
Request Options
test('authenticated API request', async ({ request }) => {
const response = await request.get('https://api.shop.example/user/orders', {
headers: {
'Authorization': 'Bearer your-token-here',
'Content-Type': 'application/json',
},
params: {
limit: 10,
status: 'completed',
},
timeout: 30000,
});
expect(response.ok()).toBeTruthy();
});
All HTTP Methods
test.describe('Product API', () => {
test('GET - list products', async ({ request }) => {
const response = await request.get('/api/products');
expect(response.ok()).toBeTruthy();
});
test('POST - create product', async ({ request }) => {
const response = await request.post('/api/products', {
data: { name: 'New Product', price: 29.99 },
});
expect(response.status()).toBe(201);
});
test('PUT - update product', async ({ request }) => {
const response = await request.put('/api/products/123', {
data: { name: 'Updated Name', price: 39.99 },
});
expect(response.ok()).toBeTruthy();
});
test('PATCH - partial update', async ({ request }) => {
const response = await request.patch('/api/products/123', {
data: { price: 24.99 },
});
expect(response.ok()).toBeTruthy();
});
test('DELETE - remove product', async ({ request }) => {
const response = await request.delete('/api/products/123');
expect(response.status()).toBe(204);
});
});
API-Based Authentication
The Login Performance Problem
// SLOW: Login via UI for every test
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Welcome')).toBeVisible();
// This takes 3-5 seconds per test
});
Solution 1: API Login + Session Storage
// auth.setup.ts - Run once before all tests
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ request }) => {
// Login via API
const response = await request.post('https://api.shop.example/auth/login', {
data: {
email: 'user@example.com',
password: 'password123',
},
});
expect(response.ok()).toBeTruthy();
// Save session state (cookies, localStorage)
await request.storageState({ path: authFile });
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
// Setup project runs first
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
// Tests use the authenticated state
{
name: 'chromium',
dependencies: ['setup'],
use: {
storageState: 'playwright/.auth/user.json',
},
},
],
});
Solution 2: Fixture-Based Auth
// fixtures/auth.fixture.ts
import { test as base, expect } from '@playwright/test';
import { APIRequestContext } from '@playwright/test';
type AuthFixture = {
authenticatedRequest: APIRequestContext;
authToken: string;
};
export const test = base.extend<AuthFixture>({
authToken: async ({ request }, use) => {
const response = await request.post('/api/auth/login', {
data: {
email: process.env.TEST_USER_EMAIL,
password: process.env.TEST_USER_PASSWORD,
},
});
const { token } = await response.json();
await use(token);
},
authenticatedRequest: async ({ playwright, authToken }, use) => {
const context = await playwright.request.newContext({
extraHTTPHeaders: {
'Authorization': `Bearer ${authToken}`,
},
});
await use(context);
await context.dispose();
},
});
export { expect };
// tests/orders.spec.ts
import { test, expect } from '../fixtures/auth.fixture';
test('can view order history', async ({ authenticatedRequest }) => {
const response = await authenticatedRequest.get('/api/orders');
expect(response.ok()).toBeTruthy();
const orders = await response.json();
expect(orders.length).toBeGreaterThan(0);
});
Hybrid API + UI Testing
The most powerful pattern: combine fast API setup with UI verification.
Pattern 1: API Setup, UI Verification
test('verify order confirmation page', async ({ page, request }) => {
// FAST: Create order via API
const orderResponse = await request.post('/api/orders', {
data: {
items: [{ productId: 'SKU-001', quantity: 1 }],
shippingAddress: {
street: '123 Main St',
city: 'San Francisco',
state: 'CA',
zip: '94102',
},
payment: {
method: 'test_card',
token: 'tok_visa',
},
},
});
const { orderId } = await orderResponse.json();
// UI: Verify the confirmation page renders correctly
await page.goto(`/orders/${orderId}/confirmation`);
await expect(page.getByRole('heading', { name: 'Order Confirmed' })).toBeVisible();
await expect(page.getByText(orderId)).toBeVisible();
await expect(page.getByText('123 Main St')).toBeVisible();
await expect(page.getByRole('button', { name: 'Track Order' })).toBeVisible();
});
Pattern 2: API Cart Population
test('checkout with pre-populated cart', async ({ page, request }) => {
// API: Add items to cart (skips product browsing UI)
await request.post('/api/cart/items', {
data: { productId: 'SKU-001', quantity: 2 },
});
await request.post('/api/cart/items', {
data: { productId: 'SKU-002', quantity: 1 },
});
// UI: Start checkout with items already in cart
await page.goto('/checkout');
await expect(page.getByTestId('cart-item')).toHaveCount(2);
await expect(page.getByTestId('cart-total')).not.toHaveText('$0.00');
// Continue with UI checkout test...
await page.getByLabel('First Name').fill('John');
// ...
});
Pattern 3: API Cleanup After UI Test
test('place order and verify', async ({ page, request }) => {
// UI: Complete checkout (the flow we're actually testing)
await page.goto('/products/laptop');
await page.getByRole('button', { name: 'Add to Cart' }).click();
await page.goto('/checkout');
// ... fill form, submit
// Get order ID from confirmation page
const orderIdElement = page.getByTestId('order-id');
await expect(orderIdElement).toBeVisible();
const orderId = await orderIdElement.textContent();
// API: Clean up (cancel order, refund, etc.)
await request.post(`/api/orders/${orderId}/cancel`, {
data: { reason: 'Test cleanup' },
});
});
Pattern 4: API Test Data Factory
// helpers/testDataFactory.ts
import { APIRequestContext } from '@playwright/test';
export class TestDataFactory {
constructor(private request: APIRequestContext) {}
async createTestUser(): Promise<{ email: string; password: string; id: string }> {
const email = `test-${Date.now()}@example.com`;
const password = 'TestPass123!';
const response = await this.request.post('/api/users', {
data: { email, password, name: 'Test User' },
});
const { id } = await response.json();
return { email, password, id };
}
async createProduct(data?: Partial<ProductData>): Promise<string> {
const response = await this.request.post('/api/products', {
data: {
name: `Test Product ${Date.now()}`,
price: 29.99,
stock: 100,
...data,
},
});
const { productId } = await response.json();
return productId;
}
async createOrder(items: OrderItem[]): Promise<string> {
const response = await this.request.post('/api/orders', {
data: {
items,
shippingAddress: this.defaultAddress,
payment: { method: 'test', token: 'tok_test' },
},
});
const { orderId } = await response.json();
return orderId;
}
async cleanup(resource: string, id: string): Promise<void> {
await this.request.delete(`/api/${resource}/${id}`);
}
private defaultAddress = {
street: '123 Test St',
city: 'Test City',
state: 'CA',
zip: '12345',
};
}
// tests/order-flow.spec.ts
import { test, expect } from '@playwright/test';
import { TestDataFactory } from '../helpers/testDataFactory';
test('order flow with test data factory', async ({ page, request }) => {
const factory = new TestDataFactory(request);
// Create test data via API
const productId = await factory.createProduct({ name: 'Test Widget', price: 19.99 });
// Use in UI test
await page.goto(`/products/${productId}`);
await expect(page.getByText('Test Widget')).toBeVisible();
await expect(page.getByText('$19.99')).toBeVisible();
// Cleanup
await factory.cleanup('products', productId);
});
API Response Validation
Schema Validation
import { test, expect } from '@playwright/test';
test('product API returns valid schema', async ({ request }) => {
const response = await request.get('/api/products/SKU-001');
expect(response.ok()).toBeTruthy();
const product = await response.json();
// Validate required fields
expect(product).toHaveProperty('id');
expect(product).toHaveProperty('name');
expect(product).toHaveProperty('price');
expect(product).toHaveProperty('stock');
// Validate types
expect(typeof product.id).toBe('string');
expect(typeof product.name).toBe('string');
expect(typeof product.price).toBe('number');
expect(typeof product.stock).toBe('number');
// Validate values
expect(product.price).toBeGreaterThan(0);
expect(product.stock).toBeGreaterThanOrEqual(0);
});
Contract Testing
test.describe('Cart API Contract', () => {
test('GET /api/cart returns cart structure', async ({ request }) => {
const response = await request.get('/api/cart');
const cart = await response.json();
expect(cart).toMatchObject({
items: expect.any(Array),
subtotal: expect.any(Number),
tax: expect.any(Number),
total: expect.any(Number),
});
});
test('POST /api/cart/items adds item correctly', async ({ request }) => {
const response = await request.post('/api/cart/items', {
data: { productId: 'SKU-001', quantity: 2 },
});
expect(response.status()).toBe(201);
const result = await response.json();
expect(result).toMatchObject({
cartId: expect.any(String),
itemCount: expect.any(Number),
});
});
test('DELETE /api/cart/items/:id removes item', async ({ request }) => {
// Add item first
const addResponse = await request.post('/api/cart/items', {
data: { productId: 'SKU-001', quantity: 1 },
});
const { itemId } = await addResponse.json();
// Remove it
const deleteResponse = await request.delete(`/api/cart/items/${itemId}`);
expect(deleteResponse.status()).toBe(204);
});
});
Performance Comparison
| Approach | 100 Tests | With Parallel (4 workers) |
|---|---|---|
| Pure UI | 22 min | 6 min |
| Hybrid API+UI | 8 min | 2.5 min |
| Pure API | 45 sec | 15 sec |
For e-commerce, hybrid is the sweet spot: fast setup via API, critical path verification via UI.
Key Takeaways
- Use
requestfixture for API calls in the same test context - Authenticate via API and save session state (massive time savings)
- Hybrid tests combine fast API setup with UI verification
- Test data factories create clean, isolated test data
- API cleanup keeps test environment clean
- Contract testing validates API structure without UI
Next Lesson
API requests are great for real backends. But what about testing edge cases like payment failures or network errors? In the next lesson, we'll learn network mocking to simulate any scenario.