Preact Testing — Unit and Integration Tests for Preact Components
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
- Not using
cleanupafter each test: Without cleanup, components from previous tests remain in the DOM, causing false positives/negatives. - Testing implementation details: Test what the user sees (rendered output), not internal state or method calls.
- Forgetting to mock
fetchor API calls: Real API calls in tests fail in CI. Always mock external dependencies. - Using
screen.debug()to debug tests: This prints the current DOM. Remove it before committing to avoid noisy test output. - Not testing edge cases: Test empty state, error state, loading state, and boundary conditions, not just the happy path.
Practice Questions
What library provides React-testing-library-like API for Preact? Answer:
@testing-library/preact. It providesrender,screen,fireEvent, andwaitFor.How do you simulate a click event in a test? Answer: Use
fireEvent.click(element)from@testing-library/preact, or useuserEventfor more realistic interactions.What is the purpose of
vi.fn()? Answer: It creates a mock function that records calls, arguments, and return values for assertions.How do you test a component that uses async effects? Answer: Mock the async dependency (e.g.,
fetch), render the component, and usefindByTextorwaitForto 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
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