Từ Zero đến Principal Frontend Engineer (P7: Chiến lược Testing Frontend - Từ Unit Test đến E2E)
Phần 1: Tại Sao Testing Là "Non-Negotiable"?
1.1 Hậu quả của Poor Testing
- Case study: Một công ty mất $2.4 triệu do bug checkout không được phát hiện vì thiếu integration test.
- Số liệu: 40% bug frontend đến từ state management sai (Redux, Context API) – có thể ngăn chặn bằng unit test.
1.2 Tư Duy Testing
- Testing như documentation: Mỗi test case phải mô tả business requirement.
- Cost of change: Chi phí sửa bug tăng gấp 10 lần nếu phát hiện muộn (theo IBM Systems Sciences Institute).
Phần 2: Chi Tiết Từng Tầng Testing Pyramid
2.1 Unit Testing: Nền Tảng Của Reliability
Công cụ tối ưu:
- Jest (cho React/Vue) hoặc Vitest (tốc độ cực nhanh với Vite).
- React Testing Library (khuyến khích test behavior thay vì implementation).
Ví dụ Phức Tạp: Test Custom Hook với Async Logic
// useAuthHook.js
export const useAuth = () => {
const [user, setUser] = useState(null);
const login = async (email, password) => {
const response = await api.login(email, password);
setUser(response.data.user);
};
return { user, login };
};
// useAuthHook.test.js
test('login updates user state', async () => {
jest.spyOn(api, 'login').mockResolvedValue({ data: { user: { id: 1, name: 'John' } } });
let hookResult;
function TestComponent() {
hookResult = useAuth();
return null;
}
render(<TestComponent />);
await act(async () => {
await hookResult.login('test@example.com', 'password');
});
expect(hookResult.user).toEqual({ id: 1, name: 'John' });
});
Best Practices:
- Mocking chuẩn: Sử dụng
jest.mock
cho module phức tạp (ví dụ: API calls). - Coverage có mục tiêu: 100% cho core business logic, 70% cho UI components.
2.2 Integration Testing: Kịch Bản Thực Tế
Công cụ đề xuất:
- MSW (Mock Service Worker): Mock API cực mạnh, hỗ trợ GraphQL/REST.
- Testing Library render với Redux Provider.
Ví dụ: Test Form Submit với API Call và State Update
// LoginForm.test.js
import { setupServer } from 'msw/node';
import { rest } from 'msw';
const server = setupServer(
rest.post('/api/login', (req, res, ctx) => {
return res(ctx.json({ token: 'fake-token' }));
})
);
beforeAll(() => server.listen());
afterAll(() => server.close());
test('submits login form and stores token', async () => {
const mockStore = configureMockStore();
const store = mockStore({});
render(
<Provider store={store}>
<LoginForm />
</Provider>
);
fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'test@example.com' } });
fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'password' } });
fireEvent.click(screen.getByRole('button', { name: 'Login' }));
await waitFor(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual('auth/loginSuccess');
expect(actions[0].payload).toEqual('fake-token');
});
});
Lỗi Thường Gặp:
- Quá nhiều mock: Dẫn đến test không giống production. Giải pháp: Dùng MSW để mock ở network level.
- Test quá rộng: Một test case kiểm tra cả UI + API + State. Nên tách thành nhiều test nhỏ.
2.3 E2E Testing: Tập Trung Vào Critical Paths
So Sánh Công Cụ:
Tiêu Chí | Cypress | Playwright |
---|---|---|
Tốc độ | Chậm hơn, chạy trên browser thật | Nhanh, hỗ trợ multi-browser |
Hỗ trợ Mobile | Giới hạn | Mạnh (device emulation) |
Parallel execution | Cần CI setup phức tạp | Hỗ trợ sẵn |
Ví dụ Playwright Test cho Checkout Flow:
// checkout.spec.ts
import { test, expect } from '@playwright/test';
test('complete checkout as guest', async ({ page }) => {
await page.goto('/products/1');
await page.click('text=Add to Cart');
await page.click('#cart-icon');
await page.fill('input[name="email"]', 'test@example.com');
await page.click('text=Proceed to Checkout');
await expect(page).toHaveURL('/checkout');
await page.click('text=Place Order');
await expect(page.locator('.order-confirmation')).toBeVisible();
});
Chiến Lược E2E Hiệu Quả:
- Chỉ test happy paths: Login, Checkout, Search.
- Dùng tagging:
@smoke
,@regression
để phân loại test suite. - Kết hợp với monitoring: Sentry để bắt lỗi production chưa được test.
Phần 3: Chiến Lược Duy Trì Test Suite
3.1 Tích Hợp Vào CI/CD
# GitHub Actions example
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm run test:unit -- --coverage
- run: npm run test:integration
- run: npx playwright test
- uses: actions/upload-artifact@v3
if: failure()
with:
name: playwright-report
path: playwright-report/
3.2 Đo Lường Hiệu Quả
- Số liệu cần track:
- Bug escape rate: Số bug lọt vào production / tổng bug.
- Test flakiness: Tỉ lệ test fail không ổn định.
- Tool: Jest Sonar reporter, Cypress Dashboard.
Kết Luận
- Testing không phải để đạt coverage 100%, mà để giảm rủi ro business.
- Luôn đặt câu hỏi: "Nếu test này fail, có ảnh hưởng đến user không?".
All rights reserved