Overview
Comprehensive testing ensures your storefront works reliably under various conditions. This guide covers testing strategies, tools, and best practices for Crave API integrations.Testing Environment Setup
1. Test Location Configuration
Use these test values for development:Copy
# Test Environment - connects to production API with test location
CRAVE_API_BASE_URL=https://api.cravejs.com
CRAVE_API_KEY=your_storefront_api_key_here
NEXT_PUBLIC_LOCATION_ID=your_test_location_id_here
# Required headers for all API calls
X-API-Key: your_storefront_api_key_here
Content-Type: application/json
2. Test Data
The test location includes:- Products: Various menu items with different prices and configurations
- Modifiers: Size options, add-ons, customizations
- Operating Hours: Standard restaurant hours
- Delivery Zones: Test delivery areas
Unit Testing
API Client Testing
Copy
// __tests__/api-client.test.js
import { fetchProducts, createCart, addItemToCart } from '../lib/api/client';
describe('API Client', () => {
const locationId = 'your_test_location_id_here';
const apiKey = 'your_storefront_api_key_here';
beforeEach(() => {
// Mock fetch for isolated testing
global.fetch = jest.fn();
process.env.CRAVE_API_KEY = apiKey;
});
afterEach(() => {
jest.resetAllMocks();
});
describe('fetchProducts', () => {
it('should fetch products successfully', async () => {
const mockProducts = [
{ _id: '1', name: 'Test Product', price: 10.99, availability: { isAvailable: true } }
];
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockProducts
});
const products = await fetchProducts(locationId);
expect(global.fetch).toHaveBeenCalledWith(
`https://api.cravejs.com/api/v1/locations/${locationId}/products`,
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'X-API-Key': apiKey
})
})
);
expect(products).toEqual(mockProducts);
});
it('should handle API errors', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 404,
json: async () => ({ message: 'Not found' })
});
await expect(fetchProducts(locationId))
.rejects.toThrow('Not found');
});
});
describe('createCart', () => {
it('should create cart successfully', async () => {
const mockResponse = { cartId: 'cart123' };
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const cart = await createCart(locationId);
expect(global.fetch).toHaveBeenCalledWith(
`https://api.cravejs.com/api/v1/locations/${locationId}/carts`,
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'X-API-Key': apiKey,
'Content-Type': 'application/json'
}),
body: JSON.stringify({
currentCartId: "",
orderType: "pickup",
orderTime: new Date().toISOString()
})
})
);
expect(cart).toEqual(mockResponse);
});
});
});
Cart State Testing
Copy
// __tests__/cart-provider.test.js
import { renderHook, act } from '@testing-library/react';
import { CartProvider, useCart } from '../providers/cart-provider';
describe('CartProvider', () => {
const wrapper = ({ children }) => (
<CartProvider>{children}</CartProvider>
);
it('should add item to cart', async () => {
const { result } = renderHook(() => useCart(), { wrapper });
const testItem = {
id: '1',
name: 'Test Item',
price: 10.99,
options: { giftBox: false }
};
await act(async () => {
await result.current.addToCart(testItem);
});
expect(result.current.items).toHaveLength(1);
expect(result.current.items[0].name).toBe('Test Item');
expect(result.current.isCartOpen).toBe(true);
});
it('should calculate totals correctly', async () => {
const { result } = renderHook(() => useCart(), { wrapper });
const testItem = {
id: '1',
name: 'Test Item',
price: 10.00,
options: { giftBox: true } // +$5
};
await act(async () => {
await result.current.addToCart(testItem);
});
expect(result.current.subtotal).toBe(15.00); // $10 + $5 gift box
expect(result.current.tax).toBeCloseTo(1.33); // 8.875% NYC tax
expect(result.current.total).toBeCloseTo(16.33);
});
});
Integration Testing
End-to-End Cart Flow
Copy
// __tests__/e2e/cart-flow.test.js
describe('Cart Flow Integration', () => {
const locationId = 'your_test_location_id_here';
it('should complete full cart flow', async () => {
// 1. Create cart
const cartResponse = await createCart(locationId);
expect(cartResponse.cartId).toBeDefined();
const cartId = cartResponse.cartId;
// 2. Fetch products
const products = await fetchProducts(locationId);
expect(products.length).toBeGreaterThan(0);
// 3. Add item to cart
const testProduct = products[0];
const addItemResponse = await addItemToCart(locationId, cartId, {
productId: testProduct._id,
quantity: 1,
modifiers: [] // Add any required modifiers
});
expect(addItemResponse.success).toBe(true);
// 4. Get updated cart
const updatedCart = await getCart(locationId, cartId);
expect(updatedCart.items).toHaveLength(1);
// 5. Update quantity
const cartItemId = updatedCart.items[0]._id;
await updateCartItemQuantity(locationId, cartId, cartItemId, 2);
// 6. Validate cart
const validationResponse = await validateAndUpdateCart(locationId, cartId, {
customerName: 'Test Customer',
emailAddress: 'test@example.com',
phoneNumber: '+15550123456'
});
expect(validationResponse.success).toBe(true);
// 7. Create payment intent
const paymentIntent = await createPaymentIntent(locationId, cartId);
expect(paymentIntent.clientSecret).toBeDefined();
// 8. Clean up
await deleteCart(locationId, cartId);
});
});
Visual Testing
Component Testing with Storybook
Copy
// stories/cart-sidebar.stories.js
import { LeclercCart } from '../components/leclerc-cart';
export default {
title: 'Components/LeclercCart',
component: LeclercCart,
decorators: [
(Story) => (
<CartProvider>
<Story />
</CartProvider>
)
]
};
export const EmptyCart = {
args: {
isOpen: true,
onClose: () => {}
}
};
export const CartWithItems = {
args: {
isOpen: true,
onClose: () => {}
},
decorators: [
(Story) => {
// Mock cart with items
const mockCart = {
items: [
{
cartId: '1',
name: 'Chocolate Chip Cookie',
price: 3.99,
quantity: 2,
image: '/images/cookie.jpg',
options: { giftBox: false }
}
]
};
return (
<CartProvider initialCart={mockCart}>
<Story />
</CartProvider>
);
}
]
};
Performance Testing
API Response Times
Copy
// __tests__/performance/api-performance.test.js
describe('API Performance', () => {
const locationId = 'your_test_location_id_here';
it('should load products within acceptable time', async () => {
const start = Date.now();
const products = await fetchProducts(locationId);
const duration = Date.now() - start;
expect(duration).toBeLessThan(2000); // 2 seconds max
expect(products.length).toBeGreaterThan(0);
});
it('should load menus with required parameters', async () => {
const orderDate = new Date().toISOString().split('T')[0];
const orderTime = '12:00';
const start = Date.now();
const menus = await fetchMenus(locationId, orderDate, orderTime);
const duration = Date.now() - start;
expect(duration).toBeLessThan(2000);
expect(menus).toBeDefined();
});
it('should handle concurrent requests', async () => {
const requests = Array(10).fill().map(() => fetchProducts(locationId));
const results = await Promise.all(requests);
results.forEach(products => {
expect(products).toBeDefined();
expect(Array.isArray(products)).toBe(true);
});
});
});
Load Testing
Copy
// scripts/load-test.js
const API_BASE = 'https://api.cravejs.com/api/v1';
const API_KEY = process.env.CRAVE_API_KEY;
const LOCATION_ID = process.env.NEXT_PUBLIC_LOCATION_ID;
const loadTest = async () => {
const concurrent = 50;
const requests = [];
for (let i = 0; i < concurrent; i++) {
requests.push(
fetch(`${API_BASE}/locations/${LOCATION_ID}/products`, {
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json'
}
})
);
}
const start = Date.now();
const responses = await Promise.all(requests);
const duration = Date.now() - start;
console.log(`${concurrent} requests completed in ${duration}ms`);
console.log(`Average: ${duration / concurrent}ms per request`);
const successCount = responses.filter(r => r.ok).length;
console.log(`Success rate: ${(successCount / concurrent) * 100}%`);
};
loadTest();
Error Scenario Testing
Network Failure Simulation
Copy
// __tests__/error-scenarios.test.js
describe('Error Scenarios', () => {
it('should handle network timeout', async () => {
// Mock network timeout
global.fetch = jest.fn().mockRejectedValue(new Error('Network timeout'));
await expect(fetchProducts(locationId))
.rejects.toThrow('Network timeout');
});
it('should handle rate limiting', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 429,
json: async () => ({ message: 'Rate limit exceeded' })
});
await expect(fetchProducts(locationId))
.rejects.toThrow('Rate limit exceeded');
});
it('should handle invalid API key', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 401,
json: async () => ({ message: 'Invalid API key' })
});
await expect(fetchProducts(locationId))
.rejects.toThrow('Invalid API key');
});
});
Automated Testing Pipeline
Jest Configuration
Copy
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/$1'
},
testMatch: [
'**/__tests__/**/*.test.js',
'**/?(*.)+(spec|test).js'
],
collectCoverageFrom: [
'lib/**/*.js',
'components/**/*.js',
'providers/**/*.js',
'!**/*.stories.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
CI/CD Integration
Copy
# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm run test:unit
- run: npm run test:integration
- run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
API Client Implementation Example
Copy
// lib/api/client.js
const API_BASE = process.env.CRAVE_API_BASE_URL || 'https://api.cravejs.com/api/v1';
const API_KEY = process.env.CRAVE_API_KEY;
const apiRequest = async (endpoint, options = {}) => {
const url = `${API_BASE}${endpoint}`;
const config = {
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
...options.headers
},
...options
};
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
};
export const fetchProducts = (locationId, productIds = null) => {
const query = productIds ? `?productIds=${productIds.join(',')}` : '';
return apiRequest(`/locations/${locationId}/products${query}`);
};
export const fetchMenus = (locationId, orderDate, orderTime) => {
return apiRequest(`/locations/${locationId}/menus?orderDate=${orderDate}&orderTime=${orderTime}`);
};
export const createCart = (locationId, cartData) => {
return apiRequest(`/locations/${locationId}/carts`, {
method: 'POST',
body: JSON.stringify({
currentCartId: '',
orderType: 'pickup',
orderTime: new Date().toISOString(),
...cartData
})
});
};
export const getCart = (locationId, cartId) => {
return apiRequest(`/locations/${locationId}/carts/${cartId}`);
};
export const addItemToCart = (locationId, cartId, itemData) => {
return apiRequest(`/locations/${locationId}/carts/${cartId}/cart-item`, {
method: 'POST',
body: JSON.stringify(itemData)
});
};
export const updateCartItemQuantity = (locationId, cartId, itemId, quantity) => {
return apiRequest(`/locations/${locationId}/carts/${cartId}/cart-item/${itemId}/quantity`, {
method: 'PATCH',
body: JSON.stringify({ quantity })
});
};
export const validateAndUpdateCart = (locationId, cartId, customerData) => {
return apiRequest(`/locations/${locationId}/cart/${cartId}/validate-and-update`, {
method: 'PUT',
body: JSON.stringify(customerData)
});
};
export const createPaymentIntent = (locationId, cartId) => {
return apiRequest(`/stripe/payment-intent?locationId=${locationId}&cartId=${cartId}`);
};
export const deleteCart = (locationId, cartId) => {
return apiRequest(`/locations/${locationId}/carts/${cartId}`, {
method: 'DELETE'
});
};
export const applyDiscount = (locationId, code, cartId, customerId = '') => {
return apiRequest(`/locations/${locationId}/discounts/apply-discount`, {
method: 'POST',
body: JSON.stringify({ code, cartId, customerId })
});
};
export const removeDiscount = (locationId, cartId) => {
return apiRequest(`/locations/${locationId}/discounts/apply-discount`, {
method: 'DELETE',
body: JSON.stringify({ cartId })
});
};
Testing Best Practices
- Test pyramid: More unit tests, fewer integration tests, minimal E2E
- Mock external dependencies for unit tests
- Test error scenarios thoroughly (401, 404, 429, 500 responses)
- Use test data that mirrors production structure
- Test with actual API endpoints and required parameters
- Include authentication in all tests
- Automate testing in CI/CD pipeline
- Monitor performance in tests
- Keep tests fast and reliable
- Document test scenarios for team knowledge
- Test location-specific operations with valid location IDs
- Validate required fields (customerName, email/phone, etc.)