Triết lý RTL: "test như cách người dùng dùng app", không test nội bộ.
Thứ tự ưu tiên query (từ tốt nhất xuống):
1. Accessible — getByRole (kèm name), getByLabelText, getByPlaceholderText, getByText. Phản ánh đúng cách user và screen reader nhìn UI.
2. Semantic — getByAltText, getByTitle.
3. Test ID — getByTestId, chỉ khi không có cách nào ở trên.
Ưu tiên getByRole('button', { name: /lưu/i }) thay vì bám class hay cấu trúc DOM.
user-event vs fireEvent: dùng @testing-library/user-event — nó mô phỏng chuỗi sự kiện thật (hover → focus → keydown → input → keyup) thay vì bắn một event đơn lẻ như fireEvent. userEvent.click/type là async → phải await.
Tránh implementation details: đừng assert vào state nội bộ, tên hàm, hay class CSS. Test hành vi quan sát được (text hiện ra, button disabled). Như vậy refactor bên trong không làm vỡ test.
await userEvent.type(screen.getByLabelText(/email/i), 'a@b.com')
await userEvent.click(screen.getByRole('button', { name: /gửi/i }))
expect(await screen.findByText(/thành công/i)).toBeInTheDocument()RTL's philosophy: "test the way users use the app", not the internals.
Query priority (best to last resort):
1. Accessible — getByRole (with name), getByLabelText, getByPlaceholderText, getByText. Mirrors how users and screen readers see the UI.
2. Semantic — getByAltText, getByTitle.
3. Test ID — getByTestId, only when nothing above works.
Prefer getByRole('button', { name: /save/i }) over coupling to a class or DOM structure.
user-event vs fireEvent: use @testing-library/user-event — it simulates the real event sequence (hover → focus → keydown → input → keyup) instead of firing a single event like fireEvent. userEvent.click/type are async → must await.
Avoid implementation details: don't assert on internal state, function names, or CSS classes. Test observable behavior (text appearing, a button disabled) so internal refactors don't break tests.
await userEvent.type(screen.getByLabelText(/email/i), 'a@b.com')
await userEvent.click(screen.getByRole('button', { name: /submit/i }))
expect(await screen.findByText(/success/i)).toBeInTheDocument()