Fireproof components
Who Am I?
- Full Stack Developer at Lean Mind
- Passionate about development
- Typescript lover 💙
Adrián Ferrera González
@afergon
adrian-afergon
What we will talk about?
- The problem
- What worked for me
- Mistakes
Why?
- Long components
- No layer overlap
- Time spend testing Manually
- Merge all of them
Long components
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>
Long components
But we want:
- To understand what we are seeing.
- Make easy to maintain it by other developers.
- We want to make changes without risk.
- We want to delegate responsabilities.
- Have a good performance
We don't want to reuse it.
Layer overlap
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 */);
};
Layer overlap
Overlaped Component:
Single Responsability:
My Solution
What is TDD?
Write a test,
watch it fail.
Write just enough code to pass the test
Improve the code wothout changing its behavior
TDD
Test
CodeÂ
Refactor
Classic way:
TDD way:
Why we use it?
Fast Feedback
High code coverage
Quality assurance
Other way to document
It's necessary?
- expect(true).toBeTruthy()
- You don't trust in your libs
- I'm faster without TDD
- It's imposible to test the unexistent
- My client doesn't want it!!!
Propably you have lisened something like...
This isn't about TDD,
it's about the way you testÂ
- How much time do you expend finding bugs?
- Are you testing code or features?
- Do you tell your surgeon how to do it?
What is the purpouse?
It's not about testing
It's all about developing in small steps
Using the tests as tools
Before starting
let's talk about concepts
Tests Types
- e2e
- Unitary
- Integration
That I ussually use
Tests Types
- e2e
That I ussually use
Expects are use cases
Clone of real environment
Too expensive
Real coverage
Tests Types
- Unitary
That I ussually use
Check concrete behaviors
Specific configurations are not required
Faster and cheaper
Local component coverage
Tests Types
- Integration
That I ussually use
Check behaviors with broader scope
Requires mocks to set limits
Perfect balance
Important coverage but not always real
Tests Types
- e2e
- Unitary
- Integration
That I ussually use
IMPORTANT!!!
You need an architecture!!
References for Architecture
Books:
Videos:
The Approach
OUTSIDE-IN
INSIDE-OUT
Emerging architecture
Componentisation
Predefined architecture
Fuzzy components
London
Chicago
Testing Concepts
describe('Big block', () => {
it('The functionality', () => {
});
});
Scope
Testing Concepts
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
});
Matchers
Testing Concepts
describe('Big block', () => {
beforeEach(() => {
// Execute this code before each it
});
afterEach(() => {
// Execute this code after each it
});
it('Some test', () => { });
it('Other test', () => { });
});
Utils
Testing Concepts
Mock
It has a very complex definition
Replace a real function or var, with other that we want
Using Jest:
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);
Testing Concepts
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);
});
Go from expect to definition
Black-box
SCHRÖDINGER CAT
Testign Concepts
REAL USE CASE
Define use cases
- We want to display a list of videos ordered by category.
Display categorized videos
- We want to pick a video and display it and its comments.
Select video
Add comments
- Can add new comment and response other comments
Define use cases
Define use cases
Define use cases
This tests take a lot of time to execute
So it's very important to be very specific
Conclusions
Emergent Design
How can we do it
Higher Order Components
TIP: The goal is not to break the app
TIP: Take small steps
Remember true === true?Â
Let's use our favorites tools to automate work
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
Emergent Design
// 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
Emergent Design
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>
);
Emergent Design
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();
});
});
Emergent Design
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
Emergent Design
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
Emergent Design
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
});
Emergent Design
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
});
Emergent Design
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>
);
Emergent Design
It works!
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
});
It works!
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
});
Conclusions
- Working code at all times
- Overengineering avoidance
- Discover new cases
TDD
Test
CodeÂ
Refactor
Emergent Design
Delay decisions
Now we have to retrieve the data... but where are they?
- API REST using fetch?
- GraphQL using a client?
- Local database?
- Firebase?
We don't have to decide it yet
We can continue developing and make the desicion later
How?
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);
});
Delay decisions
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>;
};
Delay decisions
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>
);
Delay decisions
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);
});
});
Delay decisions
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);
});
});
Delay decisions
It's very IMPORTANT to test the Promise.rejected too
More than the success case
Delay decisions
Conclusions
- Postpone decisions
- Economic tests for real cases
- Check the integration
- This is not a truly black-box, but feels like it is
Delay decisions
Some warnings
What happen with bugs?
If we found a bug when we are coding
We should create a new cases reproduce it
And then fix it
And Redux?
How can I do 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>,
);
Third party libraries
Evolution Problem
React Hooks
React
evolves
Enzyme
React-testing-library
We made a mistake by not wrapping up our libs
Wrap it!!
Check the coverage
To discover new cases
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.
Posible or Imposible?
Conclusions
- It's efficent
- Powerfull tool
- Easy for other developers
Positive
Negative
- Requires knowledge
- Requires practice
Questions?
Thank you!!
Fireproof Components
By afergon
Fireproof Components
- 488