How to Unit Test React Context API Authentication with Vitest | A Comprehensive Tutorial
Introduction
When working with React applications, the Context API is a powerful tool for managing global state. One common use case is authentication, where an AuthProvider
component manages user login, logout, and user details. However, testing components that depend on useContext
can be tricky.
In this article, we’ll explore unit testing the Context API in React with TypeScript using Vitest. We’ll focus on testing a profile component that fetches user details from an AuthProvider and conditionally renders content based on the authentication state.
By the end of this tutorial, you’ll learn how to:
✅ Mock context values in Vitest ✅ Test components that rely on the Context API ✅ Simulate different authentication states
Understanding AuthProvider
Before diving into tests, let’s understand the role of AuthProvider
. It typically handles user authentication by:
- Storing user details (name, email, ID, etc.)
- Providing login and logout functions
- Returning the user object to child components
- Checking if a user is authenticated
Here’s a basic example of an AuthProvider
:
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface User {
firstName: string;
lastName: string;
userName: string;
email: string;
}
interface AuthContextType {
user: User | null;
login: (userData: User) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const login = (userData: User) => setUser(userData);
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
The MyProfile
Component
Now, let’s look at the MyProfile
component. It fetches user details from useAuth and conditionally renders either a loading state or user details.
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { SideNavLink, Loading } from '@carbon/react';
import * as Icons from '@carbon/icons-react';
import AvatarImage from '../AvatarImage';
import { useThemePreference } from '../../utils/ThemePreference';
import { useAuth } from '../../contexts/AuthProvider';
const MyProfile = () => {
const { user, logout } = useAuth();
const { theme, setTheme } = useThemePreference();
const navigate = useNavigate();
const [goTo, setGoTo] = useState(false);
const [goToURL, setGoToURL] = useState('');
useEffect(() => {
if (goTo) {
setGoTo(false);
navigate(goToURL);
}
}, [goTo]);
const changeTheme = () => {
const newTheme = theme === 'g10' ? 'g100' : 'g10';
setTheme(newTheme);
localStorage.setItem('mode', newTheme === 'g10' ? 'light' : 'dark');
};
return user ? (
<>
<div className="user-info-section">
<AvatarImage userName={`${user.firstName} ${user.lastName}`} size="large" />
<div className="user-data">
<p className="user-name">{`${user.firstName} ${user.lastName}`}</p>
<p>{`Username: ${user.userName}`}</p>
<p>{`Email: ${user.email}`}</p>
</div>
</div>
<hr className="divisory" />
<nav className="account-nav">
<SideNavLink renderIcon={theme === 'g10' ? Icons.Asleep : Icons.Light} onClick={changeTheme}>
Change theme
</SideNavLink>
<SideNavLink renderIcon={Icons.UserFollow} onClick={logout}>
Log out
</SideNavLink>
</nav>
</>
) : (
<Loading description="Loading user details" withOverlay />
);
};
export default MyProfile;
Unit Testing MyProfile
with Vitest
To test this component, we need to mock useAuth so we can control the returned user data.
import React from 'react';
import MyProfile from '../../components/MyProfile';
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { AuthProvider, useAuth } from '../../contexts/AuthProvider';
import { ThemePreference } from '../../utils/ThemePreference';
import { BrowserRouter } from 'react-router-dom';
const renderComponent = () => {
render(
<ThemePreference>
<AuthProvider>
<BrowserRouter>
<MyProfile />
</BrowserRouter>
</AuthProvider>
</ThemePreference>
);
};
// Mock useAuth
vi.mock('../../contexts/AuthProvider', () => ({
AuthProvider: ({ children }) => <>{children}</>,
useAuth: vi.fn(),
}));
describe('MyProfile', () => {
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: query === '(prefers-color-scheme: dark)',
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
});
it('should show "Loading user details" when user is not set', () => {
(useAuth as ReturnType<typeof vi.fn>).mockReturnValue({ user: null });
renderComponent();
expect(screen.getByText('Loading user details')).toBeInTheDocument();
});
it('should display user details when user is available', () => {
(useAuth as ReturnType<typeof vi.fn>).mockReturnValue({
user: {
firstName: 'John',
lastName: 'Doe',
userName: 'jdoe',
email: 'john.doe@example.com',
},
logout: vi.fn(),
});
renderComponent();
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
Conclusion
By mocking useAuth
, we can test the MyProfile
component under different authentication states. Vitest makes it simple to mock hooks and verify component behavior efficiently.
Now you can confidently write tests for your Context API-powered components in React with TypeScript and Vitest!
Happy Coding!