Before getting started ๐
Have you encountered these frustrations while writing tests?
โI need ten mocks just to test this one component.โ
โI changed only the screen color and every test broke.โ
โIt keeps complaining that a Context Provider is missing.โ
These problems arise when code is
hard to test, or untestable
.
One of TDD's greatest strengths is that it
rejects hard-to-test designs
.
Today we will improve structure to make both testing and maintenance easier.
A bad example: The component that does everything ๐
A common anti-pattern is one component handling
fetching, transforming, and rendering data
.
A hard-to-test component ๐
src/components/BadUserProfile.tsx import { useEffect, useState } from 'react' ;
interface User {
firstName : string ;
lastName : string ;
email : string ;
}
export default function BadUserProfile () {
const [ user , setUser ] = useState < User | null >( null );
// [๋ฌธ์ 1] ์ปดํฌ๋ํธ๊ฐ '๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ'๋ฅผ ์ง์ ์ํํจ (์์กด์ฑ ๋์)
useEffect (() => {
fetch ( 'https://jsonplaceholder.typicode.com/users/1' )
. then (( res ) => res. json ())
. then (( data ) => {
// [๋ฌธ์ 2] ๋ฐ์ดํฐ๋ฅผ ๋ณํํ๋ '๋น์ฆ๋์ค ๋ก์ง'์ด ์์ฌ ์์
const formattedData = {
... data,
// ์ด๋ฆ์ ๋๋ฌธ์๋ก ๋ณํํด์ผ ํ๋ค๋ ๊ท์น
firstName: data.name. split ( ' ' )[ 0 ]. toUpperCase (),
lastName: data.name. split ( ' ' )[ 1 ]. toUpperCase (),
};
setUser (formattedData);
});
}, []);
if ( ! user) return < div >๋ก๋ฉ ์ค...</ div >;
return (
< div >
{ /* [๋ฌธ์ 3] ํ๋ฉด ๊ทธ๋ฆฌ๊ธฐ ๋ก์ง (UI) */ }
< h1 > { user.firstName } { user.lastName } </ h1 >
< p > { user.email } </ p >
</ div >
);
}
Testing it requires mocking
fetch and waiting for asynchronous behavior with
findBy.
That is true even if we only want to verify that the name appears prominently in an h1.
Improvement strategy 1: Separate UI from logic ๐
The classic
container/presentational pattern
addresses this.
In simple terms, separate the smart logic from the simple UI.
Today logic is often extracted into hooks, but the principle of keeping UI pure remains the same.
1. Presentational component: UI ๐
This component does not know where the data came from.
It receives
props and renders them.
Testing difficulty: Very low.
src/components/UserProfileView.tsx // ๋ฐ์ดํฐ ํ์
์ ์
export interface UserProfileProps {
firstName : string ;
lastName : string ;
email : string ;
}
export default function UserProfileView ({ firstName, lastName, email } : UserProfileProps) {
return (
< div >
< h1 > { firstName } { lastName } </ h1 >
< p > { email } </ p >
</ div >
);
}
2. Container component: Logic ๐
Fetch and transform the data, then pass it to the UI component.
src/components/UserProfileContainer.tsx import { useEffect, useState } from 'react' ;
import UserProfileView from './UserProfileView' ;
export default function UserProfileContainer () {
const [ user , setUser ] = useState ( null );
useEffect (() => {
// ... fetch ๋ฐ ๋ฐ์ดํฐ ๊ฐ๊ณต ๋ก์ง (ํน์ Custom Hook ์ฌ์ฉ)
// ์ฌ๊ธฐ์๋ ์๋ตํฉ๋๋ค.
}, []);
if ( ! user) return < div >๋ก๋ฉ ์ค...</ div >;
// ๊ฐ๊ณต๋ ๋ฐ์ดํฐ๋ฅผ UI ์ปดํฌ๋ํธ์ ์ฃผ์
return < UserProfileView {... user } />;
}
Writing and running the test ๐
Now test the separated
UserProfileView.
No complicated
fetch or
MSW setup is needed.
src/components/UserProfileView.test.tsx import { render, screen } from '@testing-library/react' ;
import { describe, test, expect } from 'vitest' ;
import UserProfileView from './UserProfileView' ;
describe ( 'UserProfileView UI ํ
์คํธ' , () => {
test ( 'props๋ก ๋ฐ์ ์ด๋ฆ๊ณผ ์ด๋ฉ์ผ์ ์ ํํ ํ์ํ๋ค' , () => {
// 1. ๊ฐ์ง ๋ฐ์ดํฐ(Mock Data) ์ค๋น
// API ํธ์ถ ์์ด ์ฐ๋ฆฌ๊ฐ ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ์ง์ ์ฃผ์
ํฉ๋๋ค.
const mockUser = {
firstName: 'KIM' ,
lastName: 'CHULSU' ,
email: 'test@example.com'
};
render (< UserProfileView {... mockUser } />);
// 2. ๋ก๊ทธ ํ์ธ
console. log ( '[UI Test] ์์ ์ปดํฌ๋ํธ ๋ ๋๋ง ํ์ธ' );
screen. debug ();
// 3. ๊ฒ์ฆ
// h1 ํ๊ทธ ์์ ์ด๋ฆ์ด ๋ค์ด์๋์ง ํ์ธ
expect (screen. getByRole ( 'heading' )). toHaveTextContent ( 'KIM CHULSU' );
// ์ด๋ฉ์ผ์ด ๋ฌธ์์ ์กด์ฌํ๋์ง ํ์ธ
expect (screen. getByText ( 'test@example.com' )). toBeInTheDocument ();
});
});
Running and analyzing the tests ๐
Run the test in the terminal.
Terminal npm test -- src/components/UserProfileView.test.tsx
Checking the success log ๐
Checking the success log
[UI Test] ์์ ์ปดํฌ๋ํธ ๋ ๋๋ง ํ์ธ
<body>
<div>
<div>
<h1>
KIM CHULSU
</h1>
<p>
test@example.com
</p>
</div>
</div>
</body>
PASS src/components/UserProfileView.test.tsx
Analyzing the results ๐
Purity:
The logs show our supplied KIM CHULSU data rendered directly, without fetch loading states or error messages.
Removed dependencies:
The UI test succeeds regardless of whether the API server is running.
Speed:
Without asynchronous waits such as await findBy..., the test finishes immediately.
Terminal git clone https://github.com/PROMLEE/my-tdd-app.git
cd my-tdd-app
git checkout part11
We improved a structure that obstructed testing and checked the result.
Separation of concerns
Separating data logic in the container from the view made UI tests simple and fast.
Result analysis
Logs confirmed that data rendered immediately, without unnecessary loading work.
When tests are difficult to write, treat that as a signal to
improve the structure
rather than forcing tests onto it.
Better structure naturally makes testing easier.
Part 12,
Organizing Tests to Support Refactoring
, explains how to keep tests stable when design or implementation changes.