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:
# 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

// __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

// __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

// __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

// 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

// __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

// 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

// __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

// 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

# .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

// 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

  1. Test pyramid: More unit tests, fewer integration tests, minimal E2E
  2. Mock external dependencies for unit tests
  3. Test error scenarios thoroughly (401, 404, 429, 500 responses)
  4. Use test data that mirrors production structure
  5. Test with actual API endpoints and required parameters
  6. Include authentication in all tests
  7. Automate testing in CI/CD pipeline
  8. Monitor performance in tests
  9. Keep tests fast and reliable
  10. Document test scenarios for team knowledge
  11. Test location-specific operations with valid location IDs
  12. Validate required fields (customerName, email/phone, etc.)