Mục lục

Component Testing: Viết Test giống User dùng

Hướng dẫn thực hành test components với React Testing Library. Test hành vi (Behavior) thay vì chi tiết cài đặt. Case study: Login Form Testing.

Mục tiêu: Đảm bảo component hoạt động đúng theo góc nhìn của người dùng.

1. Các Query cơ bản (Priority)

Khi chọn phần tử DOM, hãy theo thứ tự ưu tiên này (để đảm bảo Accessibility):

  1. getByRole: getByRole('button', { name: /submit/i }) (Tốt nhất).
  2. getByLabelText: getByLabelText(/password/i) (Tốt cho Form).
  3. getByPlaceholderText: getByPlaceholderText(/email/i).
  4. getByTestId: getByTestId('custom-element') (Chỉ dùng khi bí quá).

2. Case Study: Login Form

Form có: Input Email, Password, Nút Submit. Validate email rỗng.

tsx:
// Login.tsx
export default function Login({ onSubmit }) {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!email) {
      setError("Email is required");
      return;
    }
    onSubmit({ email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input 
        id="email" 
        value={email} 
        onChange={(e) => setEmail(e.target.value)} 
      />
      {error && <div role="alert">{error}</div>}
      <button type="submit">Login</button>
    </form>
  );
}

3. Viết Test (Login.test.tsx)

tsx:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; // 👈 Giả lập hành động user chuẩn hơn fireEvent
import Login from './Login';
import { vi } from 'vitest';

describe('Login Component', () => {
  it('should show error when submitting empty email', async () => {
    // 1. Arrange
    const handleLogin = vi.fn();
    render(<Login onSubmit={handleLogin} />);
    
    // 2. Act (User bấm nút mà không nhập gì)
    const submitBtn = screen.getByRole('button', { name: /login/i });
    await userEvent.click(submitBtn);
    
    // 3. Assert (Mong đợi thấy lỗi, hàm submit KHÔNG được gọi)
    expect(await screen.findByRole('alert')).toHaveTextContent(/required/i);
    expect(handleLogin).not.toHaveBeenCalled();
  });

  it('should call onSubmit with email when valid', async () => {
    const handleLogin = vi.fn();
    render(<Login onSubmit={handleLogin} />);

    // Act (User nhập liệu)
    const emailInput = screen.getByLabelText(/email/i);
    await userEvent.type(emailInput, 'test@example.com');
    
    const submitBtn = screen.getByRole('button', { name: /login/i });
    await userEvent.click(submitBtn);

    // Assert
    expect(handleLogin).toHaveBeenCalledWith({ email: 'test@example.com' });
  });
});

4. Key Takeaways

  1. Dùng userEvent (async) thay vì fireEvent. Nó giả lập đầy đủ sự kiện (focus, keydown, keyup, change) giống trình duyệt thật hơn.
  2. Dùng findByRole (async) nếu bạn mong chờ element xuất hiện sau một khoảng thời gian (VD: sau khi API trả về).
  3. Không bao giờ test state email bên trong component. Chỉ test cái user thấy (value trong input).
Quảng cáo
mdhorizontal