Success State Testing: React Components and MSW
Testing the success state focuses on verifying that your React component correctly renders data returned by the API and that user interactions with that data work as expected. Success state tests form the foundation of integration testing: they verify the happy path where your API returns valid data and your component displays it without errors, crashes, or missing UI elements.
When testing a product recommendation component at Clarity Labs, I realized that while we tested error states thoroughly, we barely tested what happened when the API worked perfectly. That gap meant subtle bugs—off-by-one errors in pagination, missing data fields, incorrect prop drilling—made it to production. Comprehensive success-state testing became essential for catching these issues early.
Testing Data Rendering and Display
The most basic success test renders a component, waits for data to load, and verifies the rendered content matches the mocked response.
import { render, screen, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { server } from "./mocks/server";
function ProductList() {
const [products, setProducts] = React.useState([]);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch("/api/products")
.then((res) => res.json())
.then((data) => {
setProducts(data.products);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id} data-testid={`product-${product.id}`}>
<strong>{product.name}</strong>
<span className="price">${product.price}</span>
<p>{product.description}</p>
</li>
))}
</ul>
</div>
);
}
test("renders products from API response", async () => {
server.use(
http.get("/api/products", () => {
return HttpResponse.json({
products: [
{
id: "1",
name: "Laptop",
price: 999.99,
description: "High-performance laptop",
},
{
id: "2",
name: "Mouse",
price: 29.99,
description: "Wireless mouse",
},
],
});
})
);
render(<ProductList />);
// Wait for product list to render
const laptop = await screen.findByText("Laptop");
expect(laptop).toBeInTheDocument();
// Verify all product data is displayed
expect(screen.getByText("High-performance laptop")).toBeInTheDocument();
expect(screen.getByText("$999.99")).toBeInTheDocument();
expect(screen.getByText("Wireless mouse")).toBeInTheDocument();
// Verify product count
const products = screen.getAllByTestId(/^product-/);
expect(products).toHaveLength(2);
});
This test verifies that the API response data is correctly rendered in the DOM. It checks product names, prices, descriptions, and the correct number of items, catching bugs where fields are missing or the DOM structure is wrong.
Testing Data Transformations
Often your component transforms or formats API data before display (e.g., converting timestamps to readable dates, truncating long text, applying currency formatting). Your tests should verify these transformations work correctly.
function UserCard({ userId }) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
// Transform joinedAt timestamp to readable date
setUser({
...data,
joinedDate: new Date(data.joinedAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}),
});
});
}, [userId]);
if (!user) return <div>Loading...</div>;
return (
<div className="card">
<h2>{user.name}</h2>
<p>Joined: {user.joinedDate}</p>
<p>Email: {user.email}</p>
</div>
);
}
test("formats timestamp to readable date", async () => {
server.use(
http.get("/api/users/:id", () => {
return HttpResponse.json({
id: "user-1",
name: "Alice Chen",
email: "[email protected]",
joinedAt: "2024-06-15T10:30:00Z",
});
})
);
render(<UserCard userId="user-1" />);
// Verify the date is displayed in readable format
const joinedDate = await screen.findByText(/joined: june 15, 2024/i);
expect(joinedDate).toBeInTheDocument();
});
By testing data transformations, you catch bugs where timestamps show as milliseconds instead of formatted dates, or prices are formatted incorrectly.
Testing Conditional Rendering
Components often render different content based on data. Your tests should verify that the correct content appears under the right conditions.
function OrderStatus({ orderId }) {
const [order, setOrder] = React.useState(null);
React.useEffect(() => {
fetch(`/api/orders/${orderId}`)
.then((res) => res.json())
.then(setOrder);
}, [orderId]);
if (!order) return <div>Loading...</div>;
return (
<div>
<h2>Order #{order.id}</h2>
<p>Status: {order.status}</p>
{order.status === "shipped" && (
<div>
<p>Tracking number: {order.trackingNumber}</p>
<a href={order.trackingUrl}>Track shipment</a>
</div>
)}
{order.status === "delivered" && (
<div>
<p>Delivered on: {order.deliveredDate}</p>
<button>Leave review</button>
</div>
)}
{order.status === "cancelled" && (
<p className="error">This order has been cancelled.</p>
)}
</div>
);
}
test("displays shipped status with tracking info", async () => {
server.use(
http.get("/api/orders/:id", () => {
return HttpResponse.json({
id: "ord-123",
status: "shipped",
trackingNumber: "1Z999AA10123456784",
trackingUrl: "https://tracking.ups.com/1Z999AA10123456784",
});
})
);
render(<OrderStatus orderId="ord-123" />);
// Tracking info should appear
const tracking = await screen.findByText(/1Z999AA10123456784/);
expect(tracking).toBeInTheDocument();
expect(screen.getByRole("link", { name: /track shipment/i })).toBeInTheDocument();
// Review button should not appear
expect(screen.queryByRole("button", { name: /review/i })).not.toBeInTheDocument();
});
test("displays delivered status with review button", async () => {
server.use(
http.get("/api/orders/:id", () => {
return HttpResponse.json({
id: "ord-124",
status: "delivered",
deliveredDate: "2026-06-01",
});
})
);
render(<OrderStatus orderId="ord-124" />);
// Review button should appear
const review = await screen.findByRole("button", { name: /review/i });
expect(review).toBeInTheDocument();
// Tracking info should not appear
expect(screen.queryByText(/tracking number/i)).not.toBeInTheDocument();
});
By testing different API response states and verifying the correct conditional branches render, you catch bugs where the wrong content appears or important UI elements are missing.
Testing Pagination
Paginated lists require handlers that return different data based on query parameters. Your tests should verify that pagination controls update the correct page and that the data changes.
function ArticleList() {
const [articles, setArticles] = React.useState([]);
const [page, setPage] = React.useState(1);
React.useEffect(() => {
fetch(`/api/articles?page=${page}`)
.then((res) => res.json())
.then(setArticles);
}, [page]);
return (
<div>
<ul>
{articles.map((a) => (
<li key={a.id}>{a.title}</li>
))}
</ul>
<button onClick={() => setPage(page - 1)} disabled={page === 1}>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(page + 1)}>Next</button>
</div>
);
}
test("paginates through articles", async () => {
server.use(
http.get("/api/articles", ({ request }) => {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1", 10);
const pages = {
1: [
{ id: "1", title: "Article One" },
{ id: "2", title: "Article Two" },
],
2: [
{ id: "3", title: "Article Three" },
{ id: "4", title: "Article Four" },
],
};
return HttpResponse.json(pages[page] || []);
})
);
render(<ArticleList />);
// First page loads
const articleOne = await screen.findByText("Article One");
expect(articleOne).toBeInTheDocument();
// Click next page
const nextButton = screen.getByRole("button", { name: /next/i });
await userEvent.click(nextButton);
// Second page content appears
const articleThree = await screen.findByText("Article Three");
expect(articleThree).toBeInTheDocument();
// First page content is gone
expect(screen.queryByText("Article One")).not.toBeInTheDocument();
});
Pagination testing verifies that your component correctly passes page parameters to the API and updates the display when pages change.
Testing Data Filtering
Filters (like category or search) change the API request. Your tests should verify that applying a filter updates the data displayed.
function CategoryFilter() {
const [category, setCategory] = React.useState("all");
const [items, setItems] = React.useState([]);
React.useEffect(() => {
const url = category === "all" ? "/api/items" : `/api/items?category=${category}`;
fetch(url)
.then((res) => res.json())
.then(setItems);
}, [category]);
return (
<div>
<select onChange={(e) => setCategory(e.target.value)} value={category}>
<option value="all">All Items</option>
<option value="featured">Featured</option>
<option value="new">New Arrivals</option>
</select>
<ul>
{items.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
test("filters items by category", async () => {
server.use(
http.get("/api/items", ({ request }) => {
const url = new URL(request.url);
const category = url.searchParams.get("category");
if (category === "featured") {
return HttpResponse.json([
{ id: "1", title: "Featured Item A" },
]);
}
return HttpResponse.json([
{ id: "1", title: "All Item A" },
{ id: "2", title: "All Item B" },
]);
})
);
render(<CategoryFilter />);
// All items appear initially
expect(await screen.findByText("All Item A")).toBeInTheDocument();
expect(screen.getByText("All Item B")).toBeInTheDocument();
// Select featured category
const select = screen.getByRole("combobox");
await userEvent.selectOptions(select, "featured");
// Only featured item appears
const featured = await screen.findByText("Featured Item A");
expect(featured).toBeInTheDocument();
expect(screen.queryByText("All Item B")).not.toBeInTheDocument();
});
Filter testing verifies that your component correctly sends filter parameters to the API and displays the filtered results.
Key Takeaways
- Success state tests verify that components correctly render API response data without errors.
- Data transformation tests catch bugs in formatting (dates, currency, text truncation).
- Conditional rendering tests verify that the correct UI appears based on API data.
- Pagination tests verify that page parameters are sent correctly and data updates when pages change.
- Filter tests verify that filter parameters change API requests and the UI updates accordingly.
Frequently Asked Questions
Should I test every field in the API response?
Test fields that are displayed or affect logic. Do not test internal fields that are not rendered. Focus on user-visible content and interaction outcomes.
How do I test large datasets without slow tests?
Return a smaller dataset in your MSW handler (e.g., 10 items instead of 1000). Test pagination/virtualization separately to verify it can handle large datasets.
What if my API returns different field names depending on context?
This is a code smell—normalize your data in your API response or use a data transformation layer in your component. Your tests should verify the final rendered state, not the intermediate transformations.
Can I test success and error in the same test?
It is better to separate them. A test should verify one scenario. Mixing success and error makes it hard to understand which path is broken if the test fails.