Skip to content

Preact Testing — Unit and Integration Tests for Preact Components

DodaTech Updated 2026-06-28 5 min read

Learn how to test Preact components using preact-testing-library, vitest, and best practices for unit and Integration Testing in the 3kB framework.

In this lesson, you'll set up a testing environment, render components in tests, simulate user interactions, and assert on component output.

What You'll Learn

How to set up vitest with Preact, use preact-testing-library to render components, test user interactions, mock dependencies, and test hooks.

Why It Matters

Automated tests catch regressions before they reach production. Testing Preact components ensures your UI behaves correctly as the application grows.

Real-World Use

Durga Antivirus Pro's scan result component has 40+ tests covering different threat levels, scan states, and edge cases. Every PR must pass these tests before deployment.

flowchart LR
    A[Write Test] --> B[Render Component]
    B --> C[Simulate Interaction]
    C --> D[Assert Output]
    D --> E{Pass?}
    E -->|Yes| F[Confidence]
    E -->|No| G[Fix Bug]
    G --> A
    style A fill:#673ab8,color:#fff
    style F fill:#4a148c,color:#fff

Setting Up Testing

npm install -D vitest @preact/preset-vite jsdom
npm install -D @testing-library/preact

Configure vitest in vite.config.js:

import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [preact()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './test-setup.js'
  }
});

Create test-setup.js:

import { cleanup } from '@testing-library/preact';
import { afterEach } from 'vitest';

afterEach(() => cleanup());

Rendering and Asserting

Test basic component rendering:

import { render, screen } from '@testing-library/preact';
import { describe, it, expect } from 'vitest';

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

describe('Greeting', () => {
  it('renders the name', () => {
    render(<Greeting name="Alice" />);
    expect(screen.getByText('Hello, Alice!')).toBeTruthy();
  });

  it('updates when name changes', () => {
    const { rerender } = render(<Greeting name="Alice" />);
    expect(screen.getByText('Hello, Alice!')).toBeTruthy();

    rerender(<Greeting name="Bob" />);
    expect(screen.getByText('Hello, Bob!')).toBeTruthy();
  });
});

Output: The test passes if the component renders "Hello, Alice!" and updates to "Hello, Bob!" on rerender.

Testing User Interactions

Simulate clicks, input, and form submission:

import { render, screen, fireEvent } from '@testing-library/preact';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

describe('Counter', () => {
  it('starts at 0', () => {
    render(<Counter />);
    expect(screen.getByText('Count: 0')).toBeTruthy();
  });

  it('increments on click', () => {
    render(<Counter />);
    fireEvent.click(screen.getByText('Increment'));
    expect(screen.getByText('Count: 1')).toBeTruthy();
  });

  it('increments multiple times', () => {
    render(<Counter />);
    const btn = screen.getByText('Increment');
    fireEvent.click(btn);
    fireEvent.click(btn);
    fireEvent.click(btn);
    expect(screen.getByText('Count: 3')).toBeTruthy();
  });
});

Output: The tests verify initial render, single increment, and multiple increments using simulated clicks.

Testing Forms

function LoginForm({ onLogin }) {
  const [email, setEmail] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onLogin(email);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email}
        onChange={e => setEmail(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  );
}

describe('LoginForm', () => {
  it('submits the email', () => {
    const onLogin = vi.fn();
    render(<LoginForm onLogin={onLogin} />);

    const input = screen.getByRole('textbox');
    fireEvent.change(input, { target: { value: 'alice@test.com' } });
    fireEvent.click(screen.getByText('Login'));

    expect(onLogin).toHaveBeenCalledWith('alice@test.com');
  });
});

Output: The test types an email, clicks Login, and verifies the onLogin callback was called with the correct email.

Testing Hooks and Async Behavior

Test components that use effects and async operations:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  if (!user) return <p>Loading...</p>;
  return <h2>{user.name}</h2>;
}

describe('UserProfile', () => {
  it('shows loading state initially', () => {
    render(<UserProfile userId={42} />);
    expect(screen.getByText('Loading...')).toBeTruthy();
  });

  it('displays user name after fetch', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ name: 'Alice' })
    });

    render(<UserProfile userId={42} />);
    const name = await screen.findByText('Alice');
    expect(name).toBeTruthy();
  });
});

Output: The first test checks the loading state. The second test mocks fetch and awaits the async user name to appear.

Testing Context

const ThemeContext = createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button class={theme}>Click me</button>;
}

describe('ThemedButton', () => {
  it('uses theme from context', () => {
    render(
      <ThemeContext.Provider value="dark">
        <ThemedButton />
      </ThemeContext.Provider>
    );
    const btn = screen.getByText('Click me');
    expect(btn.classList.contains('dark')).toBe(true);
  });
});

Output: The test wraps the component in a context provider and verifies the button has the correct class.

Common Mistakes

  1. Not using cleanup after each test: Without cleanup, components from previous tests remain in the DOM, causing false positives/negatives.
  2. Testing implementation details: Test what the user sees (rendered output), not internal state or method calls.
  3. Forgetting to mock fetch or API calls: Real API calls in tests fail in CI. Always mock external dependencies.
  4. Using screen.debug() to debug tests: This prints the current DOM. Remove it before committing to avoid noisy test output.
  5. Not testing edge cases: Test empty state, error state, loading state, and boundary conditions, not just the happy path.

Practice Questions

  1. What library provides React-testing-library-like API for Preact? Answer: @testing-library/preact. It provides render, screen, fireEvent, and waitFor.

  2. How do you simulate a click event in a test? Answer: Use fireEvent.click(element) from @testing-library/preact, or use userEvent for more realistic interactions.

  3. What is the purpose of vi.fn()? Answer: It creates a mock function that records calls, arguments, and return values for assertions.

  4. How do you test a component that uses async effects? Answer: Mock the async dependency (e.g., fetch), render the component, and use findByText or waitFor to await the async result.

Challenge

Build a todo list component and write tests covering: adding a todo, completing a todo, filtering by status, deleting a todo, and showing the empty state. Mock localStorage for persistence tests.

Mini Project

Create a form component with validation, submission, and error handling. Write at least 10 tests covering: valid input, invalid input, empty fields, async submission, submission error, and success state.

FAQ

Does @testing-library/preact support all query methods?

: Yes. It supports getByText, getByRole, getByTestId, getByLabelText, getByPlaceholderText, and their find and query variants.

Can I test hooks without rendering a component?

: Yes. Use renderHook from @testing-library/preact to test custom hooks in isolation.

How do I test components that use signals?

: Same as regular components. Signals work inside @testing-library/preact without special configuration.

Does vitest support coverage reporting?

: Yes. Add --coverage flag with @vitest/coverage-v8 or @vitest/coverage-istanbul for coverage reports.

What's Next

Build a complete application in the Preact Mini Project lesson, combining all concepts into a real-world Preact application.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro