Adrián Ferrera González
@afergon
adrian-afergon
export const LongComponent: React.FC<{}> = () => {
const [categories] = React.useState<string[]>([]);
const [videos, setVideos] = React.useState<Video[]>([]);
const [clasifiedVideos, setClasifiedVideos] = React.useState<ClasifiedVideos>({});
const [selectedVideo, setSelectedVideo] = React.useState<Video>({});
const [commentary, setCommentary] = React.useState<string>('');
React.useEffect( () => {
setVideos([
// ...videos retrieved from api
]);
setClasifiedVideos(
categories.reduce((total, category) => {
const foundedVideosOnCategory = videos.filter((video) =>
category === video.category
);
return {...total, [category]: foundedVideosOnCategory}
}, {})
);
}, []);
const saveCommentary = () => {
// ... do something
}
return (
// see right
)};
<div className="LongComponent">
<section>
<ul>
{categories.map((category) => (
<li>
{ category }
<ul>
{ clasifiedVideos[category].map( (video) => (
<li
onClick={() => setSelectedVideo(video)}
>{ video.title }</li>)
)}
</ul>
</li>
))}
</ul>
</section>
<form>
<video></video>
<ul>
{selectedVideo.comentaries.map((commentary) => (
<li>{commentary}</li>
))}
</ul>
<textarea onChange={(event) => {
setCommentary(event.target.value)
}}>{commentary}</textarea>
<button onClick={saveCommentary}></button>
</form>
</div>
But we want:
export const LayerOverlap: React.FC<{}> = () => {
const END_POINT = 'http://localhost:8080/videos';
//... some useStates
const getVideos = async () => {
const response = await fetch(END_POINT);
setVideos(await response.json());
};
React.useEffect(() => {
getVideos().then();
}, []);
React.useEffect( () => {
setClassifiedVideos(
categories.reduce((total, category) => {
const foundedVideosOnCategory = videos.filter((video) => category === video.category);
return {...total, [category]: foundedVideosOnCategory};
}, {}),
);
}, [categories, videos]);
const saveCommentary = async () => {
await fetch(`${END_POINT}/${selectedVideo.id}/comments`, {
method: 'post',
headers: {'Content-Type': 'application/json'},
body: newCommentary,
});
};
return (/* Irrelevant content */);
};
Overlaped Component:
Single Responsability:
Write a test,
watch it fail.
Write just enough code to pass the test
Improve the code wothout changing its behavior
Classic way:
TDD way:
Fast Feedback
High code coverage
Quality assurance
Other way to document
This isn't about TDD,
it's about the way you testÂ
Expects are use cases
Clone of real environment
Too expensive
Real coverage
Check concrete behaviors
Specific configurations are not required
Faster and cheaper
Local component coverage
Check behaviors with broader scope
Requires mocks to set limits
Perfect balance
Important coverage but not always real
IMPORTANT!!!
You need an architecture!!
Emerging architecture
Componentisation
Predefined architecture
Fuzzy components
London
Chicago
describe('Big block', () => {
it('The functionality', () => {
});
});
it('should have an expect inside', () => {
expect(true).toBeTruthy(); // OK
expect(false).toBeTruthy(); // KO
expect(1).toBe(1); // OK
expect(1).toBe(2); // KO
expect('Hello').toBe('Hello'); // KO
expect('Hello').toEqual('Hello'); // OK
});
describe('Big block', () => {
beforeEach(() => {
// Execute this code before each it
});
afterEach(() => {
// Execute this code after each it
});
it('Some test', () => { });
it('Other test', () => { });
});
It has a very complex definition
Replace a real function or var, with other that we want
const foo = jest.fn();
const hello = 'Hello JsDayCan!';
foo(hello);
expect(foo).toHaveBeenCalled();
expect(foo).toHaveBeenCalledWith(hello);
const hello = 'Hello JsDayCan!';
const bye = 'Happy coding!';
const foo = jest.fn( (message) => bye);
const response = foo(hello);
expect(foo).toHaveBeenCalled();
expect(foo).toHaveBeenCalledWith(hello);
expect(response).toEqual(bye);
Very helpfull!!
// 1
it('should display all categories', () => {
expect(foundCategories).toEqual(categories.length);
});
// 2
it('should display all categories', () => {
expect(foundCategories).toEqual(categories.length); // But... what is it?
});
// 3
it('should display all categories', () => {
const categories = ['education', 'terror', 'technology'];
const foundCategories; // ?
expect(foundCategories).toEqual(categories.length);
});
// 4
it('should display all categories', () => {
const categories = ['education', 'terror', 'technology'];
const foundCategories = getCategories(/* need CategoryRepository*/);
expect(foundCategories).toEqual(categories.length);
});
// 5
it('should display all categories', () => {
const categories = ['education', 'terror', 'technology'];
const categoryRepository = new CategoryRepository();
const foundCategories = getCategories(categoryRepository); // it calls DB
expect(foundCategories).toEqual(categories.length);
});
// 6
it('should display all categories', () => {
const categories = ['education', 'terror', 'technology'];
const categoryRepository = new CategoryRepository();
categoryRepository.getAll = jest.fn(() => categories); // mock the result
const foundCategories = getCategories(categoryRepository);
expect(foundCategories).toEqual(categories.length);
});
SCHRÖDINGER CAT
This tests take a lot of time to execute
So it's very important to be very specific
TIP: The goal is not to break the app
TIP: Take small steps
Remember true === true?Â
C:\Users\adria\IdeaProjects\jsday-2k19>npm run new-fc
> react-boilerplate@0.1.0 new-fc C:\Users\adria\IdeaProjects\jsday-2k19
> hygen fc new
√ What path do you want? · containers
√ What name do you want? · VideosNavbar
Loaded templates: C:\Users\adria\IdeaProjects\jsday-2k19\_templates
added: src/containers/VideosNavbar/VideosNavbar.tsx
added: src/containers/VideosNavbar/index.ts
added: src/containers/VideosNavbar/VideosNavbar.spec.tsx
added: src/containers/VideosNavbar/VideosNavbar.stories.tsx
added: src/containers/VideosNavbar/VideosNavbar.scss
Later we can decide if we should remove it
// 1
describe('VideosNavbar', () => {
it('should display the default message', () => {
const { queryByText } = render(
<VideosNavbar/>,
);
expect(
queryByText('Hello from VideosNavbar!')
).toBeTruthy();
});
});
// 1
export const VideosNavbar: React.FC<{}> = () => (
<nav className="VideosNavbar">
Hello from VideosNavbar!
</nav>
);
// 2
describe('CategoryTitle', () => {
it('should display the default message', () => {
const { queryByText } = render(
<CategoryTitle/>,
);
expect(
queryByText('Hello from CategoryTitle!')
).toBeTruthy();
});
});
// 2
export const CategoryTitle: React.FC<{}> =
() => (
<div className="CategoryTitle">
Hello from CategoryTitle!
</div>
);
// 3
describe('CategoryTitle', () => {
it('should display the default message', () => {
const aCategoryTitle = 'Irrelevant title';
const { queryByText } = render(
<CategoryTitle>{aCategoryTitle}</CategoryTitle>,
);
expect(
queryByText(aCategoryTitle)
).toBeTruthy();
});
});
// 4
export const CategoryTitle: React.FC<{}> =
({...props}) => (
<h3 className="CategoryTitle" {...props} />
);
// 3
describe('CategoryTitle', () => {
it('should display the default message', () => {
const aCategoryTitle = 'Irrelevant title';
const { queryByText } = render(
<CategoryTitle>{aCategoryTitle}</CategoryTitle>,
);
expect(
queryByText(aCategoryTitle)
).toBeTruthy();
});
});
Refactor?
Storybook?
Sass?
CategoryTitle
VideoItem
// 1
export const VideosNavbar: React.FC<{}> = () => (
<nav className="VideosNavbar">
Hello from VideosNavbar!
</nav>
);
// 1
describe('VideoItem', () => { //Same as Category
it('should display the default message', () => {
const selectAVideo = jest.fn();
const aVideoName = 'irrelevant video name';
const { getByText, queryByText } = render(
<VideoItem onClick={selectAVideo}>{aVideoName}</VideoItem>,
);
expect(queryByText(aVideoName)).toBeTruthy();
});
});
// 2
describe('VideoItem', () => {
it('should display the default message', () => {...});
it('should do something when is clicked', () => {
const selectAVideo = jest.fn();
const aVideoName = 'irrelevant video name';
const { getByText, queryByText } = render(
<VideoItem onClick={selectAVideo}>{aVideoName}</VideoItem>,
);
fireEvent.click(getByText(aVideoName));
expect(selectAVideo).toHaveBeenCalled();
});
});
export const VideoItem: React.FC<VideoItemProps> =
({children}) => (
<div
className="VideoItem"
>
{children}
</div>
);
export const VideoItem: React.FC<VideoItemProps> =
({onClick, children}) => (
<div
className="VideoItem"
onClick={(event) => {onClick(); }}
>
{children}
</div>
);
VideoItem - Refactoring
// 1
describe('VideoItem', () => {
it('should display the default message', () => {
const selectAVideo = jest.fn();
const aVideoName = 'irrelevant video name';
const { getByText, queryByText } = render(
<VideoItem onClick={selectAVideo}>
{aVideoName}
</VideoItem>,
);
expect(queryByText(aVideoName)).toBeTruthy();
});
it('should do something when is clicked', () => {
const selectAVideo = jest.fn();
const aVideoName = 'irrelevant video name';
const { getByText, queryByText } = render(
<VideoItem onClick={selectAVideo}>
{aVideoName}
</VideoItem>,
);
fireEvent.click(getByText(aVideoName));
expect(selectAVideo).toHaveBeenCalled();
});
});
// 2
describe('VideoItem', () => {
let selectAVideo: () => void;
let aVideoName: string;
let videoItem: RenderResult;
beforeEach(() => {
selectAVideo = jest.fn();
aVideoName = 'irrelevant video name';
videoItem = render(
<VideoItem onClick={selectAVideo}>
{aVideoName}
</VideoItem>,
);
});
it('should display the default message', () => {
expect(videoItem.queryByText(aVideoName)).toBeTruthy();
});
it('should do something when is clicked', () => {
fireEvent.click(videoItem.getByText(aVideoName));
expect(selectAVideo).toHaveBeenCalled();
});
});
All together
// 1
describe('VideosNavbar', () => {
it('should display the default message', () => {
const { queryByText } = render(
<VideosNavbar/>,
);
expect(
queryByText('Hello from VideosNavbar!')
).toBeTruthy();
});
});
// 1
export const VideosNavbar:
React.FC<{}> = () => (
<nav className="VideosNavbar">
Hello from VideosNavbar!
</nav>
);
// 2
describe('VideosNavbar', () => {
let categories: string [];
let videos: Video[];
beforeEach(() => {
categories = [];
videos = [];
});
it('should display all the videos', () => {
const properties = {categories, videos}
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
expect(
renderResult.getByTestId('VideosNavbar')
).toBeTruthy();
// expect(foundVideos.length).toBe(videos.length);
});
});
// 3
interface VideosNavbarProps {
videos: Video [];
categories: string[];
}
export const VideosNavbar:
React.FC<VideosNavbarProps>
= ({videos, categories}) => (
<nav className="VideosNavbar"
data-testid="VideosNavbar">
Hello from VideosNavbar!
</nav>
);
It should be done in only one step
// 4
describe('VideosNavbar', () => {
let categories: string [];
let videos: Video[];
beforeEach(() => {
categories = [];
videos = [];
});
it('should display all the videos', () => {
const properties = {categories, videos}
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
const foundVideos = renderResult
.queryAllByTestId('video-item', { exact: false });
expect(foundVideos.length).toBe(videos.length);
});
});
// 2
describe('VideosNavbar', () => {
let categories: string [];
let videos: Video[];
beforeEach(() => {
categories = [];
videos = [];
});
it('should display all the videos', () => {
const properties = {categories, videos}
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
expect(
renderResult.getByTestId('VideosNavbar')
).toBeTruthy();
// expect(foundVideos.length).toBe(videos.length);
});
});
Why it's green
Is that the expected behaviour
All together
// 1
it('should display a message when list of videos is empty', () => {...});
it('should display all the videos', () => {
const aVideo1: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: 'irrelevant', id: '1'}
const aVideo2: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: 'irrelevant', id: '2'}
videos = [aVideo1, aVideo2]
const properties = {categories, videos};
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
const foundVideos = renderResult
.queryAllByTestId('video-item', { exact: false });
expect(foundVideos.length).toBe(videos.length);
});
interface VideosNavbarProps {
videos: Video [];
categories: string[];
}
export const VideosNavbar: React.FC<VideosNavbarProps> =
({videos, categories}) => (
<nav className="VideosNavbar" data-testid="VideosNavbar">
{videos && videos.length === 0 && 'Not videos found'}
</nav>
);
VideosNavbar.displayName = 'VideosNavbar';
// 2
it('should display a message when list of videos is empty', () => {...});
it('should display all the videos', () => {
const aVideo1: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: 'irrelevant', id: '1'}
const aVideo2: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: 'irrelevant', id: '2'}
videos = [aVideo1, aVideo2]
const properties = {categories, videos};
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
const foundVideos = renderResult
.queryAllByTestId('video-item', { exact: false });
expect(foundVideos.length).toBe(videos.length);
});
interface VideosNavbarProps {...}
export const VideosNavbar: React.FC<VideosNavbarProps>
= ({videos, categories}) => {
const handleClick = () => {
throw new Error('Uninplemented method');
};
return (
<nav className="VideosNavbar" data-testid="VideosNavbar">
{videos && videos.length === 0 && 'Not videos found'}
{videos.map((video) =>
<VideoItem
key={video.id}
onClick={handleClick}
data-testid="video-item"/>,
)}
</nav>
);
};
...Same with categories
All together
Okey... but we want to group videos by category...
it('should display the videos classified by categories', () => {
const terrorCategory = 'terror';
const actionCategory = 'action';
const aVideo1: Video =
{title: 'irrelevant', url: 'irrelevant', comments: [], category: terrorCategory, id: '1'};
const aVideo2: Video =
{title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '2'};
const aVideo3: Video =
{title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '3'};
categories = [terrorCategory, actionCategory];
videos = [aVideo1, aVideo2, aVideo3];
const properties = {categories, videos};
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
const videosInTerror = []; // ?
const videosInAction = []; // ?
expect(videosInTerror.length).toBe(1);
expect(videosInAction.length).toBe(2);
// what happen in videos without category? -> new test
});
All together
it('should display the videos classified by categories', () => {
const terrorCategory = 'terror';
const actionCategory = 'action';
const aVideo1: Video =
{title: 'irrelevant', url: 'irrelevant', comments: [], category: terrorCategory, id: '1'};
const aVideo2: Video =
{title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '2'};
const aVideo3: Video =
{title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '3'};
categories = [terrorCategory, actionCategory];
videos = [aVideo1, aVideo2, aVideo3];
const properties = {categories, videos};
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
const videosInTerror = renderResult
.getByTestId('category-block-terror')
.querySelectorAll('[data-testid=\'video-item\']');
const videosInAction = renderResult
.getByTestId('category-block-action')
.querySelectorAll('[data-testid=\'video-item\']');
expect(videosInTerror.length).toBe(1);
expect(videosInAction.length).toBe(2);
// what happen in videos without category? -> new test
});
All together - Result
export const VideosNavbar: React.FC<VideosNavbarProps> = ({videos, categories}) => {
const [classifiedVideos, setClassifiedVideos] = React.useState({} as ClassifiedVideos);
const isVideoOnCategory = (category: string) =>
(video: Video) => category === video.category;
React.useEffect( () => {
setClassifiedVideos(
categories.reduce((total, category) => {
const foundedVideosOnCategory = videos
.filter(isVideoOnCategory(category));
return {
...total,
[category]: foundedVideosOnCategory
};
}, {}),
);
}, []);
const handleClick = () => {
return new Error('Uninplemented method');
};
return (...)
};
return (
<nav className="VideosNavbar" data-testid="VideosNavbar">
{videos && videos.length === 0 && 'Not videos found'}
{categories.map((category, index) =>
<section key={`category-block-${category}`}
data-testid={`category-block-${category}`}>
<CategoryTitle data-testid={`category-title-${index}`}>
{category}</CategoryTitle>
{classifiedVideos[category]
&& classifiedVideos[category].map( (video) =>
<VideoItem
key={video.id}
onClick={handleClick}
data-testid="video-item"/>,
)}
</section>)}
</nav>
);
But... it's a bit dirty
We can use helpers to make it more redeable
it('should display the videos classified by categories', () => {
const terrorCategory = 'terror';
const actionCategory = 'action';
const aVideo1: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: terrorCategory, id: '1'};
const aVideo2: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '2'};
const aVideo3: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '3'};
categories = [terrorCategory, actionCategory];
videos = [aVideo1, aVideo2, aVideo3];
const properties = {categories, videos};
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
const videosInTerror = renderResult
.getByTestId('category-block-terror')
.querySelectorAll('[data-testid=\'video-item\']');
const videosInAction = renderResult
.getByTestId('category-block-action')
.querySelectorAll('[data-testid=\'video-item\']');
expect(videosInTerror).toBe(1);
expect(videosInAction).toBe(2);
// what happen in videos without category? -> new test
});
const buildVideoWidth = ({
id = 'irrelevant',
title = 'irrelevant',
category = 'irrelevant',
url = 'irrelevant',
comments= [],
}): Video => ({
id,
title,
category,
url,
comments,
});
it('should display the videos classified by categories', () => {
const terrorCategory = 'terror';
const actionCategory = 'action';
const aVideo1: Video = buildVideoWidth({id : '1', category: terrorCategory});
const aVideo2: Video = buildVideoWidth({id : '2', category: actionCategory});
const aVideo3: Video = buildVideoWidth({id : '3', category: actionCategory});
categories = [terrorCategory, actionCategory];
videos = [aVideo1, aVideo2, aVideo3];
const properties = {categories, videos};
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
const videosInTerror = renderResult
.getByTestId('category-block-terror')
.querySelectorAll('[data-testid=\'video-item\']');
const videosInAction = renderResult
.getByTestId('category-block-action')
.querySelectorAll('[data-testid=\'video-item\']');
expect(videosInTerror).toBe(1);
expect(videosInAction).toBe(2);
// what happen in videos without category? -> new test
});
But... it's a bit dirty
it('should display the videos classified by categories', () => {
const terrorCategory = 'terror';
const actionCategory = 'action';
const aVideo1: Video = buildVideoWidth({id : '1', category: terrorCategory});
const aVideo2: Video = buildVideoWidth({id : '2', category: actionCategory});
const aVideo3: Video = buildVideoWidth({id : '3', category: actionCategory});
categories = [terrorCategory, actionCategory];
videos = [aVideo1, aVideo2, aVideo3];
const properties = {categories, videos};
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
const videosInTerror = renderResult
.getByTestId('category-block-terror')
.querySelectorAll('[data-testid=\'video-item\']');
const videosInAction = renderResult
.getByTestId('category-block-action')
.querySelectorAll('[data-testid=\'video-item\']');
expect(videosInTerror).toBe(1);
expect(videosInAction).toBe(2);
// what happen in videos without category? -> new test
});
const findVideosInCategory = (
renderResult: RenderResult,
categoryBlockTestId: string) =>
renderResult
.getByTestId(categoryBlockTestId)
.querySelectorAll(
'[data-testid=\'video-item\']'
);
it('should display the videos classified by categories', () => {
const terrorCategory = 'terror';
const actionCategory = 'action';
const aVideo1: Video = buildVideoWidth({id : '1', category: terrorCategory});
const aVideo2: Video = buildVideoWidth({id : '2', category: actionCategory});
const aVideo3: Video = buildVideoWidth({id : '3', category: actionCategory});
categories = [terrorCategory, actionCategory];
videos = [aVideo1, aVideo2, aVideo3];
const properties = {categories, videos};
const renderResult: RenderResult = render(
<VideosNavbar {...properties}/>,
);
const videosInTerror = findVideosInCategory(renderResult, 'category-block-terror');
const videosInAction = findVideosInCategory(renderResult, 'category-block-action');
expect(videosInTerror).toBe(1);
expect(videosInAction).toBe(2);
// what happen in videos without category? -> new test
});
Now we have to retrieve the data... but where are they?
We don't have to decide it yet
We can continue developing and make the desicion later
Let's create a test
it('should get the videos when component is loaded', () => {
// type is not defined
const videosRepository: VideosRepository;
expect(videosRepository.getVideos)
.toHaveBeenCalledTimes(1);
});
And imagine what is the perfect api that we want
export interface VideosRepository {
getVideos: () => Promise<Video[]>;
}
it('should get the videos when component is loaded', () => {
const videosRepository: VideosRepository = {
getVideos: jest.fn(() => Promise.resolve([]),
};
expect(videosRepository.getVideos).toHaveBeenCalledTimes(1);
});
it('should get the the videos when component is loaded',
() => {
const videosRepository: VideosRepository = {
getVideos: jest.fn(
() => Promise.resolve([])
),
};
const renderResult: RenderResult =
render(
<Videos
dependencies={{videosRepository}}
/>,
);
expect(videosRepository.getVideos)
.toHaveBeenCalledTimes(1);
});
// 1
export const Videos: React.FC<{}> = () => {
const [selectedVideo, setSelectedVideo] = useState(null);
const handleSave = () => {
throw new Error('Not implemented');
};
return <section className="Videos">
<VideosNavbar videos={[]} categories={[]}/>
<VideoDetails selectedVideo={selectedVideo} onSave={handleSave}/>
</section>;
};
// 2
interface VideosProps {
dependencies: {
videosRepository: VideosRepository,
};
}
export const Videos: React.FC<VideosProps> = ({dependencies}) => {
const { videosRepository } = dependencies;
const [selectedVideo, setSelectedVideo] = useState(null);
const handleSave = () => {
throw new Error('Not implemented');
};
return <section className="Videos">
<VideosNavbar videos={[]} categories={[]}/>
<VideoDetails selectedVideo={selectedVideo} onSave={handleSave}/>
</section>;
};
// 3
interface VideosProps {
dependencies: {
videosRepository: VideosRepository,
};
}
export const Videos: React.FC<VideosProps> = ({dependencies}) => {
const { videosRepository } = dependencies;
const [videos, setVideos] = React.useState([] as Video[]);
React.useEffect(() => {
videosRepository.getVideos().then(setVideos);
}, []);
const [selectedVideo, setSelectedVideo] = useState(null);
const handleSave = () => {
throw new Error('Not implemented');
};
return <section className="Videos">
<VideosNavbar videos={videos} categories={[]}/>
<VideoDetails selectedVideo={selectedVideo} onSave={handleSave}/>
</section>;
};
In our App we will initialize all our dependencies and pass them in to the rest of the application
const dependencies = {
videosRepository: { getVideos: () => Promise.resolve([])},
}
return (
<div className="App">
<header className="App-header" >
My videos
</header>
<section className="App-content">
<Videos dependencies={dependencies}/>
</section>
</div>
);
const dependencies = {
videosRepository: new VideosRepository(),
}
return (
<div className="App">
<header className="App-header" >
My videos
</header>
<section className="App-content">
<Videos dependencies={dependencies}/>
</section>
</div>
);
We can make it more complex by adding more dependencies:
describe('Videos', () => {
let dependencies: AppDependencies;
let videosRepository: VideosRepository;
let categoriesRepository: CategoriesRepository;
beforeEach(() => {
videosRepository = { getVideos: jest.fn(() => Promise.resolve([])), };
categoriesRepository = { getCategories: jest.fn(() => Promise.resolve([])), };
dependencies = { videosRepository, categoriesRepository};
});
it('should get the starting values when component is loaded', () => {
const renderResult: RenderResult = render(
<Videos dependencies={dependencies}/>,
);
expect(videosRepository.getVideos).toHaveBeenCalledTimes(1);
expect(categoriesRepository.getCategories).toHaveBeenCalledTimes(1);
});
});
Or we can work with real values and mock it:
describe('Videos', () => {
let dependencies: AppDependencies;
beforeEach(() => {
dependencies = initializeDependencies();
dependencies.videosRepository.getVideos = jest.fn(() => Promise.resolve([]));
dependencies.categoriesRepository.getCategories = jest.fn(() => Promise.resolve([]));
});
it('should get the starting values when component is loaded', () => {
const {videosRepository, categoriesRepository} = dependencies;
const renderResult: RenderResult = render(
<Videos dependencies={dependencies}/>,
);
expect(videosRepository.getVideos).toHaveBeenCalledTimes(1);
expect(categoriesRepository.getCategories).toHaveBeenCalledTimes(1);
});
});
It's very IMPORTANT to test the Promise.rejected too
More than the success case
If we found a bug when we are coding
We should create a new cases reproduce it
And then fix it
You can test your parent component like this
describe('VideoItem', () => {
let dependencies: AppDependencies;
let store: Store;
describe('Scope 1', () => {
before(() => {
//...
});
it('should load the user information', () => {...});
it('should save the user information', () => {...});
});
describe('Scope 2', () => {...});
});
const dependencies: AppDependencies = initializeDependencies();
// Override dependencies when components mount
dependencies.videosRepository.getVideos = () => Promise.resolve([]);
store = initializeStore():
const parentContainer =
render(
<Provider store={store}>
<Videos dependencies={dependencies}/>
</Provider>,
);
React Hooks
React
evolves
Enzyme
React-testing-library
We made a mistake by not wrapping up our libs
Wrap it!!
PASS src/components/Commment/Commment.spec.tsx
-------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-------------------|----------|----------|----------|----------|-------------------|
All files | 96.87 | 96.87 | 93.75 | 96.87 | |
App.tsx | 100 | 100 | 100 | 100 | |
CategoryTitle.tsx | 100 | 100 | 100 | 100 | |
Comments.tsx | 100 | 100 | 100 | 100 | |
ReplyComment.tsx | 100 | 100 | 100 | 100 | |
VideoDetails.tsx | 75 | 83.33 | 50 | 75 | 19 |
VideoItem.tsx | 100 | 100 | 100 | 100 | |
Videos.tsx | 100 | 100 | 100 | 100 | |
VideosNavbar.tsx | 100 | 100 | 100 | 100 | |
-------------------|----------|----------|----------|----------|-------------------|
Test Suites: 8 passed, 8 total
Tests: 15 passed, 16 total
Snapshots: 0 total
Time: 4.86s
Ran all test suites.