Feb 19, 2026
Next.js
Jest
Refactoring Before Testing

Overview
As discussed in Designing Tests
- Validation (interpreting untrusted input)
- Calculation (numeric logic, boundary values, state handling)
are the kinds of logic that make me want to write tests.
However, when I continued developing without considering testing, I sometimes ended up writing code that was difficult to test.
Code That Is Hard to Test
The following example is part of a UI component for the notes list page:
...
export default async function NotesList({ searchParams, techStacks }: NotesListProps) {
const validCategoryIds = techStacks.map((stack) => stack.id);
const { page, category } = searchParams;
const currentPage = page ? Number(page) : 1;
if (!Number.isInteger(currentPage) || currentPage < 1) {
return <h2>Invalid Page</h2>;
}
const currentCategory = category && validCategoryIds.includes(category) ? category : undefined;
const filters = currentCategory ? `techStack[contains]${currentCategory}` : undefined;
const limit = 5;
const offset = (currentPage - 1) * limit;
const notes = await getNotesList({ filters, limit, offset });
const totalPages = Math.ceil(notes.totalCount / limit);
const techStack = currentCategory ? TECH_STACKS.find((s) => s.id === currentCategory) : undefined;
const Icon = techStack?.Icon;
const listParams = new URLSearchParams();
if (page) listParams.set('page', page);
if (currentCategory) listParams.set('category', currentCategory);
const listUrl = listParams.toString().length > 0 ? `/notes?${listParams.toString()}` : '/notes';
return (
<section>
<div>
<div>
....
This code includes:
- Logic to validate incoming values
- Numeric calculations for pagination
According to the criteria described in Designing Tests, this is code that makes me want to write tests.
However, in this implementation, the logic is written directly inside the UI component.
If I try to write tests in this state, I would need:
- Mocking of CMS communication functions
- Mocking of next/link
- Mocking of next/image
- Other UI-related mocks
Even though I only want to test the logic, I end up writing something closer to a UI test.
This is what I mean by code that is hard to test.
What I Became Aware Of While Writing Code
When I wrote code without thinking about testing, I tended to mix UI and logic together.
However, if I consider that I may want to test the logic later, this structure is not ideal.
While writing code, when I notice:
- Untrusted input
- Branching or calculations
I should separate that logic into functions.
Rules for Separation
- Logic functions should only process state and return values.
- Data fetching (CMS or API communication) should remain in the UI layer.
- Logic functions should not return JSX.
- Logic functions should not depend on UI-specific modules such as next/link or next/image.
Refactoring Process
Reviewing the Current Code
Before refactoring, review the existing code and identify which parts should be separated.
Based on the separation rules above, consider:
- What should be moved into a logic function
- What should remain in the UI
- How to split responsibilities clearly
Clarifying the Refactoring Structure
In the previous implementation, some logic directly returned JSX:
if (!Number.isInteger(currentPage) || currentPage < 1) {
return <h2>Invalid Page</h2>;
}
Although this is validation logic, it also includes UI responsibility because it returns JSX.
Therefore:
- The logic function should return isInvalidPage.
- The UI component should decide how to render <h2>Invalid Page</h2>.
Data fetching was also mixed into this component:
const notes = await getNotesList({ filters, limit, offset });
Since this includes side effects, it should not be part of the logic function.
However, the fetched data is used later for pagination calculations. For that reason, instead of creating a single logic function, I separated the code into:
- Logic independent of fetched data
- Logic dependent on fetched data
The structure became:
Function 1: Query-related logic (independent of fetched data)
↓
Data fetching (UI layer)
↓
Function 2: Pagination logic (dependent on fetched data)
Refactoring Structure
After refactoring, the component looks like this:
...
export default async function NotesList({ searchParams, techStacks }: NotesListProps) {
const base = buildNotesQueryState({
page: searchParams.page,
category: searchParams.category,
techStacks,
});
if (base.isInvalidPage) {
return <h2>Invalid Page</h2>;
}
const notes = await getNotesList({
filters: base.filters,
limit: base.limit,
offset: base.offset,
});
const pagination = buildPaginationState({
limit: base.limit,
currentPage: base.currentPage,
totalCount: notes.totalCount,
currentCategory: base.currentCategory,
});
...
}
With this separation, the test targets become:
- Validation logic in buildNotesQueryState
- Pagination logic in buildPaginationState
As a result:
- No async logic in the test target
- No JSX in the test target
- No need to mock next/link
- No need to mock CMS requests
I can now test only the logic using Jest.
