Skip to content

📅 NGÀY 57: Integration & E2E Testing Preview

🎯 Mục tiêu học tập (5 phút)

  • [ ] Phân biệt rõ ràng Unit, Integration, và E2E testing
  • [ ] Viết được integration tests cho multiple components
  • [ ] Hiểu khi nào nên dùng loại test nào (decision matrix)
  • [ ] Nắm được E2E testing concepts và tools (Playwright preview)
  • [ ] Thiết kế testing strategy cho production application

🤔 Kiểm tra đầu vào (5 phút)

  1. RTL Fundamentals (Ngày 54-55): Làm sao test component có state và props?
  2. MSW (Ngày 56): Tại sao nên mock API calls trong tests?
  3. Testing Philosophy (Ngày 53): Test pyramid có các tầng nào?

📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)

1.1 Vấn Đề Thực Tế

Scenario: Bug trong Production

jsx
// Tất cả unit tests đều PASS ✅

// UserProfile.test.jsx - PASS ✅
test('displays user name', () => {
  render(<UserProfile user={{ name: 'John' }} />);
  expect(screen.getByText('John')).toBeInTheDocument();
});

// UserSettings.test.jsx - PASS ✅
test('updates settings', () => {
  render(<UserSettings onSave={mockSave} />);
  fireEvent.click(screen.getByRole('button', { name: /save/i }));
  expect(mockSave).toHaveBeenCalled();
});

// Nhưng trong production: ❌
// UserProfile lấy data từ API
// UserSettings cập nhật API
// Khi save settings → Profile KHÔNG update!

// Vấn đề: Components riêng lẻ hoạt động, nhưng INTEGRATION bị lỗi

Root Cause:

  • Unit tests chỉ test components ISOLATED
  • Không test COMMUNICATION giữa components
  • Không test REAL DATA FLOW
  • Không test USER JOURNEY hoàn chỉnh

1.2 Giải Pháp: Testing Levels

3 Tầng Testing:

E2E Testing (10%)           🎭 Test như real user
     ↑                      - Mở browser thật
     |                      - Click, type thật
     |                      - API thật, DB thật
     |
Integration Testing (20%)   🔗 Test components together
     ↑                      - Multiple components
     |                      - Mocked APIs
     |                      - Data flow verification
     |
Unit Testing (70%)          🧱 Test individual pieces
                           - Single component
                           - Mocked dependencies
                           - Isolated behavior

Ví dụ cụ thể: Shopping Cart

jsx
// UNIT TEST: Test CartButton riêng
test('shows item count', () => {
  render(<CartButton itemCount={3} />);
  expect(screen.getByText('3')).toBeInTheDocument();
});

// INTEGRATION TEST: Test Cart + Product together
test('adding product updates cart', () => {
  render(
    <CartProvider>
      <ProductList />
      <Cart />
    </CartProvider>,
  );

  fireEvent.click(screen.getByRole('button', { name: /add laptop/i }));
  expect(screen.getByText(/1 item in cart/i)).toBeInTheDocument();
});

// E2E TEST: Test toàn bộ user flow
test('user can checkout', async () => {
  await page.goto('http://localhost:3000');
  await page.click('text=Add to Cart');
  await page.click('text=Checkout');
  await page.fill('#email', 'test@example.com');
  await page.click('text=Place Order');
  await expect(page).toHaveURL(/\/success/);
});

1.3 Mental Model

TEST TYPES COMPARISON

┌─────────────────────────────────────────────────────────┐
│ UNIT TEST                                               │
│ ┌──────────┐                                           │
│ │Component │  ← Test này                               │
│ └──────────┘                                           │
│ Mock mọi thứ xung quanh                                │
│ Fast ⚡ | Isolated 🔒 | Nhiều ✅                        │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ INTEGRATION TEST                                        │
│ ┌──────────┐   ┌──────────┐   ┌──────────┐           │
│ │Component │ → │Component │ → │Component │            │
│ └──────────┘   └──────────┘   └──────────┘           │
│ Test data flow giữa components                         │
│ Medium ⚡ | Realistic 🎯 | Ít hơn ✅                   │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ E2E TEST                                                │
│ ┌──────────────────────────────────────────────────┐  │
│ │ Real Browser → App → API → Database              │  │
│ └──────────────────────────────────────────────────┘  │
│ Test toàn bộ hệ thống                                  │
│ Slow 🐌 | Real 💯 | Rất ít ✅                          │
└─────────────────────────────────────────────────────────┘

ANALOGY:
- Unit Test = Test từng món ăn riêng lẻ
- Integration = Test món ăn khi kết hợp (combo meal)
- E2E = Test toàn bộ trải nghiệm nhà hàng (từ vào cửa đến thanh toán)

1.4 Hiểu Lầm Phổ Biến

❌ Sai Lầm✅ Đúng💡 Giải Thích
"100% coverage = no bugs"Tests đúng chỗ quan trọngCoverage != Quality
"E2E tests bao quát mọi thứ"70% unit, 20% integration, 10% E2EE2E chậm và flaky
"Integration = test 2 components"Test complete user flowVề scope, không về số lượng
"Chỉ cần unit tests thôi"Cần cả 3 loại testsUnit không catch integration bugs
"E2E replace manual testing"E2E bổ sung cho manualKhông thể test mọi case

💻 PHẦN 2: LIVE CODING (45 phút)

Demo 1: Integration Test Cơ Bản ⭐

Testing Data Flow Between Components

jsx
// Components to test together
// AuthContext.jsx
const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  const login = async (email, password) => {
    setLoading(true);
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      });
      const data = await response.json();
      setUser(data.user);
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    } finally {
      setLoading(false);
    }
  };

  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, loading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

// LoginForm.jsx
const LoginForm = () => {
  const { login, loading } = useContext(AuthContext);
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    const result = await login(email, password);
    if (!result.success) {
      setError(result.error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>Login</h2>
      <input
        type='email'
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder='Email'
        aria-label='Email'
      />
      <input
        type='password'
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder='Password'
        aria-label='Password'
      />
      <button
        type='submit'
        disabled={loading}
      >
        {loading ? 'Logging in...' : 'Login'}
      </button>
      {error && <div role='alert'>{error}</div>}
    </form>
  );
};

// UserProfile.jsx
const UserProfile = () => {
  const { user, logout } = useContext(AuthContext);

  if (!user) {
    return <p>Please log in</p>;
  }

  return (
    <div>
      <h2>Welcome, {user.name}!</h2>
      <p>Email: {user.email}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
};

// ❌ UNIT TEST (không catch integration bug)
describe('LoginForm - Unit Test', () => {
  test('renders form fields', () => {
    // Mock context
    const mockLogin = jest.fn();
    jest.spyOn(React, 'useContext').mockReturnValue({
      login: mockLogin,
      loading: false,
    });

    render(<LoginForm />);
    expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
  });

  // Problem: Không test REAL integration với AuthProvider
});

// ✅ INTEGRATION TEST (test real data flow)
describe('Auth Flow - Integration Test', () => {
  test('user can login and see profile', async () => {
    // Setup MSW mock
    server.use(
      rest.post('/api/login', (req, res, ctx) => {
        return res(
          ctx.json({
            user: { id: '1', name: 'John Doe', email: 'john@example.com' },
          }),
        );
      }),
    );

    // Render BOTH components with REAL provider
    render(
      <AuthProvider>
        <LoginForm />
        <UserProfile />
      </AuthProvider>,
    );

    // Initial state: not logged in
    expect(screen.getByText(/please log in/i)).toBeInTheDocument();
    expect(screen.queryByText(/welcome/i)).not.toBeInTheDocument();

    // Fill login form
    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: 'john@example.com' },
    });
    fireEvent.change(screen.getByLabelText(/password/i), {
      target: { value: 'password123' },
    });

    // Submit
    fireEvent.click(screen.getByRole('button', { name: /login/i }));

    // Wait for async login
    await waitFor(() => {
      expect(screen.queryByText(/logging in/i)).not.toBeInTheDocument();
    });

    // Verify INTEGRATION: Profile updates automatically
    expect(screen.getByText(/welcome, john doe/i)).toBeInTheDocument();
    expect(screen.getByText(/email: john@example.com/i)).toBeInTheDocument();
    expect(screen.queryByText(/please log in/i)).not.toBeInTheDocument();
  });

  test('shows error on failed login', async () => {
    server.use(
      rest.post('/api/login', (req, res, ctx) => {
        return res(ctx.status(401), ctx.json({ error: 'Invalid credentials' }));
      }),
    );

    render(
      <AuthProvider>
        <LoginForm />
        <UserProfile />
      </AuthProvider>,
    );

    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: 'wrong@example.com' },
    });
    fireEvent.change(screen.getByLabelText(/password/i), {
      target: { value: 'wrong' },
    });

    fireEvent.click(screen.getByRole('button', { name: /login/i }));

    // Error appears in LoginForm
    const error = await screen.findByRole('alert');
    expect(error).toHaveTextContent(/invalid credentials/i);

    // Profile still shows not logged in
    expect(screen.getByText(/please log in/i)).toBeInTheDocument();
  });

  test('user can logout', async () => {
    server.use(
      rest.post('/api/login', (req, res, ctx) => {
        return res(
          ctx.json({
            user: { id: '1', name: 'John', email: 'john@example.com' },
          }),
        );
      }),
    );

    render(
      <AuthProvider>
        <LoginForm />
        <UserProfile />
      </AuthProvider>,
    );

    // Login first
    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: 'john@example.com' },
    });
    fireEvent.change(screen.getByLabelText(/password/i), {
      target: { value: 'password' },
    });
    fireEvent.click(screen.getByRole('button', { name: /login/i }));

    await screen.findByText(/welcome, john/i);

    // Logout
    fireEvent.click(screen.getByRole('button', { name: /logout/i }));

    // Back to initial state
    expect(screen.getByText(/please log in/i)).toBeInTheDocument();
    expect(screen.queryByText(/welcome/i)).not.toBeInTheDocument();
  });
});

Key Differences:

  • Unit test: Mock context, test component isolated
  • Integration test: Real provider, test actual data flow

Demo 2: Multi-Component Integration ⭐⭐

Testing Shopping Cart Flow

jsx
// Full shopping cart integration
// ProductList.jsx
const ProductList = ({ products }) => {
  const { addToCart } = useContext(CartContext);

  return (
    <div>
      <h2>Products</h2>
      <ul aria-label='Product list'>
        {products.map((product) => (
          <li key={product.id}>
            <h3>{product.name}</h3>
            <p>${product.price}</p>
            <button
              onClick={() => addToCart(product)}
              aria-label={`Add ${product.name} to cart`}
            >
              Add to Cart
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

// CartSummary.jsx
const CartSummary = () => {
  const { items, removeFromCart, total } = useContext(CartContext);

  return (
    <div>
      <h2>Cart ({items.length} items)</h2>
      {items.length === 0 ? (
        <p>Cart is empty</p>
      ) : (
        <>
          <ul aria-label='Cart items'>
            {items.map((item) => (
              <li key={item.id}>
                {item.name} - ${item.price} x {item.quantity}
                <button
                  onClick={() => removeFromCart(item.id)}
                  aria-label={`Remove ${item.name}`}
                >
                  Remove
                </button>
              </li>
            ))}
          </ul>
          <p>Total: ${total}</p>
        </>
      )}
    </div>
  );
};

// CheckoutButton.jsx
const CheckoutButton = () => {
  const { items, checkout } = useContext(CartContext);
  const [processing, setProcessing] = useState(false);

  const handleCheckout = async () => {
    setProcessing(true);
    await checkout();
    setProcessing(false);
  };

  return (
    <button
      onClick={handleCheckout}
      disabled={items.length === 0 || processing}
      aria-label='Proceed to checkout'
    >
      {processing ? 'Processing...' : `Checkout (${items.length})`}
    </button>
  );
};

// Integration Test
describe('Shopping Cart Integration', () => {
  const mockProducts = [
    { id: '1', name: 'Laptop', price: 999 },
    { id: '2', name: 'Mouse', price: 25 },
    { id: '3', name: 'Keyboard', price: 75 },
  ];

  test('complete shopping flow', async () => {
    server.use(
      rest.post('/api/checkout', (req, res, ctx) => {
        return res(ctx.json({ orderId: '12345', success: true }));
      }),
    );

    render(
      <CartProvider>
        <ProductList products={mockProducts} />
        <CartSummary />
        <CheckoutButton />
      </CartProvider>,
    );

    // Initial state: empty cart
    expect(screen.getByText(/cart is empty/i)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /checkout/i })).toBeDisabled();

    // Add first product
    fireEvent.click(screen.getByRole('button', { name: /add laptop/i }));

    // Verify cart updated
    expect(screen.getByText(/cart \(1 items\)/i)).toBeInTheDocument();
    expect(screen.getByText(/laptop - \$999 x 1/i)).toBeInTheDocument();
    expect(screen.getByText(/total: \$999/i)).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: /checkout/i }),
    ).not.toBeDisabled();

    // Add second product
    fireEvent.click(screen.getByRole('button', { name: /add mouse/i }));

    // Verify cart has 2 items
    expect(screen.getByText(/cart \(2 items\)/i)).toBeInTheDocument();
    expect(screen.getByText(/total: \$1024/i)).toBeInTheDocument();

    // Add same product again (quantity should increase)
    fireEvent.click(screen.getByRole('button', { name: /add laptop/i }));

    expect(screen.getByText(/laptop - \$999 x 2/i)).toBeInTheDocument();
    expect(screen.getByText(/total: \$2023/i)).toBeInTheDocument();

    // Remove one item
    fireEvent.click(screen.getByRole('button', { name: /remove mouse/i }));

    expect(screen.queryByText(/mouse/i)).not.toBeInTheDocument();
    expect(screen.getByText(/total: \$1998/i)).toBeInTheDocument();

    // Checkout
    fireEvent.click(screen.getByRole('button', { name: /checkout \(2\)/i }));

    // Shows processing
    expect(screen.getByText(/processing/i)).toBeInTheDocument();

    // Wait for checkout to complete
    await waitFor(() => {
      expect(screen.queryByText(/processing/i)).not.toBeInTheDocument();
    });
  });

  test('cart persists across component remounts', () => {
    const { rerender } = render(
      <CartProvider>
        <ProductList products={mockProducts} />
        <CartSummary />
      </CartProvider>,
    );

    // Add item
    fireEvent.click(screen.getByRole('button', { name: /add keyboard/i }));
    expect(screen.getByText(/keyboard/i)).toBeInTheDocument();

    // Remount components
    rerender(
      <CartProvider>
        <ProductList products={mockProducts} />
        <CartSummary />
      </CartProvider>,
    );

    // Cart should still have item (if using localStorage or context state)
    expect(screen.getByText(/keyboard/i)).toBeInTheDocument();
  });
});

Demo 3: E2E Testing Preview ⭐⭐⭐

Playwright Concepts (không implement chi tiết)

jsx
/**
 * E2E Testing với Playwright
 *
 * Khác biệt:
 * - Chạy trong REAL browser (Chrome, Firefox, Safari)
 * - Test REAL backend (không mock)
 * - Test REAL user interactions (mouse, keyboard)
 * - Test cross-browser compatibility
 */

// playwright.config.js
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
});

// e2e/shopping.spec.js
import { test, expect } from '@playwright/test';

/**
 * E2E Test: Complete shopping journey
 *
 * Scope: Toàn bộ user flow từ đầu đến cuối
 * - Real browser interactions
 * - Real API calls
 * - Real database operations
 * - Visual validation
 */
test('user can complete purchase', async ({ page }) => {
  // Navigate to app
  await page.goto('/');

  // Verify homepage loaded
  await expect(page).toHaveTitle(/shop/i);

  // Browse products
  await page.click('text=Products');
  await expect(page.locator('h2')).toContainText('Our Products');

  // Add product to cart
  await page.click('button:has-text("Add to Cart"):near(:text("Laptop"))');

  // Verify cart badge updated
  await expect(page.locator('.cart-badge')).toHaveText('1');

  // Go to cart
  await page.click('text=Cart');

  // Verify product in cart
  await expect(page.locator('.cart-item')).toContainText('Laptop');
  await expect(page.locator('.cart-total')).toContainText('$999');

  // Proceed to checkout
  await page.click('button:has-text("Checkout")');

  // Fill shipping form
  await page.fill('[name="email"]', 'test@example.com');
  await page.fill('[name="address"]', '123 Main St');
  await page.fill('[name="city"]', 'New York');

  // Select shipping method
  await page.selectOption('[name="shipping"]', 'express');

  // Fill payment (in real app, this would be Stripe iframe)
  await page.fill('[name="cardNumber"]', '4242424242424242');
  await page.fill('[name="expiry"]', '12/25');
  await page.fill('[name="cvc"]', '123');

  // Place order
  await page.click('button:has-text("Place Order")');

  // Wait for success page
  await expect(page).toHaveURL(/\/success/);
  await expect(page.locator('h1')).toContainText('Order Confirmed');

  // Verify order number displayed
  await expect(page.locator('.order-number')).toBeVisible();

  // Take screenshot for visual regression
  await page.screenshot({ path: 'order-success.png' });
});

/**
 * E2E: Test error handling
 */
test('shows error for invalid payment', async ({ page }) => {
  await page.goto('/checkout');

  // Fill form with invalid card
  await page.fill('[name="cardNumber"]', '0000000000000000');
  await page.click('button:has-text("Place Order")');

  // Verify error message
  await expect(page.locator('.error')).toContainText('Invalid card');

  // User should still be on checkout page
  await expect(page).toHaveURL(/\/checkout/);
});

/**
 * E2E: Test mobile responsiveness
 */
test('mobile: can complete purchase', async ({ page }) => {
  // Set mobile viewport
  await page.setViewportSize({ width: 375, height: 667 });

  await page.goto('/');

  // Open mobile menu
  await page.click('[aria-label="Menu"]');
  await page.click('text=Products');

  // Rest of flow...
});

/**
 * QUAN TRỌNG: Đây chỉ là PREVIEW
 *
 * E2E testing sẽ được học sâu ở module riêng.
 * Bây giờ chỉ cần hiểu:
 * - E2E test toàn bộ hệ thống
 * - Chạy trong real browser
 * - Chậm nhưng realistic
 * - Dùng cho critical user flows
 */

So Sánh RTL vs Playwright:

jsx
// RTL (Integration Test)
test('adds product to cart', () => {
  render(
    <CartProvider>
      <ProductList />
      <Cart />
    </CartProvider>,
  );

  fireEvent.click(screen.getByRole('button', { name: /add/i }));
  expect(screen.getByText(/1 item/i)).toBeInTheDocument();
});

// Playwright (E2E Test)
test('adds product to cart', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await page.click('button:has-text("Add to Cart")');
  await expect(page.locator('.cart')).toContainText('1 item');
});

// Differences:
// RTL: Mocks, JSDOM, fast, isolated
// Playwright: Real browser, real backend, slow, realistic

🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (90 phút)

⭐ Bài 1: First Integration Test (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Viết integration test đầu tiên
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: Playwright, Cypress
 *
 * Requirements:
 * 1. Test 2 components tương tác với nhau
 * 2. Verify data flow giữa components
 * 3. Dùng real Context provider
 *
 * 💡 Gợi ý: Bắt đầu với simple counter + display
 */

// Counter.jsx
const CounterContext = createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(0);

  return (
    <CounterContext.Provider value={{ count, increment, decrement, reset }}>
      {children}
    </CounterContext.Provider>
  );
};

const CounterControls = () => {
  const { increment, decrement, reset } = useContext(CounterContext);

  return (
    <div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

const CounterDisplay = () => {
  const { count } = useContext(CounterContext);

  return (
    <div>
      <p>Count: {count}</p>
      <p>{count > 0 ? 'Positive' : count < 0 ? 'Negative' : 'Zero'}</p>
    </div>
  );
};

// TODO: Write integration test
// Test that:
// 1. Clicking + in Controls updates Display
// 2. Clicking - in Controls updates Display
// 3. Clicking Reset in Controls resets Display
// 4. Display shows correct positive/negative/zero status
💡 Solution
jsx
import { render, screen, fireEvent } from '@testing-library/react';

describe('Counter Integration', () => {
  test('controls update display correctly', () => {
    render(
      <CounterProvider>
        <CounterControls />
        <CounterDisplay />
      </CounterProvider>,
    );

    // Initial state
    expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
    expect(screen.getByText(/zero/i)).toBeInTheDocument();

    // Increment
    fireEvent.click(screen.getByRole('button', { name: '+' }));
    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
    expect(screen.getByText(/positive/i)).toBeInTheDocument();

    // Increment again
    fireEvent.click(screen.getByRole('button', { name: '+' }));
    expect(screen.getByText(/count: 2/i)).toBeInTheDocument();

    // Decrement
    fireEvent.click(screen.getByRole('button', { name: '-' }));
    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();

    // Decrement to negative
    fireEvent.click(screen.getByRole('button', { name: '-' }));
    fireEvent.click(screen.getByRole('button', { name: '-' }));
    expect(screen.getByText(/count: -1/i)).toBeInTheDocument();
    expect(screen.getByText(/negative/i)).toBeInTheDocument();

    // Reset
    fireEvent.click(screen.getByRole('button', { name: /reset/i }));
    expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
    expect(screen.getByText(/zero/i)).toBeInTheDocument();
  });

  test('multiple displays stay in sync', () => {
    render(
      <CounterProvider>
        <CounterControls />
        <CounterDisplay />
        <CounterDisplay />
      </CounterProvider>,
    );

    // Both displays should show same value
    fireEvent.click(screen.getByRole('button', { name: '+' }));

    const displays = screen.getAllByText(/count: 1/i);
    expect(displays).toHaveLength(2);
  });
});

// Result:
// ✓ controls update display correctly
// ✓ multiple displays stay in sync

⭐⭐ Bài 2: Form Submission Flow (25 phút)

jsx
/**
 * 🎯 Mục tiêu: Test form submission và result display
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: User submits contact form, sees confirmation
 *
 * 🤔 PHÂN TÍCH:
 * Components involved:
 * 1. ContactForm - input fields, submit button
 * 2. SubmissionStatus - shows success/error message
 * 3. FormContext - manages state and API call
 *
 * Test cases:
 * 1. Successful submission shows success message
 * 2. Failed submission shows error message
 * 3. Form clears after successful submission
 */

// ContactForm.jsx
const ContactForm = () => {
  const { submitForm, submitting } = useContext(FormContext);
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    await submitForm(formData);
    setFormData({ name: '', email: '', message: '' });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name='name'
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        placeholder='Name'
        aria-label='Name'
      />
      <input
        name='email'
        type='email'
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        placeholder='Email'
        aria-label='Email'
      />
      <textarea
        name='message'
        value={formData.message}
        onChange={(e) => setFormData({ ...formData, message: e.target.value })}
        placeholder='Message'
        aria-label='Message'
      />
      <button
        type='submit'
        disabled={submitting}
      >
        {submitting ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  );
};

// SubmissionStatus.jsx
const SubmissionStatus = () => {
  const { status, error } = useContext(FormContext);

  if (status === 'idle') return null;

  if (status === 'success') {
    return <div role='alert'>Message sent successfully!</div>;
  }

  if (status === 'error') {
    return <div role='alert'>Error: {error}</div>;
  }

  return null;
};

// FormContext.jsx (implementation provided)
const FormContext = createContext();

const FormProvider = ({ children }) => {
  const [status, setStatus] = useState('idle');
  const [error, setError] = useState('');
  const [submitting, setSubmitting] = useState(false);

  const submitForm = async (data) => {
    setSubmitting(true);
    setStatus('idle');
    setError('');

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify(data),
      });

      if (!response.ok) throw new Error('Submission failed');

      setStatus('success');
    } catch (err) {
      setStatus('error');
      setError(err.message);
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <FormContext.Provider value={{ status, error, submitting, submitForm }}>
      {children}
    </FormContext.Provider>
  );
};

// TODO: Write integration tests
describe('Contact Form Integration', () => {
  test('successful submission shows success message', async () => {
    // TODO: Setup MSW to return success
    // TODO: Render FormProvider with both components
    // TODO: Fill form
    // TODO: Submit
    // TODO: Verify success message appears
    // TODO: Verify form is cleared
  });

  test('failed submission shows error message', async () => {
    // TODO: Setup MSW to return error
    // TODO: Submit form
    // TODO: Verify error message appears
    // TODO: Verify form is NOT cleared
  });

  test('shows loading state during submission', async () => {
    // TODO: Setup MSW with delay
    // TODO: Submit form
    // TODO: Verify "Sending..." button text
    // TODO: Verify button is disabled
  });
});
💡 Solution
jsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { server } from './mocks/server';

describe('Contact Form Integration', () => {
  test('successful submission shows success message', async () => {
    server.use(
      rest.post('/api/contact', (req, res, ctx) => {
        return res(ctx.json({ success: true }));
      }),
    );

    render(
      <FormProvider>
        <ContactForm />
        <SubmissionStatus />
      </FormProvider>,
    );

    // Fill form
    fireEvent.change(screen.getByLabelText(/name/i), {
      target: { value: 'John Doe' },
    });
    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: 'john@example.com' },
    });
    fireEvent.change(screen.getByLabelText(/message/i), {
      target: { value: 'Hello world' },
    });

    // Submit
    fireEvent.click(screen.getByRole('button', { name: /send message/i }));

    // Wait for success message
    const successMessage = await screen.findByRole('alert');
    expect(successMessage).toHaveTextContent(/message sent successfully/i);

    // Form should be cleared
    expect(screen.getByLabelText(/name/i)).toHaveValue('');
    expect(screen.getByLabelText(/email/i)).toHaveValue('');
    expect(screen.getByLabelText(/message/i)).toHaveValue('');
  });

  test('failed submission shows error message', async () => {
    server.use(
      rest.post('/api/contact', (req, res, ctx) => {
        return res(ctx.status(500), ctx.json({ error: 'Server error' }));
      }),
    );

    render(
      <FormProvider>
        <ContactForm />
        <SubmissionStatus />
      </FormProvider>,
    );

    // Fill and submit
    fireEvent.change(screen.getByLabelText(/name/i), {
      target: { value: 'John' },
    });
    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: 'john@example.com' },
    });
    fireEvent.change(screen.getByLabelText(/message/i), {
      target: { value: 'Test' },
    });

    fireEvent.click(screen.getByRole('button', { name: /send message/i }));

    // Error message appears
    const errorMessage = await screen.findByRole('alert');
    expect(errorMessage).toHaveTextContent(/error/i);

    // Form NOT cleared (user can retry)
    expect(screen.getByLabelText(/name/i)).toHaveValue('John');
  });

  test('shows loading state during submission', async () => {
    server.use(
      rest.post('/api/contact', (req, res, ctx) => {
        return res(ctx.delay(1000), ctx.json({ success: true }));
      }),
    );

    render(
      <FormProvider>
        <ContactForm />
        <SubmissionStatus />
      </FormProvider>,
    );

    // Fill minimal data
    fireEvent.change(screen.getByLabelText(/name/i), {
      target: { value: 'Test' },
    });
    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: 'test@test.com' },
    });
    fireEvent.change(screen.getByLabelText(/message/i), {
      target: { value: 'Hi' },
    });

    // Submit
    fireEvent.click(screen.getByRole('button', { name: /send message/i }));

    // Loading state
    expect(screen.getByRole('button', { name: /sending/i })).toBeDisabled();

    // Wait for completion
    await waitFor(() => {
      expect(
        screen.queryByRole('button', { name: /sending/i }),
      ).not.toBeInTheDocument();
    });
  });
});

// Results:
// ✓ successful submission shows success message
// ✓ failed submission shows error message
// ✓ shows loading state during submission

⭐⭐⭐ Bài 3: Todo App with Filters (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Test complex integration với multiple features
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là user, tôi muốn manage todos và filter chúng"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Add new todos
 * - [ ] Mark todos as complete
 * - [ ] Delete todos
 * - [ ] Filter by status (all/active/completed)
 * - [ ] Show count of active todos
 * - [ ] Clear all completed todos
 *
 * 🎨 Technical Constraints:
 * - Components: TodoForm, TodoList, FilterBar, TodoStats
 * - All components use TodoContext
 * - Filters don't modify underlying data
 *
 * 🚨 Edge Cases cần handle:
 * - Empty todo list
 * - All todos completed
 * - Filter with no matching todos
 * - Clear completed when none exist
 */

// Components provided (TodoForm, TodoList, FilterBar, TodoStats)
// TodoContext provided

// TODO: Write comprehensive integration tests covering:
// 1. Adding todos updates list and stats
// 2. Completing todos updates stats
// 3. Filtering shows correct todos
// 4. Deleting todos updates everything
// 5. Clear completed functionality
// 6. Edge cases (empty states, filters with no results)
💡 Solution
jsx
import { render, screen, fireEvent, within } from '@testing-library/react';

/**
 * Comprehensive integration test for Todo App
 */
describe('Todo App Integration', () => {
  const addTodo = (text) => {
    const input = screen.getByPlaceholderText(/add todo/i);
    fireEvent.change(input, { target: { value: text } });
    fireEvent.submit(input.closest('form'));
  };

  const renderApp = () => {
    render(
      <TodoProvider>
        <TodoForm />
        <FilterBar />
        <TodoStats />
        <TodoList />
      </TodoProvider>,
    );
  };

  test('adding todos updates list and stats', () => {
    renderApp();

    // Initially empty
    expect(screen.getByText(/no todos/i)).toBeInTheDocument();
    expect(screen.getByText(/0 active/i)).toBeInTheDocument();

    // Add first todo
    addTodo('Buy groceries');

    expect(screen.getByText(/buy groceries/i)).toBeInTheDocument();
    expect(screen.getByText(/1 active/i)).toBeInTheDocument();

    // Add second todo
    addTodo('Walk dog');

    expect(screen.getByText(/walk dog/i)).toBeInTheDocument();
    expect(screen.getByText(/2 active/i)).toBeInTheDocument();
  });

  test('completing todos updates stats and appearance', () => {
    renderApp();

    addTodo('Task 1');
    addTodo('Task 2');

    expect(screen.getByText(/2 active/i)).toBeInTheDocument();

    // Complete first todo
    const checkbox1 = screen.getByLabelText(/toggle task 1/i);
    fireEvent.click(checkbox1);

    expect(checkbox1).toBeChecked();
    expect(screen.getByText(/1 active/i)).toBeInTheDocument();

    // Complete second todo
    const checkbox2 = screen.getByLabelText(/toggle task 2/i);
    fireEvent.click(checkbox2);

    expect(screen.getByText(/0 active/i)).toBeInTheDocument();
  });

  test('filtering shows correct todos', () => {
    renderApp();

    // Add mixed todos
    addTodo('Active 1');
    addTodo('Active 2');
    addTodo('To Complete');

    // Complete one
    fireEvent.click(screen.getByLabelText(/toggle to complete/i));

    // All filter (default)
    expect(screen.getByText(/active 1/i)).toBeInTheDocument();
    expect(screen.getByText(/active 2/i)).toBeInTheDocument();
    expect(screen.getByText(/to complete/i)).toBeInTheDocument();

    // Active filter
    fireEvent.click(screen.getByRole('button', { name: /^active$/i }));

    expect(screen.getByText(/active 1/i)).toBeInTheDocument();
    expect(screen.getByText(/active 2/i)).toBeInTheDocument();
    expect(screen.queryByText(/to complete/i)).not.toBeInTheDocument();

    // Completed filter
    fireEvent.click(screen.getByRole('button', { name: /completed/i }));

    expect(screen.queryByText(/active 1/i)).not.toBeInTheDocument();
    expect(screen.getByText(/to complete/i)).toBeInTheDocument();

    // Back to all
    fireEvent.click(screen.getByRole('button', { name: /^all$/i }));

    expect(screen.getAllByRole('listitem')).toHaveLength(3);
  });

  test('deleting todos updates list and stats', () => {
    renderApp();

    addTodo('Delete me');
    addTodo('Keep me');

    expect(screen.getByText(/2 active/i)).toBeInTheDocument();

    // Delete first todo
    fireEvent.click(screen.getByRole('button', { name: /delete delete me/i }));

    expect(screen.queryByText(/delete me/i)).not.toBeInTheDocument();
    expect(screen.getByText(/keep me/i)).toBeInTheDocument();
    expect(screen.getByText(/1 active/i)).toBeInTheDocument();
  });

  test('clear completed removes all completed todos', () => {
    renderApp();

    addTodo('Task 1');
    addTodo('Task 2');
    addTodo('Task 3');

    // Complete 2 todos
    fireEvent.click(screen.getByLabelText(/toggle task 1/i));
    fireEvent.click(screen.getByLabelText(/toggle task 2/i));

    expect(screen.getAllByRole('listitem')).toHaveLength(3);

    // Clear completed
    fireEvent.click(screen.getByRole('button', { name: /clear completed/i }));

    // Only active todo remains
    expect(screen.getAllByRole('listitem')).toHaveLength(1);
    expect(screen.getByText(/task 3/i)).toBeInTheDocument();
    expect(screen.queryByText(/task 1/i)).not.toBeInTheDocument();
  });

  test('filter with no matching todos shows empty message', () => {
    renderApp();

    addTodo('Active todo');

    // No completed todos
    fireEvent.click(screen.getByRole('button', { name: /completed/i }));

    expect(screen.getByText(/no completed todos/i)).toBeInTheDocument();
  });

  test('stats update correctly during full workflow', () => {
    renderApp();

    // Add 3 todos
    addTodo('One');
    addTodo('Two');
    addTodo('Three');

    expect(screen.getByText(/3 active/i)).toBeInTheDocument();
    expect(screen.getByText(/0 completed/i)).toBeInTheDocument();

    // Complete 2
    fireEvent.click(screen.getByLabelText(/toggle one/i));
    fireEvent.click(screen.getByLabelText(/toggle two/i));

    expect(screen.getByText(/1 active/i)).toBeInTheDocument();
    expect(screen.getByText(/2 completed/i)).toBeInTheDocument();

    // Uncomplete one
    fireEvent.click(screen.getByLabelText(/toggle one/i));

    expect(screen.getByText(/2 active/i)).toBeInTheDocument();
    expect(screen.getByText(/1 completed/i)).toBeInTheDocument();

    // Delete active todo
    fireEvent.click(screen.getByRole('button', { name: /delete three/i }));

    expect(screen.getByText(/1 active/i)).toBeInTheDocument();
    expect(screen.getByText(/1 completed/i)).toBeInTheDocument();
  });
});

// Results:
// ✓ adding todos updates list and stats
// ✓ completing todos updates stats and appearance
// ✓ filtering shows correct todos
// ✓ deleting todos updates list and stats
// ✓ clear completed removes all completed todos
// ✓ filter with no matching todos shows empty message
// ✓ stats update correctly during full workflow

⭐⭐⭐⭐ Bài 4: Testing Strategy Design (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Thiết kế comprehensive testing strategy
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Analysis (20 phút)
 *
 * Given: E-commerce product page
 * Components:
 * - ProductGallery (images, zoom)
 * - ProductInfo (name, price, description)
 * - AddToCartButton
 * - ReviewsList
 * - RelatedProducts
 *
 * Nhiệm vụ:
 * 1. Identify what needs unit tests
 * 2. Identify what needs integration tests
 * 3. Identify what needs E2E tests
 * 4. Document rationale cho mỗi quyết định
 *
 * 💻 PHASE 2: Implementation (30 phút)
 * Write tests theo strategy
 *
 * 🧪 PHASE 3: Review (10 phút)
 * - [ ] Coverage adequate?
 * - [ ] Test types appropriate?
 * - [ ] Maintainable?
 */

// Provided: Full e-commerce product page implementation
// TODO: Design and implement test strategy
💡 Solution
jsx
/**
 * TESTING STRATEGY DOCUMENT
 *
 * Application: E-commerce Product Page
 * Components: ProductGallery, ProductInfo, AddToCartButton, ReviewsList, RelatedProducts
 */

// ============================================
// PHASE 1: ANALYSIS & STRATEGY
// ============================================

/**
 * UNIT TESTS (70% of tests)
 *
 * What: Individual components in isolation
 * Why: Fast, focused, easy to debug
 *
 * Components for unit testing:
 * 1. ProductGallery
 *    - Image display
 *    - Thumbnail selection
 *    - Zoom functionality
 *
 * 2. ProductInfo
 *    - Price formatting
 *    - Description rendering
 *    - Stock status display
 *
 * 3. AddToCartButton
 *    - Disabled state when out of stock
 *    - Button text changes
 *    - Click handler called
 *
 * 4. ReviewsList
 *    - Review rendering
 *    - Rating display
 *    - Sorting/filtering
 */

describe('ProductGallery - Unit Tests', () => {
  const mockImages = [
    { id: '1', url: '/img1.jpg', thumbnail: '/thumb1.jpg' },
    { id: '2', url: '/img2.jpg', thumbnail: '/thumb2.jpg' },
  ];

  test('displays main image', () => {
    render(<ProductGallery images={mockImages} />);
    const mainImage = screen.getByAltText(/product image/i);
    expect(mainImage).toHaveAttribute('src', '/img1.jpg');
  });

  test('changes image when thumbnail clicked', () => {
    render(<ProductGallery images={mockImages} />);

    const thumbnail = screen.getByAltText(/thumbnail 2/i);
    fireEvent.click(thumbnail);

    const mainImage = screen.getByAltText(/product image/i);
    expect(mainImage).toHaveAttribute('src', '/img2.jpg');
  });

  test('zoom shows enlarged image', () => {
    render(<ProductGallery images={mockImages} />);

    const mainImage = screen.getByAltText(/product image/i);
    fireEvent.click(mainImage);

    expect(screen.getByRole('dialog')).toBeInTheDocument();
    expect(screen.getByAltText(/zoomed/i)).toHaveAttribute('src', '/img1.jpg');
  });
});

/**
 * INTEGRATION TESTS (20% of tests)
 *
 * What: Multiple components working together
 * Why: Test data flow and component communication
 *
 * Scenarios for integration testing:
 * 1. Product Page Flow
 *    - ProductInfo + AddToCartButton + Cart state
 *    - Selecting variant updates price and stock
 *    - Adding to cart updates global cart count
 *
 * 2. Review Interaction
 *    - ReviewsList + ReviewForm + API
 *    - Submitting review updates list
 *    - Filtering reviews works correctly
 *
 * 3. Related Products
 *    - RelatedProducts + Navigation
 *    - Clicking related product navigates correctly
 */

describe('Product Page - Integration Tests', () => {
  test('adding to cart updates cart count', async () => {
    server.use(
      rest.post('/api/cart', (req, res, ctx) => {
        return res(ctx.json({ success: true, itemCount: 1 }));
      }),
    );

    render(
      <CartProvider>
        <ProductInfo product={mockProduct} />
        <AddToCartButton productId={mockProduct.id} />
        <CartIndicator />
      </CartProvider>,
    );

    // Initially empty cart
    expect(screen.getByText(/0 items/i)).toBeInTheDocument();

    // Add to cart
    fireEvent.click(screen.getByRole('button', { name: /add to cart/i }));

    // Cart count updates
    await screen.findByText(/1 items/i);
  });

  test('selecting variant updates price and availability', () => {
    const productWithVariants = {
      ...mockProduct,
      variants: [
        { id: '1', size: 'S', price: 29.99, inStock: true },
        { id: '2', size: 'M', price: 29.99, inStock: false },
        { id: '3', size: 'L', price: 34.99, inStock: true },
      ],
    };

    render(
      <ProductProvider>
        <ProductInfo product={productWithVariants} />
        <AddToCartButton />
      </ProductProvider>,
    );

    // Default variant
    expect(screen.getByText(/\$29\.99/i)).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: /add to cart/i }),
    ).not.toBeDisabled();

    // Select out-of-stock variant
    fireEvent.click(screen.getByRole('button', { name: /size m/i }));

    expect(screen.getByText(/out of stock/i)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /add to cart/i })).toBeDisabled();

    // Select different price variant
    fireEvent.click(screen.getByRole('button', { name: /size l/i }));

    expect(screen.getByText(/\$34\.99/i)).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: /add to cart/i }),
    ).not.toBeDisabled();
  });

  test('submitting review updates review list', async () => {
    server.use(
      rest.post('/api/reviews', (req, res, ctx) => {
        return res(
          ctx.json({
            id: '123',
            rating: 5,
            comment: 'Great product!',
            author: 'Test User',
          }),
        );
      }),
    );

    render(
      <ReviewProvider productId='product-1'>
        <ReviewsList />
        <ReviewForm />
      </ReviewProvider>,
    );

    // Initial reviews
    expect(screen.getByText(/3 reviews/i)).toBeInTheDocument();

    // Submit new review
    fireEvent.change(screen.getByLabelText(/rating/i), {
      target: { value: '5' },
    });
    fireEvent.change(screen.getByLabelText(/comment/i), {
      target: { value: 'Great product!' },
    });
    fireEvent.click(screen.getByRole('button', { name: /submit/i }));

    // Review list updates
    await screen.findByText(/4 reviews/i);
    expect(screen.getByText(/great product!/i)).toBeInTheDocument();
  });
});

/**
 * E2E TESTS (10% of tests)
 *
 * What: Complete user journeys in real browser
 * Why: Catch issues that only appear in production environment
 *
 * Critical flows for E2E:
 * 1. Purchase Flow
 *    - Browse → View Product → Add to Cart → Checkout → Payment → Confirmation
 *
 * 2. Search to Purchase
 *    - Search → Filter → Select Product → Add to Cart → Checkout
 *
 * 3. User Journey
 *    - Sign Up → Browse → Add to Cart → Save for Later → Return → Complete Purchase
 */

// E2E Test (Playwright) - PREVIEW ONLY
/**
 * test('complete purchase flow', async ({ page }) => {
 *   // Navigate to product
 *   await page.goto('/products/laptop-xyz');
 *
 *   // Verify product loaded
 *   await expect(page.locator('h1')).toContainText('Laptop XYZ');
 *
 *   // Select variant
 *   await page.click('button:has-text("16GB RAM")');
 *
 *   // Add to cart
 *   await page.click('button:has-text("Add to Cart")');
 *
 *   // Verify cart badge
 *   await expect(page.locator('.cart-badge')).toHaveText('1');
 *
 *   // Go to checkout
 *   await page.click('text=Checkout');
 *
 *   // Fill shipping
 *   await page.fill('[name="address"]', '123 Main St');
 *   await page.fill('[name="city"]', 'New York');
 *
 *   // Fill payment
 *   await page.fill('[name="cardNumber"]', '4242424242424242');
 *
 *   // Place order
 *   await page.click('button:has-text("Place Order")');
 *
 *   // Verify success
 *   await expect(page).toHaveURL(/\/order-confirmation/);
 *   await expect(page.locator('h1')).toContainText('Order Confirmed');
 * });
 */

// ============================================
// TESTING STRATEGY SUMMARY
// ============================================

/**
 * Distribution:
 * - Unit Tests: 15 tests (70%)
 *   * ProductGallery: 4 tests
 *   * ProductInfo: 3 tests
 *   * AddToCartButton: 2 tests
 *   * ReviewsList: 3 tests
 *   * RelatedProducts: 3 tests
 *
 * - Integration Tests: 5 tests (20%)
 *   * Cart integration: 2 tests
 *   * Variant selection: 1 test
 *   * Review submission: 1 test
 *   * Related products navigation: 1 test
 *
 * - E2E Tests: 2 tests (10%)
 *   * Complete purchase flow: 1 test
 *   * Search to purchase: 1 test
 *
 * Total: 22 tests
 * Estimated execution time:
 * - Unit: ~5 seconds
 * - Integration: ~15 seconds
 * - E2E: ~2 minutes
 *
 * Trade-offs:
 * ✅ Fast feedback from unit tests
 * ✅ Confident in component interactions
 * ✅ Critical paths validated end-to-end
 * ⚠️ E2E tests slower but necessary
 * ⚠️ Maintenance overhead for 3 test types
 */

⭐⭐⭐⭐⭐ Bài 5: Production Testing Suite (90 phút)

jsx
/**
 * 🎯 Mục tiêu: Build production-ready test suite
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 * Multi-user chat application
 * - Real-time messaging
 * - User presence (online/offline)
 * - Typing indicators
 * - Message read receipts
 * - Channel switching
 *
 * 🏗️ Technical Design Doc:
 * 1. Component Architecture:
 *    - MessageList (displays messages)
 *    - MessageInput (send messages)
 *    - UserList (online users)
 *    - TypingIndicator
 *    - ChannelSelector
 *
 * 2. State Management:
 *    - ChatContext for global state
 *    - WebSocket for real-time updates (simulated)
 *
 * 3. Testing Strategy:
 *    - Unit: Individual components
 *    - Integration: Message flow, user presence
 *    - E2E concept: Complete chat session (preview only)
 *
 * ✅ Production Checklist:
 * - [ ] Unit tests for all components
 * - [ ] Integration tests for main flows
 * - [ ] Edge cases covered
 * - [ ] Error handling tested
 * - [ ] Loading states tested
 * - [ ] Accessibility verified
 * - [ ] Performance considerations
 *
 * 📝 Test Documentation:
 * - Clear test descriptions
 * - Grouped by feature
 * - Setup helpers for reusability
 */

// TODO: Design and implement comprehensive test suite
// Consider:
// - What needs unit tests?
// - What needs integration tests?
// - How to simulate real-time updates?
// - Edge cases (network errors, empty states, concurrent users)
// - Performance (many messages, many users)
💡 Solution
jsx
import {
  render,
  screen,
  fireEvent,
  waitFor,
  within,
} from '@testing-library/react';
import { rest } from 'msw';
import { server } from './mocks/server';

/**
 * PRODUCTION CHAT APP - COMPREHENSIVE TEST SUITE
 */

// ============================================
// TEST HELPERS & SETUP
// ============================================

const mockMessages = [
  { id: '1', text: 'Hello!', sender: 'Alice', timestamp: Date.now() - 5000 },
  { id: '2', text: 'Hi there', sender: 'Bob', timestamp: Date.now() - 3000 },
];

const mockUsers = [
  { id: '1', name: 'Alice', status: 'online' },
  { id: '2', name: 'Bob', status: 'online' },
  { id: '3', name: 'Charlie', status: 'offline' },
];

const renderChat = (props = {}) => {
  return render(
    <ChatProvider
      currentUser='Alice'
      {...props}
    >
      <ChannelSelector />
      <MessageList />
      <MessageInput />
      <UserList />
      <TypingIndicator />
    </ChatProvider>,
  );
};

// ============================================
// UNIT TESTS
// ============================================

describe('Chat Components - Unit Tests', () => {
  describe('MessageList', () => {
    test('displays messages correctly', () => {
      render(
        <MessageList
          messages={mockMessages}
          currentUser='Alice'
        />,
      );

      expect(screen.getByText(/hello!/i)).toBeInTheDocument();
      expect(screen.getByText(/hi there/i)).toBeInTheDocument();
    });

    test('highlights own messages', () => {
      render(
        <MessageList
          messages={mockMessages}
          currentUser='Alice'
        />,
      );

      const aliceMessage = screen
        .getByText(/hello!/i)
        .closest('[role="listitem"]');
      const bobMessage = screen
        .getByText(/hi there/i)
        .closest('[role="listitem"]');

      expect(aliceMessage).toHaveClass('own-message');
      expect(bobMessage).not.toHaveClass('own-message');
    });

    test('shows empty state when no messages', () => {
      render(
        <MessageList
          messages={[]}
          currentUser='Alice'
        />,
      );

      expect(screen.getByText(/no messages yet/i)).toBeInTheDocument();
    });
  });

  describe('MessageInput', () => {
    test('sends message on submit', () => {
      const mockSend = jest.fn();
      render(<MessageInput onSendMessage={mockSend} />);

      const input = screen.getByPlaceholderText(/type a message/i);
      fireEvent.change(input, { target: { value: 'Test message' } });
      fireEvent.submit(input.closest('form'));

      expect(mockSend).toHaveBeenCalledWith('Test message');
      expect(input).toHaveValue(''); // Input cleared
    });

    test('disables send button when empty', () => {
      render(<MessageInput onSendMessage={() => {}} />);

      const button = screen.getByRole('button', { name: /send/i });
      expect(button).toBeDisabled();

      fireEvent.change(screen.getByPlaceholderText(/type/i), {
        target: { value: 'Text' },
      });

      expect(button).not.toBeDisabled();
    });
  });

  describe('UserList', () => {
    test('displays online and offline users separately', () => {
      render(<UserList users={mockUsers} />);

      const onlineSection = screen.getByRole('region', { name: /online/i });
      const offlineSection = screen.getByRole('region', { name: /offline/i });

      expect(within(onlineSection).getByText('Alice')).toBeInTheDocument();
      expect(within(onlineSection).getByText('Bob')).toBeInTheDocument();
      expect(within(offlineSection).getByText('Charlie')).toBeInTheDocument();
    });

    test('shows user count', () => {
      render(<UserList users={mockUsers} />);

      expect(screen.getByText(/2 online/i)).toBeInTheDocument();
    });
  });
});

// ============================================
// INTEGRATION TESTS
// ============================================

describe('Chat App - Integration Tests', () => {
  test('sending message updates message list', async () => {
    server.use(
      rest.post('/api/messages', (req, res, ctx) => {
        return res(
          ctx.json({
            id: '3',
            text: req.body.text,
            sender: 'Alice',
            timestamp: Date.now(),
          }),
        );
      }),
    );

    renderChat();

    // Initially 2 messages
    expect(screen.getAllByRole('listitem')).toHaveLength(2);

    // Send new message
    const input = screen.getByPlaceholderText(/type/i);
    fireEvent.change(input, { target: { value: 'New message' } });
    fireEvent.submit(input.closest('form'));

    // Message appears in list
    await screen.findByText(/new message/i);
    expect(screen.getAllByRole('listitem')).toHaveLength(3);
  });

  test('typing indicator shows when user is typing', async () => {
    renderChat();

    // Initially no typing indicator
    expect(screen.queryByText(/typing/i)).not.toBeInTheDocument();

    // Start typing
    const input = screen.getByPlaceholderText(/type/i);
    fireEvent.change(input, { target: { value: 'T' } });

    // Simulate receiving typing event from WebSocket
    // (In real app, this would come from WebSocket)
    const typingEvent = new CustomEvent('user-typing', {
      detail: { user: 'Bob' },
    });
    window.dispatchEvent(typingEvent);

    // Typing indicator appears
    await screen.findByText(/bob is typing/i);

    // Stop typing after delay
    await waitFor(
      () => {
        expect(screen.queryByText(/typing/i)).not.toBeInTheDocument();
      },
      { timeout: 3000 },
    );
  });

  test('switching channels loads different messages', async () => {
    server.use(
      rest.get('/api/channels/:channelId/messages', (req, res, ctx) => {
        const { channelId } = req.params;

        if (channelId === 'general') {
          return res(ctx.json(mockMessages));
        } else if (channelId === 'random') {
          return res(
            ctx.json([
              {
                id: '10',
                text: 'Random msg',
                sender: 'Charlie',
                timestamp: Date.now(),
              },
            ]),
          );
        }
      }),
    );

    renderChat();

    // Initially in #general
    expect(screen.getByText(/hello!/i)).toBeInTheDocument();

    // Switch to #random
    fireEvent.click(screen.getByRole('button', { name: /#random/i }));

    // Messages change
    await screen.findByText(/random msg/i);
    expect(screen.queryByText(/hello!/i)).not.toBeInTheDocument();
  });

  test('user presence updates in real-time', async () => {
    renderChat();

    // Initially 2 online
    expect(screen.getByText(/2 online/i)).toBeInTheDocument();

    // Simulate user going offline
    const offlineEvent = new CustomEvent('user-status', {
      detail: { user: 'Bob', status: 'offline' },
    });
    window.dispatchEvent(offlineEvent);

    // Count updates
    await screen.findByText(/1 online/i);

    // Bob moves to offline section
    const offlineSection = screen.getByRole('region', { name: /offline/i });
    expect(within(offlineSection).getByText('Bob')).toBeInTheDocument();
  });

  test('error handling for failed message send', async () => {
    server.use(
      rest.post('/api/messages', (req, res, ctx) => {
        return res(ctx.status(500), ctx.json({ error: 'Server error' }));
      }),
    );

    renderChat();

    const input = screen.getByPlaceholderText(/type/i);
    fireEvent.change(input, { target: { value: 'This will fail' } });
    fireEvent.submit(input.closest('form'));

    // Error message appears
    const error = await screen.findByRole('alert');
    expect(error).toHaveTextContent(/failed to send/i);

    // Message NOT added to list
    expect(screen.queryByText(/this will fail/i)).not.toBeInTheDocument();

    // Input NOT cleared (so user can retry)
    expect(input).toHaveValue('This will fail');
  });

  test('message read receipts update correctly', async () => {
    renderChat();

    // Send message
    fireEvent.change(screen.getByPlaceholderText(/type/i), {
      target: { value: 'Read this' },
    });
    fireEvent.submit(screen.getByPlaceholderText(/type/i).closest('form'));

    const message = await screen.findByText(/read this/i);
    const messageItem = message.closest('[role="listitem"]');

    // Initially unread (single checkmark)
    expect(
      within(messageItem).getByTestId('checkmark-single'),
    ).toBeInTheDocument();

    // Simulate read receipt from server
    const readEvent = new CustomEvent('message-read', {
      detail: { messageId: '3', readBy: ['Bob'] },
    });
    window.dispatchEvent(readEvent);

    // Double checkmark appears
    await waitFor(() => {
      expect(
        within(messageItem).getByTestId('checkmark-double'),
      ).toBeInTheDocument();
    });
  });
});

// ============================================
// E2E TEST CONCEPTS (PREVIEW ONLY)
// ============================================

/**
 * E2E Test with Playwright - Multi-user chat session
 *
 * test('two users can chat in real-time', async ({ browser }) => {
 *   // Open two browser contexts (two users)
 *   const context1 = await browser.newContext();
 *   const context2 = await browser.newContext();
 *
 *   const user1 = await context1.newPage();
 *   const user2 = await context2.newPage();
 *
 *   // User 1 logs in
 *   await user1.goto('/chat');
 *   await user1.fill('[name="username"]', 'Alice');
 *   await user1.click('button:has-text("Join")');
 *
 *   // User 2 logs in
 *   await user2.goto('/chat');
 *   await user2.fill('[name="username"]', 'Bob');
 *   await user2.click('button:has-text("Join")');
 *
 *   // User 1 sees User 2 online
 *   await expect(user1.locator('text=Bob')).toBeVisible();
 *   await expect(user1.locator('text=1 online')).toBeVisible();
 *
 *   // User 1 sends message
 *   await user1.fill('[placeholder="Type a message"]', 'Hi Bob!');
 *   await user1.press('[placeholder="Type a message"]', 'Enter');
 *
 *   // User 2 receives message
 *   await expect(user2.locator('text=Hi Bob!')).toBeVisible();
 *
 *   // User 2 sees typing indicator
 *   await user1.fill('[placeholder="Type a message"]', 'How are you?');
 *   await expect(user2.locator('text=Alice is typing')).toBeVisible();
 *
 *   // User 2 replies
 *   await user2.fill('[placeholder="Type a message"]', 'Good thanks!');
 *   await user2.press('[placeholder="Type a message"]', 'Enter');
 *
 *   // User 1 receives reply
 *   await expect(user1.locator('text=Good thanks!')).toBeVisible();
 *
 *   // Cleanup
 *   await context1.close();
 *   await context2.close();
 * });
 */

// ============================================
// EDGE CASES & PERFORMANCE
// ============================================

describe('Edge Cases & Performance', () => {
  test('handles many messages efficiently', () => {
    const manyMessages = Array.from({ length: 1000 }, (_, i) => ({
      id: String(i),
      text: `Message ${i}`,
      sender: i % 2 === 0 ? 'Alice' : 'Bob',
      timestamp: Date.now() - i * 1000,
    }));

    const { container } = render(
      <MessageList
        messages={manyMessages}
        currentUser='Alice'
      />,
    );

    // Should use virtualization for performance
    // Only visible messages should be rendered
    const renderedMessages = container.querySelectorAll('[role="listitem"]');
    expect(renderedMessages.length).toBeLessThan(100); // Virtual scrolling
  });

  test('handles network disconnection gracefully', async () => {
    renderChat();

    // Simulate network error
    server.use(
      rest.post('/api/messages', (req, res) => {
        return res.networkError('Failed to connect');
      }),
    );

    fireEvent.change(screen.getByPlaceholderText(/type/i), {
      target: { value: 'Offline message' },
    });
    fireEvent.submit(screen.getByPlaceholderText(/type/i).closest('form'));

    // Shows connection error
    await screen.findByText(/connection lost/i);

    // Message queued for retry
    expect(screen.getByText(/queued for sending/i)).toBeInTheDocument();
  });

  test('prevents duplicate message submission', async () => {
    const mockSend = jest
      .fn()
      .mockImplementation(
        () => new Promise((resolve) => setTimeout(resolve, 1000)),
      );

    render(<MessageInput onSendMessage={mockSend} />);

    const input = screen.getByPlaceholderText(/type/i);
    fireEvent.change(input, { target: { value: 'Test' } });

    // Submit multiple times quickly
    const form = input.closest('form');
    fireEvent.submit(form);
    fireEvent.submit(form);
    fireEvent.submit(form);

    // Should only be called once
    await waitFor(() => {
      expect(mockSend).toHaveBeenCalledTimes(1);
    });
  });
});

// ============================================
// TEST SUMMARY
// ============================================

/**
 * Total Tests: 20
 *
 * Unit Tests: 10 (50%)
 * - MessageList: 3 tests
 * - MessageInput: 2 tests
 * - UserList: 2 tests
 * - TypingIndicator: 1 test
 * - ChannelSelector: 2 tests
 *
 * Integration Tests: 7 (35%)
 * - Message flow: 2 tests
 * - Typing indicators: 1 test
 * - Channel switching: 1 test
 * - User presence: 1 test
 * - Error handling: 1 test
 * - Read receipts: 1 test
 *
 * Edge Cases: 3 (15%)
 * - Performance (1000+ messages): 1 test
 * - Network errors: 1 test
 * - Duplicate prevention: 1 test
 *
 * Coverage:
 * ✅ Core functionality
 * ✅ Real-time updates
 * ✅ Error states
 * ✅ Edge cases
 * ✅ Performance
 * ✅ Accessibility (implicit in query choices)
 */

📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)

Bảng So Sánh Testing Levels

Test TypeScopeSpeedConfidenceMaintenanceWhen to Use
UnitSingle component/function⚡⚡⚡ Very Fast⭐⭐ Medium✅ EasyIndividual logic, utilities, pure components
IntegrationMultiple components⚡⚡ Fast⭐⭐⭐ High⚠️ MediumData flow, component communication
E2EFull application🐌 Slow⭐⭐⭐⭐ Very High❌ HardCritical user journeys, cross-browser

Trade-offs Matrix

ApproachProsConsCostROI
Only Unit TestsFast feedback, easy debugMiss integration bugsLowLow-Medium
Only E2E TestsRealistic, high confidenceSlow, flaky, hard to maintainVery HighMedium
Balanced PyramidFast + Confident + MaintainableNeed expertise in all 3 typesMediumHigh
Heavy IntegrationGood coverage, reasonable speedSome duplication with unit/E2EMediumHigh

Decision Tree: Which Test Type?

START: Need to test feature

├─ Testing single function/component in isolation?
│  └─ YES → UNIT TEST
│     Examples:
│     - Pure function calculations
│     - Component rendering with props
│     - Event handler logic

├─ Testing data flow between components?
│  └─ YES → INTEGRATION TEST
│     Examples:
│     - Form submission updating display
│     - Cart adding product from list
│     - Auth flow login → profile

├─ Testing complete user journey?
│  └─ YES → Consider complexity
│     ├─ Simple flow, no backend → INTEGRATION TEST
│     │   Examples:
│     │   - Multi-step form
│     │   - Filtered list
│     │
│     └─ Complex flow, needs backend → E2E TEST
│         Examples:
│         - Complete purchase (payment gateway)
│         - Multi-user interaction
│         - Cross-browser compatibility

RULE OF THUMB:
- Can mock everything? → Unit
- Need some real components? → Integration
- Need real backend/browser? → E2E

Testing Strategy Decision Matrix

ScenarioUnitIntegrationE2ERationale
Utility functionPure logic, no dependencies
Styled buttonVisual component, props only
Form validationUnit for rules, Integration for UX
Shopping cart⚠️Unit for calc, Integration for flow, E2E for checkout
Login flowAll 3! Critical path
Payment processing⚠️Too complex for unit, needs real gateway
Real-time chatComplex feature, needs all levels

🧪 PHẦN 5: DEBUG LAB (20 phút)

Bug 1: Integration Test Với Dependencies Sai

jsx
// Bug: Integration test vẫn mock quá nhiều

// ❌ Code bị lỗi (fake integration test)
test('user can login', () => {
  // Mock EVERYTHING
  const mockLogin = jest.fn().mockResolvedValue({ success: true });
  const mockSetUser = jest.fn();

  jest.spyOn(React, 'useContext').mockReturnValue({
    login: mockLogin,
    setUser: mockSetUser,
  });

  render(<LoginForm />);
  render(<UserProfile />);

  fireEvent.submit(screen.getByRole('form'));

  expect(mockLogin).toHaveBeenCalled();
  // Problem: Components không thực sự talk to each other!
});

// 🤔 DEBUG QUESTIONS:
// 1. Tại sao test này KHÔNG phải integration test?
// 2. Components có thực sự interact không?
// 3. Nếu AuthContext bị lỗi, test có catch được không?

// ✅ FIX: Real integration test
test('user can login', async () => {
  // Mock CHỈ external API
  server.use(
    rest.post('/api/login', (req, res, ctx) => {
      return res(ctx.json({ user: { name: 'John' } }));
    }),
  );

  // Render với REAL provider
  render(
    <AuthProvider>
      <LoginForm />
      <UserProfile />
    </AuthProvider>,
  );

  // Initially not logged in
  expect(screen.getByText(/please log in/i)).toBeInTheDocument();

  // Fill and submit
  fireEvent.change(screen.getByLabelText(/email/i), {
    target: { value: 'john@example.com' },
  });
  fireEvent.change(screen.getByLabelText(/password/i), {
    target: { value: 'password' },
  });
  fireEvent.click(screen.getByRole('button', { name: /login/i }));

  // Profile updates (REAL integration!)
  await screen.findByText(/welcome, john/i);
});

// 💡 LESSON:
// Integration test = Real components + Real context
// Only mock external dependencies (API, WebSocket, etc.)
// If you mock too much, it's just a complicated unit test

Bug 2: Không Test Toàn Bộ Flow

jsx
// Bug: Test incomplete integration

// ❌ Test thiếu sót
test('adding product to cart', () => {
  render(
    <CartProvider>
      <ProductList />
      <Cart />
    </CartProvider>,
  );

  fireEvent.click(screen.getByRole('button', { name: /add laptop/i }));

  // Only check cart count
  expect(screen.getByText(/1 item/i)).toBeInTheDocument();

  // Missing: Product details, price, quantity, remove button, total
});

// 🤔 DEBUG QUESTIONS:
// 1. Test pass nhưng cart có bug, tại sao?
// 2. Missing verification nào?
// 3. User có thể remove product không?

// ✅ FIX: Complete flow verification
test('adding product to cart - complete flow', () => {
  const laptop = { id: '1', name: 'Laptop', price: 999 };

  render(
    <CartProvider>
      <ProductList products={[laptop]} />
      <Cart />
    </CartProvider>,
  );

  // Initial state
  expect(screen.getByText(/cart is empty/i)).toBeInTheDocument();
  expect(screen.queryByText(/total/i)).not.toBeInTheDocument();

  // Add product
  fireEvent.click(screen.getByRole('button', { name: /add laptop/i }));

  // Verify complete cart state
  expect(screen.getByText(/1 item/i)).toBeInTheDocument();
  expect(screen.getByText(/laptop/i)).toBeInTheDocument();
  expect(screen.getByText(/\$999/i)).toBeInTheDocument();
  expect(screen.getByText(/total: \$999/i)).toBeInTheDocument();

  // Verify actions available
  expect(screen.getByRole('button', { name: /remove/i })).toBeInTheDocument();
  expect(screen.getByRole('button', { name: /checkout/i })).not.toBeDisabled();

  // Add same product again (quantity should increase)
  fireEvent.click(screen.getByRole('button', { name: /add laptop/i }));

  expect(screen.getByText(/2 items/i)).toBeInTheDocument();
  expect(screen.getByText(/total: \$1998/i)).toBeInTheDocument();

  // Remove product
  fireEvent.click(screen.getByRole('button', { name: /remove/i }));

  expect(screen.getByText(/1 item/i)).toBeInTheDocument();
  expect(screen.getByText(/total: \$999/i)).toBeInTheDocument();
});

// 💡 LESSON:
// Integration tests should verify COMPLETE user flow:
// 1. Initial state
// 2. Action
// 3. All affected components update
// 4. Related actions still work
// 5. Can undo/reverse action

Bug 3: E2E vs Integration Confusion

jsx
// Bug: Calling integration test "E2E"

// ❌ Misleading test name
test('E2E: complete checkout flow', () => {
  // This is NOT E2E!
  render(
    <App>
      <ProductList />
      <Cart />
      <Checkout />
    </App>,
  );

  // ... RTL interactions
});

// 🤔 DEBUG QUESTIONS:
// 1. Tại sao test này KHÔNG phải E2E?
// 2. Khác biệt gì với integration test?
// 3. Khi nào cần REAL E2E test?

// ✅ CORRECT: This is integration test
test('Integration: checkout flow', () => {
  server.use(
    rest.post('/api/checkout', (req, res, ctx) => {
      return res(ctx.json({ orderId: '123' }));
    }),
  );

  render(
    <CartProvider>
      <ProductList />
      <Cart />
      <Checkout />
    </CartProvider>,
  );

  // ... test flow with RTL
});

// ✅ REAL E2E Test (Playwright)
/**
 * test('E2E: complete checkout flow', async ({ page }) => {
 *   // Real browser
 *   await page.goto('http://localhost:3000');
 *
 *   // Real clicks
 *   await page.click('button:has-text("Add to Cart")');
 *   await page.click('text=Checkout');
 *
 *   // Real payment gateway
 *   await page.fill('[name="cardNumber"]', '4242424242424242');
 *   await page.click('button:has-text("Place Order")');
 *
 *   // Real backend, real database
 *   await expect(page).toHaveURL(/\/success/);
 * });
 */

// 💡 LESSON:
// Integration Test:
// - RTL + JSDOM
// - Mocked APIs
// - Multiple components
// - Fast (~seconds)
//
// E2E Test:
// - Real browser (Playwright/Cypress)
// - Real APIs
// - Real backend
// - Slow (~minutes)
//
// Don't confuse them! Different tools, different purposes.

✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)

Knowledge Check

jsx
// Tự đánh giá kiến thức:

// 1. Testing Levels
[ ] Tôi phân biệt được Unit vs Integration vs E2E
[ ] Tôi biết khi nào dùng test type nào
[ ] Tôi hiểu test pyramid (70/20/10)

// 2. Integration Testing
[ ] Tôi biết cách test multiple components together
[ ] Tôi render với real providers (Context, etc.)
[ ] Tôi chỉ mock external dependencies
[ ] Tôi verify toàn bộ user flow

// 3. E2E Concepts
[ ] Tôi hiểu E2E test hoạt động như thế nào
[ ] Tôi biết khi nào cần E2E test
[ ] Tôi hiểu tools (Playwright/Cypress)

// 4. Testing Strategy
[ ] Tôi biết cách phân bổ tests (70/20/10)
[ ] Tôi xác định được test type cho feature
[ ] Tôi cân nhắc trade-offs (speed vs confidence)

// 5. Best Practices
[ ] Tests của tôi test behavior, not implementation
[ ] Tôi verify complete flows
[ ] Tôi handle edge cases
[ ] Tôi document test purpose clearly

// 6. Debugging
[ ] Tôi phát hiện được fake integration tests
[ ] Tôi verify complete flows (not just happy path)
[ ] Tôi không confuse integration với E2E

Code Review Checklist

jsx
// Review integration tests:

INTEGRATION TEST QUALITY
[ ] Multiple components rendered together
[ ] Real Context providers used
[ ] Only external APIs mocked
[ ] Complete user flow verified
[ ] Both happy path và error cases

TEST COVERAGE
[ ] Unit tests: Individual components (70%)
[ ] Integration tests: Feature flows (20%)
[ ] E2E tests: Critical paths (10%)
[ ] Edge cases covered
[ ] Error states tested

TEST DESIGN
[ ] Each test focused on one scenario
[ ] Clear test descriptions
[ ] Proper setup/teardown
[ ] Reusable test helpers
[ ] No implementation details leaked

ASSERTIONS
[ ] Verify all affected components
[ ] Check initial state
[ ] Check final state
[ ] Check intermediate states (loading, etc.)
[ ] Use appropriate queries

MAINTAINABILITY
[ ] Tests survive refactoring
[ ] Easy to understand what's being tested
[ ] Failures provide clear error messages
[ ] Setup không quá complex

🏠 BÀI TẬP VỀ NHÀ

Bắt buộc (30 phút)

Bài 1: Authentication Integration Test

jsx
/**
 * Test complete auth flow:
 * - Login updates header
 * - Protected routes work
 * - Logout clears user data
 */

// Components:
// - Header (shows user name when logged in)
// - LoginForm
// - ProtectedPage (only shows when authenticated)

// TODO: Write integration tests for:
// 1. Login → Header shows username → ProtectedPage visible
// 2. Logout → Header shows login button → ProtectedPage hidden
// 3. Failed login → Error message → User still logged out

Nâng cao (60 phút)

Bài 2: E-commerce Integration Suite

jsx
/**
 * Build comprehensive test suite for e-commerce flow
 *
 * Features:
 * - Product browsing
 * - Cart management
 * - Checkout process
 * - Order confirmation
 *
 * Requirements:
 * 1. Design testing strategy (Unit vs Integration)
 * 2. Write unit tests for individual components
 * 3. Write integration tests for main flows
 * 4. Document why each test type was chosen
 * 5. Include edge cases and error handling
 */

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. Testing Library - Integration Testing

  2. Kent C. Dodds - Write Tests

Đọc thêm

  1. Playwright Documentation

  2. Testing Trophy vs Pyramid

  3. Integration Test Best Practices


🔗 KẾT NỐI KIẾN THỨC

Kiến thức nền

  • Ngày 54: RTL Basics
    • render, screen, queries
    • fireEvent, assertions
  • Ngày 55: Testing Hooks & Context
    • renderHook, wrapper pattern
  • Ngày 56: Mocking API Calls
    • MSW, async testing
  • Ngày 53: Testing Philosophy
    • Test pyramid
    • Behavior vs implementation

Hướng tới

  • Ngày 58: TypeScript Fundamentals
    • Type-safe tests
    • Typing test helpers
  • Ngày 59: TypeScript Advanced
    • Generic test utilities
  • Module riêng: E2E Testing Professional
    • Playwright deep dive
    • Visual regression
    • Performance testing

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Testing Budget

Startup (limited resources):
- 80% Unit tests (fast feedback)
- 15% Integration tests (critical flows)
- 5% E2E tests (must-work paths)

Enterprise (quality critical):
- 60% Unit tests
- 25% Integration tests
- 15% E2E tests (comprehensive coverage)

Mission Critical (banking, healthcare):
- 50% Unit tests
- 30% Integration tests
- 20% E2E tests (compliance required)

2. Test Execution Strategy

jsx
// CI/CD Pipeline

// PR Check (Fast - 2 minutes)
- All unit tests
- Critical integration tests
- Lint + TypeScript

// Main Branch (Medium - 10 minutes)
- All unit tests
- All integration tests
- Visual regression
- A11y checks

// Nightly (Comprehensive - 1 hour)
- All unit tests
- All integration tests
- All E2E tests
- Performance benchmarks
- Cross-browser tests
- Security scans

// Before Release (Full - 2-3 hours)
- Everything above
- Manual exploratory testing
- Staging environment validation

3. When E2E is Worth It

✅ WRITE E2E TEST:
- Payment processing
- User registration/authentication
- Critical checkout flows
- Multi-step wizards
- Cross-browser issues suspected
- Third-party integrations (Stripe, etc.)

❌ SKIP E2E TEST:
- Simple forms
- Static content
- UI styling
- Isolated components
- Logic already covered by integration tests

Câu Hỏi Phỏng Vấn

Junior Level:

Q1: "Unit test và integration test khác nhau như thế nào?"

Expected Answer:
- Unit: Test component isolated, mock dependencies
- Integration: Test multiple components, real providers
- Example:
  * Unit: <Button onClick={mock} /> isolated
  * Integration: <Form> + <Button> + Context working together
- Unit faster, Integration more confident

Q2: "Khi nào nên dùng integration test thay vì unit test?"

Expected Answer:
- When testing data flow between components
- When testing Context + consumers
- When testing forms với multiple fields
- When user flow spans multiple components
- Example: Login form updating user profile

Mid Level:

Q3: "Làm sao design testing strategy cho new feature?"

Expected Answer:
1. Identify components involved
2. Map user flows
3. Decide test types:
   - Unit: Individual component logic
   - Integration: Component interactions
   - E2E: Critical end-to-end flows
4. Consider:
   - Feature criticality
   - Team resources
   - CI/CD time budget
5. Start with integration tests for main flow
6. Add unit tests for edge cases
7. E2E only for must-work paths

Q4: "Integration test chạy chậm, làm sao optimize?"

Expected Answer:
Strategies:
1. Reduce unnecessary renders
   - Render only components needed
   - Don't render entire app tree
2. Parallel execution
   - jest --maxWorkers
   - Independent test files
3. Setup optimization
   - beforeAll vs beforeEach
   - Reuse providers where possible
4. Mock heavy operations
   - Image loading
   - Animations
5. Smart test organization
   - Group related tests
   - Share setup code

Example:
// ❌ Slow
beforeEach(() => {
  render(<EntireApp />);
});

// ✅ Fast
beforeEach(() => {
  render(
    <Provider>
      <FeatureUnderTest />
    </Provider>
  );
});

Senior Level:

Q5: "Testing strategy cho distributed team với CI/CD constraints?"

Expected Answer:

Constraints:
- Limited CI time (15 min max)
- Multiple timezones (can't wait for feedback)
- Frequent deployments (10x/day)

Solution:
1. Tiered Testing:

PR Check (2 min):

  • Unit tests
  • Critical integration tests
  • Fast linting

Main (10 min):

  • Full unit + integration
  • Smoke E2E tests

Nightly:

  • Comprehensive E2E
  • Performance
  • Visual regression

2. Test Sharding:
- Split tests across parallel jobs
- 5 parallel jobs = 5x faster

3. Smart Test Selection:
- Only run affected tests (git diff)
- Skip unchanged areas

4. Fast Feedback Loops:
- Local pre-commit hooks
- Watch mode during development
- Clear failure messages

5. Quality Gates:
- Block merge on unit test failures
- Warning on integration failures
- Manual review for E2E failures

Trade-offs:
✅ Fast feedback
✅ Frequent deployments
⚠️ Some E2E tests delayed
⚠️ Need good monitoring in production

War Stories

Story 1: The Integration Test That Wasn't

Situation:
Team had 200 "integration tests". CI took 45 minutes.
New dev complained tests too slow.

Investigation:
test('integration: user can login', () => {
  const mockLogin = jest.fn();
  const mockContext = jest.fn();

  jest.mock('./AuthContext');
  jest.mock('./api');

  render(<LoginForm />);
  // Everything mocked!
});

Problem:
- Everything mocked = just complicated unit tests
- Not testing real integration
- False sense of coverage

Solution:
1. Audit all "integration" tests
2. Found 150 were actually unit tests (overcomplicated)
3. Renamed/moved to unit test suite
4. Rewrote 50 as real integration tests
5. CI time dropped to 12 minutes
6. Actually caught integration bugs

Lesson:
"If you mock everything, it's not an integration test.
Integration = Real components + Real context + Mocked APIs only."

Story 2: E2E Test Hell

Situation:
Company decided "E2E tests for everything!"
Wrote 500 E2E tests with Cypress.
CI took 3 hours. Tests flaky (50% fail rate).

Problems:
- E2E for simple forms (overkill)
- No test pyramid strategy
- Flaky due to network/timing
- Impossible to maintain
- Blocked deployments

Recovery:
1. Analyzed test value
2. Kept 50 critical E2E tests (10%)
3. Converted 300 to integration tests (60%)
4. Converted 150 to unit tests (30%)
5. Added retry logic for real E2E
6. Parallel execution
7. Result: 15 min CI, 95% stable

Metrics:
Before:
- 500 E2E tests
- 3 hours CI time
- 50% flaky rate
- 0 deployments/week (blocked)

After:
- 400 unit tests
- 100 integration tests
- 50 E2E tests
- 15 min CI time
- 5% flaky rate
- 20 deployments/week

Lesson:
"E2E tests are expensive. Use sparingly for critical paths.
Most confidence comes from good integration tests."

Story 3: The Missing Integration

Situation:
Shopping cart feature shipped.
All unit tests passing.
Production: Cart items disappear on refresh.

Investigation:
Unit tests existed for:
- CartContext ✅
- ProductList ✅
- Cart display ✅

But NO integration test for:
- CartContext + localStorage
- Product add → Cart update
- Page refresh → Cart restore

Problem:
- Components tested in isolation
- Integration assumptions not tested
- localStorage integration missing

Fix:
1. Added integration tests:
test('cart persists on refresh', () => {
  render(
    <CartProvider>
      <ProductList />
      <Cart />
    </CartProvider>
  );

  // Add item
  fireEvent.click(screen.getByText(/add laptop/i));

  // Simulate refresh
  rerender(
    <CartProvider>
      <Cart />
    </CartProvider>
  );

  // Should still be there
  expect(screen.getByText(/laptop/i)).toBeInTheDocument();
});

2. Caught the bug immediately
3. Fixed localStorage integration
4. Added to regression suite

Lesson:
"Unit tests prove components work alone.
Integration tests prove they work together.
You need both."

🎯 Preview Ngày Mai

Ngày 58: TypeScript Fundamentals cho React

Ngày mai chúng ta sẽ học:

  • TypeScript setup với React project
  • Typing component props
  • Event typing
  • Children prop typing
  • React.FC vs function declarations

Concepts mới:

  • interfacetype cho props
  • Generic components
  • TypeScript với hooks
  • Type inference trong React

Chuẩn bị:

  • Review tất cả components đã viết
  • Suy nghĩ: Props nào có thể có type errors?
  • Cài đặt TypeScript nếu chưa có

Sau 4 ngày testing intensive, giờ là lúc make code type-safe! 🚀

Hẹn gặp lại!

Personal tech knowledge base