+3

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

Viblo
Let's register a Viblo Account to get more interesting posts.