📅 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)
- RTL Fundamentals (Ngày 54-55): Làm sao test component có state và props?
- MSW (Ngày 56): Tại sao nên mock API calls trong tests?
- 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
// 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ỗiRoot 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 behaviorVí dụ cụ thể: Shopping Cart
// 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ọng | Coverage != Quality |
| "E2E tests bao quát mọi thứ" | 70% unit, 20% integration, 10% E2E | E2E chậm và flaky |
| "Integration = test 2 components" | Test complete user flow | Về scope, không về số lượng |
| "Chỉ cần unit tests thôi" | Cần cả 3 loại tests | Unit không catch integration bugs |
| "E2E replace manual testing" | E2E bổ sung cho manual | Khô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
// 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
// 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)
/**
* 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:
// 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)
/**
* 🎯 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
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)
/**
* 🎯 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
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)
/**
* 🎯 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
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)
/**
* 🎯 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
/**
* 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)
/**
* 🎯 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
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 Type | Scope | Speed | Confidence | Maintenance | When to Use |
|---|---|---|---|---|---|
| Unit | Single component/function | ⚡⚡⚡ Very Fast | ⭐⭐ Medium | ✅ Easy | Individual logic, utilities, pure components |
| Integration | Multiple components | ⚡⚡ Fast | ⭐⭐⭐ High | ⚠️ Medium | Data flow, component communication |
| E2E | Full application | 🐌 Slow | ⭐⭐⭐⭐ Very High | ❌ Hard | Critical user journeys, cross-browser |
Trade-offs Matrix
| Approach | Pros | Cons | Cost | ROI |
|---|---|---|---|---|
| Only Unit Tests | Fast feedback, easy debug | Miss integration bugs | Low | Low-Medium |
| Only E2E Tests | Realistic, high confidence | Slow, flaky, hard to maintain | Very High | Medium |
| Balanced Pyramid | Fast + Confident + Maintainable | Need expertise in all 3 types | Medium | High |
| Heavy Integration | Good coverage, reasonable speed | Some duplication with unit/E2E | Medium | High |
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? → E2ETesting Strategy Decision Matrix
| Scenario | Unit | Integration | E2E | Rationale |
|---|---|---|---|---|
| Utility function | ✅ | ❌ | ❌ | Pure logic, no dependencies |
| Styled button | ✅ | ❌ | ❌ | Visual component, props only |
| Form validation | ✅ | ✅ | ❌ | Unit for rules, Integration for UX |
| Shopping cart | ✅ | ✅ | ⚠️ | Unit for calc, Integration for flow, E2E for checkout |
| Login flow | ✅ | ✅ | ✅ | All 3! Critical path |
| Payment processing | ❌ | ⚠️ | ✅ | Too complex for unit, needs real gateway |
| Real-time chat | ✅ | ✅ | ✅ | Complex feature, needs all levels |
🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Integration Test Với Dependencies Sai
// 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 testBug 2: Không Test Toàn Bộ Flow
// 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 actionBug 3: E2E vs Integration Confusion
// 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
// 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 E2ECode Review Checklist
// 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
/**
* 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 outNâng cao (60 phút)
Bài 2: E-commerce Integration Suite
/**
* 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
Testing Library - Integration Testing
- https://testing-library.com/docs/example-react-intl/
- Real integration test examples
Kent C. Dodds - Write Tests
- https://kentcdodds.com/blog/write-tests
- Philosophy và strategy
Đọc thêm
Playwright Documentation
- https://playwright.dev/docs/intro
- E2E testing guide
Testing Trophy vs Pyramid
- https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications
- Alternative testing strategy
Integration Test Best Practices
- https://testing-library.com/docs/react-testing-library/example-intro/
- Patterns và anti-patterns
🔗 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
// 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 validation3. 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 testsCâ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 confidentQ2: "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 profileMid 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 pathsQ4: "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 productionWar 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:
interfacevàtypecho 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!