+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 219 of 355

📘 Testing Library Best Practices: Queries and Assertions

Master testing library best practices: queries and assertions in TypeScript with practical examples, best practices, and real-world applications 🚀

🚀Intermediate
30 min read

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:

  1. Accessibility First 🔒: Find elements the way users do
  2. Better Test Reliability 💻: Tests that don’t break from UI changes
  3. Clear Intent 📖: Readers understand what you’re testing
  4. 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

  1. 🎯 Query Like a User: Use role-based queries first
  2. 📝 Meaningful Assertions: Test behavior, not implementation
  3. ⏱️ Wait for Async: Always use waitFor for dynamic content
  4. 🎨 Descriptive Names: Use clear, descriptive query patterns
  5. ✨ 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:

  1. 💻 Practice with the exercises above
  2. 🏗️ Add comprehensive tests to your current project
  3. 📚 Move on to our next tutorial: Advanced Testing Patterns
  4. 🌟 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! 🎉🚀✨