Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
What you'll learn
- Understand testing library fundamentals 🎯
- Apply queries and assertions in real projects 🏗️
- Debug common testing issues 🐛
- Write type-safe test code ✨
🎯 Introduction
Welcome to the exciting world of testing library best practices! 🎉 In this guide, we’ll explore how to write effective queries and assertions that make your tests rock-solid and maintainable.
You’ll discover how proper query selection and assertion techniques can transform your TypeScript testing experience. Whether you’re testing React components 🌐, Vue apps 🖥️, or any other UI framework 📚, understanding these fundamentals is essential for writing robust, reliable tests.
By the end of this tutorial, you’ll feel confident choosing the right queries and writing meaningful assertions in your own projects! Let’s dive in! 🏊♂️
📚 Understanding Testing Library Queries and Assertions
🤔 What are Testing Library Queries?
Testing library queries are like finding specific LEGO pieces in a huge collection 🧱. Think of them as your treasure-hunting tools that help you locate exactly the right element in your component’s DOM tree.
In TypeScript terms, queries are functions that return DOM elements (or null) based on different criteria. This means you can:
- ✨ Find elements by their text content
- 🚀 Locate elements by their roles (buttons, headings, etc.)
- 🛡️ Search for elements by test attributes
💡 Why Use Proper Queries?
Here’s why developers love good query practices:
- Accessibility First 🔒: Find elements the way users do
- Better Test Reliability 💻: Tests that don’t break from UI changes
- Clear Intent 📖: Readers understand what you’re testing
- Debugging Confidence 🔧: Know exactly why tests fail
Real-world example: Imagine testing a shopping cart 🛒. With proper queries, you can find the “Add to Cart” button just like a user would, not by hunting for CSS classes!
🔧 Basic Syntax and Usage
📝 Simple Query Examples
Let’s start with friendly examples:
import { render, screen } from '@testing-library/react';
import { UserCard } from './UserCard';
// 👋 Hello, Testing Library!
const user = {
name: "Sarah the Developer 👩💻",
role: "Senior Engineer",
email: "[email protected]"
};
// 🎨 Render our component
render(<UserCard user={user} />);
// 🎯 Different ways to find elements
const heading = screen.getByRole('heading', { name: /sarah/i });
const emailText = screen.getByText(/sarah@awesome.dev/i);
const editButton = screen.getByRole('button', { name: /edit/i });
💡 Explanation: Notice how we find elements by their role and content - just like a user would! The getByRole
query is your best friend.
🎯 Query Priority Order
Here’s the recommended order (from best to worst):
// 🏆 Level 1: Accessible to everyone (including screen readers)
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText(/username/i);
screen.getByPlaceholderText(/enter your name/i);
screen.getByText(/welcome back/i);
// 🥈 Level 2: Still semantic
screen.getByDisplayValue(/current value/i);
screen.getByAltText(/profile picture/i);
screen.getByTitle(/tooltip text/i);
// 🥉 Level 3: Last resort (use sparingly)
screen.getByTestId('user-card');
🎯 Pro Tip: Always start with role-based queries - they’re the most user-centric!
💡 Practical Examples
🛒 Example 1: Testing a Shopping Cart
Let’s build something real:
// 🛍️ Our shopping cart component test
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ShoppingCart } from './ShoppingCart';
describe('ShoppingCart 🛒', () => {
const mockProducts = [
{ id: '1', name: 'TypeScript Book 📘', price: 29.99 },
{ id: '2', name: 'Coffee Mug ☕', price: 12.99 }
];
test('should add items to cart successfully 🎯', async () => {
const user = userEvent.setup();
render(<ShoppingCart products={mockProducts} />);
// 🎮 Find the add button for TypeScript book
const addBookButton = screen.getByRole('button', {
name: /add typescript book to cart/i
});
// ✨ Click to add item
await user.click(addBookButton);
// 🔍 Verify item appears in cart
expect(screen.getByText(/typescript book.*\$29\.99/i)).toBeInTheDocument();
// 📊 Check cart total updates
expect(screen.getByText(/total:.*\$29\.99/i)).toBeInTheDocument();
// 🎊 Verify success message
await waitFor(() => {
expect(screen.getByText(/added to cart! 🎉/i)).toBeInTheDocument();
});
});
test('should remove items from cart 🗑️', async () => {
const user = userEvent.setup();
render(<ShoppingCart products={mockProducts} initialItems={mockProducts} />);
// 🎯 Find remove button for coffee mug
const removeButton = screen.getByRole('button', {
name: /remove coffee mug from cart/i
});
await user.click(removeButton);
// ✅ Verify item is gone
expect(screen.queryByText(/coffee mug/i)).not.toBeInTheDocument();
// 📉 Check updated total
expect(screen.getByText(/total:.*\$29\.99/i)).toBeInTheDocument();
});
});
🎯 Try it yourself: Add tests for quantity updates and empty cart states!
🎮 Example 2: Testing a Game Score Display
Let’s make it fun:
// 🏆 Game score component tests
import { render, screen, act } from '@testing-library/react';
import { GameScoreBoard } from './GameScoreBoard';
describe('GameScoreBoard 🎮', () => {
test('should display player scores correctly 📊', () => {
const players = [
{ name: 'Alice 🦸♀️', score: 1500, level: 5 },
{ name: 'Bob 🤖', score: 1200, level: 4 }
];
render(<GameScoreBoard players={players} />);
// 🎯 Find the scoreboard heading
expect(screen.getByRole('heading', {
name: /leaderboard/i
})).toBeInTheDocument();
// 🥇 Check first place
const aliceScore = screen.getByText(/alice.*1500.*level 5/i);
expect(aliceScore).toBeInTheDocument();
// 🥈 Check second place
const bobScore = screen.getByText(/bob.*1200.*level 4/i);
expect(bobScore).toBeInTheDocument();
// 🏆 Verify winner badge
expect(screen.getByText(/👑.*winner/i)).toBeInTheDocument();
});
test('should handle score updates in real-time ⚡', async () => {
const initialPlayers = [
{ name: 'Charlie 🎯', score: 1000, level: 3 }
];
const { rerender } = render(<GameScoreBoard players={initialPlayers} />);
// 📊 Initial score
expect(screen.getByText(/1000/)).toBeInTheDocument();
// 🚀 Update score
const updatedPlayers = [
{ name: 'Charlie 🎯', score: 1500, level: 4 }
];
rerender(<GameScoreBoard players={updatedPlayers} />);
// ✨ Verify new score appears
expect(screen.getByText(/1500/)).toBeInTheDocument();
expect(screen.getByText(/level 4/i)).toBeInTheDocument();
// 🎊 Check for level up message
expect(screen.getByText(/level up! 🎉/i)).toBeInTheDocument();
});
});
🚀 Advanced Query Techniques
🧙♂️ Custom Query Matchers
When you’re ready to level up, try custom matchers:
// 🎯 Custom matcher for better assertions
expect.extend({
toHaveAccessibleName(received: HTMLElement, expectedName: string) {
const accessibleName = received.getAttribute('aria-label') ||
received.textContent ||
received.getAttribute('title');
const pass = accessibleName?.toLowerCase().includes(expectedName.toLowerCase()) ?? false;
return {
message: () => `expected element to have accessible name containing "${expectedName}"`,
pass,
};
},
});
// 🪄 Using the custom matcher
const submitButton = screen.getByRole('button');
expect(submitButton).toHaveAccessibleName('submit form');
🏗️ Advanced Query Combinations
For complex scenarios:
// 🚀 Complex query patterns
describe('Advanced Query Patterns 🎭', () => {
test('should find nested elements efficiently', () => {
render(<UserProfile user={mockUser} />);
// 🎯 Find within a specific section
const contactSection = screen.getByRole('region', { name: /contact info/i });
const emailField = within(contactSection).getByLabelText(/email/i);
// ✨ Multiple query combination
const editButton = screen.getByRole('button', {
name: /edit/i,
description: /modify user information/i
});
// 🔍 Query with custom function
const dynamicElement = screen.getByText((content, element) => {
return element?.tagName.toLowerCase() === 'span' &&
content.includes('Score:');
});
});
});
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Over-relying on test IDs
// ❌ Wrong way - brittle and not user-focused!
const button = screen.getByTestId('submit-btn'); // 😰 Fragile!
button.click(); // 💥 Breaks when test ID changes!
// ✅ Correct way - user-centric approach!
const button = screen.getByRole('button', { name: /submit/i }); // 🛡️ Resilient!
await user.click(button); // ✅ Tests what users actually do!
🤯 Pitfall 2: Weak Assertions
// ❌ Weak - doesn't tell us much!
expect(screen.getByText('Submit')).toBeInTheDocument();
// ✅ Strong - specific and meaningful!
const submitButton = screen.getByRole('button', { name: /submit order/i });
expect(submitButton).toBeEnabled();
expect(submitButton).toHaveAttribute('type', 'submit');
expect(submitButton).toHaveAccessibleDescription(/complete your purchase/i);
😵 Pitfall 3: Not Waiting for Async Updates
// ❌ Dangerous - race conditions!
fireEvent.click(loadDataButton);
expect(screen.getByText('Data loaded')).toBeInTheDocument(); // 💥 Might fail!
// ✅ Safe - wait for changes!
await user.click(loadDataButton);
await waitFor(() => {
expect(screen.getByText('Data loaded 🎉')).toBeInTheDocument();
});
🛠️ Best Practices
- 🎯 Query Like a User: Use role-based queries first
- 📝 Meaningful Assertions: Test behavior, not implementation
- ⏱️ Wait for Async: Always use
waitFor
for dynamic content - 🎨 Descriptive Names: Use clear, descriptive query patterns
- ✨ Test User Journeys: Focus on what users actually do
🧪 Hands-On Exercise
🎯 Challenge: Build a Login Form Test Suite
Create comprehensive tests for a login form:
📋 Requirements:
- ✅ Test successful login flow
- 🔍 Verify form validation errors
- 🛡️ Check accessibility features
- ⏱️ Test loading states
- 🎨 Use proper queries and assertions!
🚀 Bonus Points:
- Add tests for “forgot password” flow
- Test keyboard navigation
- Verify error message accessibility
💡 Solution
🔍 Click to see solution
// 🎯 Complete login form test suite!
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
// 🎭 Mock our API
const mockLogin = jest.fn();
jest.mock('../api', () => ({
login: mockLogin
}));
describe('LoginForm 🔐', () => {
beforeEach(() => {
mockLogin.mockReset();
});
test('should render login form with all required fields 📝', () => {
render(<LoginForm onLogin={mockLogin} />);
// 🎯 Check form structure
expect(screen.getByRole('form', { name: /login/i })).toBeInTheDocument();
// 📝 Verify input fields
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
// 🔘 Check submit button
const submitButton = screen.getByRole('button', { name: /log in/i });
expect(submitButton).toBeInTheDocument();
expect(submitButton).toBeEnabled();
// 🔗 Verify forgot password link
expect(screen.getByRole('link', {
name: /forgot password/i
})).toBeInTheDocument();
});
test('should successfully login with valid credentials ✅', async () => {
const user = userEvent.setup();
mockLogin.mockResolvedValue({ success: true, user: { name: 'John' } });
render(<LoginForm onLogin={mockLogin} />);
// 📝 Fill out form
await user.type(screen.getByLabelText(/email/i), '[email protected]');
await user.type(screen.getByLabelText(/password/i), 'securePassword123');
// 🎯 Submit form
await user.click(screen.getByRole('button', { name: /log in/i }));
// ⏱️ Wait for success
await waitFor(() => {
expect(screen.getByText(/welcome back, john! 🎉/i)).toBeInTheDocument();
});
// ✅ Verify API was called
expect(mockLogin).toHaveBeenCalledWith({
email: '[email protected]',
password: 'securePassword123'
});
});
test('should show validation errors for invalid input ⚠️', async () => {
const user = userEvent.setup();
render(<LoginForm onLogin={mockLogin} />);
// 🎯 Submit empty form
await user.click(screen.getByRole('button', { name: /log in/i }));
// 🔍 Check error messages
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
// 🛡️ Verify accessibility
const emailError = screen.getByText(/email is required/i);
expect(emailError).toHaveAttribute('role', 'alert');
// 📝 Check invalid email format
await user.type(screen.getByLabelText(/email/i), 'invalid-email');
await user.click(screen.getByRole('button', { name: /log in/i }));
await waitFor(() => {
expect(screen.getByText(/please enter a valid email/i)).toBeInTheDocument();
});
});
test('should handle login failure gracefully 💥', async () => {
const user = userEvent.setup();
mockLogin.mockRejectedValue({ message: 'Invalid credentials' });
render(<LoginForm onLogin={mockLogin} />);
// 📝 Fill valid form
await user.type(screen.getByLabelText(/email/i), '[email protected]');
await user.type(screen.getByLabelText(/password/i), 'wrongPassword');
await user.click(screen.getByRole('button', { name: /log in/i }));
// ❌ Check error message
await waitFor(() => {
const errorMessage = screen.getByText(/invalid credentials/i);
expect(errorMessage).toBeInTheDocument();
expect(errorMessage).toHaveAttribute('role', 'alert');
});
// 🔘 Verify button is enabled again
expect(screen.getByRole('button', { name: /log in/i })).toBeEnabled();
});
test('should show loading state during submission ⏳', async () => {
const user = userEvent.setup();
// 🐌 Slow response simulation
mockLogin.mockImplementation(() =>
new Promise(resolve => setTimeout(() => resolve({ success: true }), 100))
);
render(<LoginForm onLogin={mockLogin} />);
// 📝 Fill and submit
await user.type(screen.getByLabelText(/email/i), '[email protected]');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /log in/i }));
// ⏳ Check loading state
expect(screen.getByRole('button', { name: /logging in.../i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /logging in.../i })).toBeDisabled();
// 🎯 Verify loading indicator
expect(screen.getByText(/please wait.../i)).toBeInTheDocument();
// ✅ Wait for completion
await waitFor(() => {
expect(screen.getByRole('button', { name: /log in/i })).toBeEnabled();
});
});
});
🎓 Key Takeaways
You’ve learned so much! Here’s what you can now do:
- ✅ Choose the right queries that focus on user experience 💪
- ✅ Write meaningful assertions that test behavior, not implementation 🛡️
- ✅ Handle async operations properly with waitFor 🎯
- ✅ Debug test failures like a pro 🐛
- ✅ Build comprehensive test suites that give you confidence! 🚀
Remember: Good tests are like good friends - they tell you the truth and help you improve! 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve mastered testing library queries and assertions!
Here’s what to do next:
- 💻 Practice with the exercises above
- 🏗️ Add comprehensive tests to your current project
- 📚 Move on to our next tutorial: Advanced Testing Patterns
- 🌟 Share your testing wins with the community!
Remember: Every testing expert was once a beginner who wrote their first expect()
. Keep testing, keep learning, and most importantly, have fun building reliable software! 🚀
Happy testing! 🎉🚀✨