Frontend
Possible or impossible?
Adrián Ferrera González
@afergon
adrian-afergon
Lean Coders
Classic way:
TDD way:
FF -> Fast feedback
Hight code coverage
Ensure the features
Other way to document
This is not about TDD,
is about the way you test
This is not about create test
Is about develop based in use small use cases
Using the test as tools
Expects are use cases
Clone of real environment
Are to expensive
Real coverage
Expects are abstraction functionalities
No require specific configs
Are faster and cheaper
Local component coverage
Expects are abstraction functionalities
Require mocks to set limits
Perfect balance
Important coverage but not always real
IMPORTANT!!!
You need an architecture!!
Emerging architecture
Componentisation
Predefined architecture
Fuzzy components
Replace a real function or var, by other that we want
Lisent a value for make questions
const foo = jest.fn();
const message = 'Hello TLP Innova';
foo(message);
expect(foo).toHaveBeenCalled();
expect(foo).toHaveBeenCalledWith(message);
const spy = jest.spyOn(video, 'play', 'get'); // we pass 'get'
const isPlaying = video.play;
expect(spy).toHaveBeenCalled();
expect(isPlaying).toBe(true);
spy.mockRestore();
Mock a function or mock an object
SCHRÖDINGER CAT
React
Redux
Hooks
Library to create components
Pattern to handle the state
Functions to share logic
We have to mount it
We have to use as black-box
We have to test independently
Providers
React Tool to handle the context
This tests take a lot of time to be runned
So it is very important to be very specific
TIP: The end goal is not to break the app
TIP: Don't rush through the code
# Help TIP 1:
describe('Champion items', () => {
it('should be displayed', () => {
expect(wrapper.exists()).toBeTruthy();
});
});
describe('Champion items', () => {
it('should be displayed', () => {
expect(wrapper.exists()).toBeTruthy(); // what is wrapper?
});
});
describe('Champion items', () => {
it('should be displayed', () => {
const wrapper = shallow(
<ChampionItems
service={aService}
isLoading={aLoadState}
/>
);
expect(wrapper.exists()).toBeTruthy();
});
});
describe('Champion items', () => {
it('should be displayed', () => {
const wrapper = shallow(
<ChampionItems
service={aService}
isLoading={aLoadState}
/>
); // and this props? :(
expect(wrapper.exists()).toBeTruthy();
});
});
describe('Champion items', () => {
it('should be displayed', () => {
const aLoadState = false;
const aService = { getItems: () => Promise.resolve(someItems)}
const wrapper = shallow(
<ChampionItems
service={aService}
isLoading={aLoadState}
/>
);
expect(wrapper.exists()).toBeTruthy();
});
});
describe('Champion items', () => {
it('should be displayed', () => {
const someItems = ['B.F. Sword', 'Recurve Bow', 'Spatula'];
const aLoadState = false;
const aService = { getItems: () => Promise.resolve(someItems)}
const wrapper = shallow(
<ChampionItems
service={aService}
isLoading={aLoadState}
/>
);
expect(wrapper.exists()).toBeTruthy();
});
});
TODO: introduce jest
# Help TIP 2:
Remeber true === true?
import { shallow } from 'enzyme';
import * as React from 'react';
import { Image} from './';
describe('Image', ()=> {
it('should display the component', ()=>{
const wrapper = shallow(<Image />);
expect(wrapper.exists()).toBeTruthy();
});
});
import { shallow } from 'enzyme';
import * as React from 'react';
import { Image} from './';
describe('Image', ()=> {
it('should display the component', ()=>{
const wrapper = shallow(<Image />);
expect(wrapper.exists()).toBeTruthy();
});
it('should display the image source', function () {
const wrapper = shallow(<Image src="irrelevant source"/>);
expect(wrapper.exists()).toBeTruthy();
});
});
import { shallow } from 'enzyme';
import * as React from 'react';
import { Image} from './';
describe('Image', ()=> {
it('should display the image source', function () {
const wrapper = shallow(<Image src="irrelevant source"/>);
expect(wrapper.exists()).toBeTruthy();
});
});
Refactor
import * as React from 'react';
import './Image.scss';
type ImageProps = React.DetailedHTMLProps<
React.ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
>;
export const Image: React.FC<ImageProps> = (props) => (
<img {...props} />
);
Image.displayName = 'Image';
Implement
import { shallow } from 'enzyme';
import * as React from 'react';
import { Image} from './';
describe('Image', ()=> {
it('should display the component', ()=>{
const wrapper = shallow(<Image />);
expect(wrapper.exists()).toBeTruthy();
});
it('should display the image source', function () {
const wrapper = shallow(<Image src="irrelevant source"/>);
expect(wrapper.exists()).toBeTruthy();
});
});
import * as React from 'react';
import './ChampionImage.scss';
import { Image } from '../Image/';
type ChampionImageProps = {
id:string,
name: string,
src: string,
onClick: (id: string) => void
};
export const ChampionImage: React.FC<ChampionImageProps> =
({id, name, src, onClick}) => {
const handleClick = () => {
onClick(id)
};
return (
<div onClick={handleClick}>
<Image src={src} alt={name} data-test-id="image"/>
</div>
)};
ChampionImage.displayName = 'ChampionImage';
it('should find the image inside', () => {
});
it('should call an event when is clicked', () => {
});
it('should find the image inside', () => {
const wrapper = shallow(
<ChampionImage
src="irrelevant champion image"
name="Irrelevant champion name"
/>);
expect(
wrapper.find('[data-test-id="image"]').exists()
).toBeTruthy();
});
it('should call an event when is clicked', () => {
});
it('should find the image inside', () => {
const wrapper = shallow(
<ChampionImage
src="irrelevant champion image"
name="Irrelevant champion name"
/>);
expect(
wrapper.find('[data-test-id="image"]').exists()
).toBeTruthy();
});
it('should call an event when is clicked', () => {
});
import * as React from 'react';
import './ChampionImage.scss';
import { Image } from '../Image/';
type ChampionImageProps = {
name: string,
src: string,
};
export const ChampionImage: React.FC<ChampionImageProps> =
({name, src}) => {
return (
<div>
<Image src={src} alt={name} data-test-id="image"/>
</div>
)};
ChampionImage.displayName = 'ChampionImage';
it('should find the image inside', () => {
const wrapper = shallow(
<ChampionImage
src="irrelevant champion image"
name="Irrelevant champion name"
/>);
expect(
wrapper.find('[data-test-id="image"]').exists()
).toBeTruthy();
});
it('should call an event when is clicked', () => {
const aClickHandler = jest.fn();
const wrapper = shallow(
<ChampionImage
id="irrelevant id"
src="irrelevant champion image"
name="Irrelevant champion name"
onClick={aClickHandler}
/>);
wrapper.simulate('click');
expect(aClickHandler).toHaveBeenCalledWith("irrelevant id");
});
import * as React from 'react';
import './ChampionImage.scss';
import { Image } from '../Image/';
type ChampionImageProps = {
id?:string,
name: string,
src: string,
onClick?: (id: string) => void
};
export const ChampionImage: React.FC<ChampionImageProps> =
({id, name, src, onClick}) => {
const handleClick = () => {
if(onClick && id) {
onClick(id)
}
};
return (
<div onClick={handleClick}>
<Image src={src} alt={name} data-test-id="image"/>
</div>
)};
ChampionImage.displayName = 'ChampionImage';
it('should find the image inside', () => {
const wrapper = shallow(
<ChampionImage
src="irrelevant champion image"
name="Irrelevant champion name"
/>);
expect(
wrapper.find('[data-test-id="image"]').exists()
).toBeTruthy();
});
it('should call an event when is clicked', () => {
const aClickHandler = jest.fn();
const wrapper = shallow(
<ChampionImage
id="irrelevant id"
src="irrelevant champion image"
name="Irrelevant champion name"
onClick={aClickHandler}
/>);
wrapper.simulate('click');
expect(aClickHandler).toHaveBeenCalledWith("irrelevant id");
});
Implement
x2
# Help TIP 3:
When we pass the test, we can refactor this code
type ChampionId = string;
interface Champion {
id: ChampionId,
name: string,
image: string
}
let aChampion: Champion;
beforeEach(() => {
aChampion = {
id: "Irrelevant champion id",
name: "Irrelevant champion name",
image: "Irrelevant champion image"
}
});
type ChampionImageProps = {
id?:string,
name: string,
src: string,
onClick?: (id: string) => void
champion?: Champion;
}
<div onClick={handleClick}>
<Image src={src} alt={name} data-test-id="image"/>
</div>
<div onClick={handleClick}>
<Image src={champion.image} alt={champion.name} data-test-id="image"/>
</div>
it('should find the image inside', () => {
const wrapper = shallow(
<ChampionImage
src="irrelevant champion image"
name="Irrelevant champion name"
/>);
expect(
wrapper.find('[data-test-id="image"]').exists()
).toBeTruthy();
});
it('should call an event when is clicked', () => {
const aClickHandler = jest.fn();
const wrapper = shallow(
<ChampionImage
id="irrelevant id"
src="irrelevant champion image"
name="Irrelevant champion name"
onClick={aClickHandler}
/>);
wrapper.simulate('click');
expect(aClickHandler).toHaveBeenCalledWith("irrelevant id");
});
it('should find the image inside', () => {
const wrapper = shallow(
<ChampionImage
src="irrelevant champion image"
name="Irrelevant champion name"
champion={aChampion}
/>);
expect(
wrapper.find('[data-test-id="image"]').exists()
).toBeTruthy();
});
it('should call an event when is clicked', () => {
const aClickHandler = jest.fn();
const wrapper = shallow(
<ChampionImage
id="irrelevant id"
src="irrelevant champion image"
name="Irrelevant champion name"
champion={aChampion}
onClick={aClickHandler}
/>);
wrapper.simulate('click');
expect(aClickHandler).toHaveBeenCalledWith("irrelevant id");
});
it('should find the image inside', () => {
const wrapper = shallow(
<ChampionImage
champion={aChampion}
/>);
expect(
wrapper.find('[data-test-id="image"]').exists()
).toBeTruthy();
});
it('should call an event when is clicked', () => {
const aClickHandler = jest.fn();
const wrapper = shallow(
<ChampionImage
champion={aChampion}
onClick={aClickHandler}
/>);
wrapper.simulate('click');
expect(aClickHandler).toHaveBeenCalledWith("irrelevant id");
});
Now let's create a Champion selector
import { shallow } from 'enzyme';
import * as React from 'react';
import { ChampionSelector} from './';
describe('ChampionSelector', ()=> {
it('should display the component', ()=>{
const wrapper = shallow(<ChampionSelector />);
expect(wrapper.exists()).toBeTruthy();
})
});
champions
import { shallow } from 'enzyme';
import * as React from 'react';
import { ChampionSelector} from './';
import {Champion} from "../ChampionImage/ChampionImage.spec";
describe('ChampionSelector', ()=> {
it('should display the champions', ()=>{
const someChampions: Champion[] = [];
const wrapper = shallow(<ChampionSelector champions={someChampions}/>);
const championImages = wrapper.find('ChampionImage');
expect(championImages.length).toBe(someChampions.length);
})
});
import { shallow } from 'enzyme';
import * as React from 'react';
import { ChampionSelector} from './';
import {Champion} from "../ChampionImage/ChampionImage.spec";
describe('ChampionSelector', ()=> {
it('should display the champions', ()=>{
const someChampions: Champion[] = [aatrox, kayle, fiora];
const wrapper = shallow(<ChampionSelector champions={someChampions}/>);
const championImages = wrapper.find('ChampionImage');
expect(championImages.length).toBe(someChampions.length);
})
});
Building helpers
import { shallow } from 'enzyme';
import * as React from 'react';
import { ChampionSelector} from './';
import {Champion} from "../ChampionImage/ChampionImage.spec";
describe('ChampionSelector', ()=> {
it('should display the champions', ()=>{
const someChampions: Champion[] = [aatrox, kayle, fiora];
const wrapper = shallow(<ChampionSelector champions={someChampions}/>);
const championImages = wrapper.find('ChampionImage');
expect(championImages.length).toBe(someChampions.length);
})
});
const aatrox: Champion = {};
const kayle: Champion = {};
const fiora: Champion = {};
const someChampions: Champion[] = [aatrox, kayle, fiora];
const buildChampion = ({id, name, image}: Champion):Champion => ({
id: id ? id : 'irrelevant id',
name: name ? name : 'irrelevant name',
image: image ? image : 'irrelevant image'
});
it('should display the champions', ()=>{
const aatrox: Champion = buildChampion({name: 'Aatrox'} as Champion);
const kayle: Champion = buildChampion({name: 'Kayle'} as Champion);
const fiora: Champion = buildChampion({name: 'Fiora'} as Champion);
const someChampions: Champion[] = [atrox, kayle, fiora];
const wrapper = shallow(<ChampionSelector champions={someChampions}/>);
const championImages = wrapper.find('ChampionImage');
expect(championImages.length).toBe(someChampions.length);
});
# Help TIP 4:
Now we should implement for green
import * as React from 'react';
import './ChampionSelector.scss';
import {Champion} from "../ChampionImage/ChampionImage.spec";
import {ChampionImage} from "../ChampionImage";
interface ChampionSelectorProps {
champions: Champion[];
}
export const ChampionSelector: React.FC<ChampionSelectorProps>
= ({champions}) =>
champions.map((champion) =>
<ChampionImage
key={champion.id}
champion={champion}
/>
);
ChampionSelector.displayName = 'ChampionSelector';
it('should display the champions', ()=>{
const aatrox: Champion = buildChampion({name: 'Aatrox'} as Champion);
const kayle: Champion = buildChampion({name: 'Kayle'} as Champion);
const fiora: Champion = buildChampion({name: 'Fiora'} as Champion);
const someChampions: Champion[] = [atrox, kayle, fiora];
const wrapper = shallow(<ChampionSelector champions={someChampions}/>);
const championImages = wrapper.find('ChampionImage');
expect(championImages.length).toBe(someChampions.length);
});
Implement
it('should select a champion when is clicked', () => {
const aSelectHandler = jest.fn();
const aatrox: Champion = buildChampion({name: 'Aatrox'} as Champion);
const kayle: Champion = buildChampion({name: 'Kayle'} as Champion);
const fiora: Champion = buildChampion({name: 'Fiora'} as Champion);
const someChampions: Champion[] = [aatrox, kayle, fiora];
const wrapper = shallow(
<ChampionSelector
champions={someChampions}
onSelect={aSelectHandler}
/>);
const aatroxImage = wrapper.find('[data-test-id]="Aatrox"');
aatroxImage.simulate('click');
expect(aSelectHandler).toHaveBeenCalled();
});
it('should select a champion when is clicked', () => {
const aSelectHandler = jest.fn();
const aatrox: Champion = buildChampion({name: 'Aatrox'} as Champion);
const kayle: Champion = buildChampion({name: 'Kayle'} as Champion);
const fiora: Champion = buildChampion({name: 'Fiora'} as Champion);
const someChampions: Champion[] = [aatrox, kayle, fiora];
const wrapper = shallow(
<ChampionSelector
champions={someChampions}
onSelect={aSelectHandler}
/>);
const aatroxImage = wrapper.find('[data-test-id]="Aatrox"');
aatroxImage.simulate('click');
expect(aSelectHandler).toHaveBeenCalled();
});
Implement
import * as React from 'react';
import './ChampionSelector.scss';
import {Champion} from "../ChampionImage/ChampionImage.spec";
import {ChampionImage} from "../ChampionImage";
interface ChampionSelectorProps {
champions: Champion[];
}
export const ChampionSelector: React.FC<ChampionSelectorProps>
= ({champions}) =>
champions.map((champion) =>
<ChampionImage
key={champion.id}
champion={champion}
onClick={onSelect}
data-test-id={champion.name}/>
);
ChampionSelector.displayName = 'ChampionSelector';
describe('ChampionSelector', () => {
const aatrox: Champion = buildChampion({name: 'Aatrox'} as Champion);
const kayle: Champion = buildChampion({name: 'Kayle'} as Champion);
const fiora: Champion = buildChampion({name: 'Fiora'} as Champion);
const someChampions: Champion[] = [aatrox, kayle, fiora];
let wrapper: ShallowWrapper;
let aSelectHandler: jest.Mock;
beforeEach(() => {
aSelectHandler = jest.fn();
wrapper = shallow(<ChampionSelector champions={someChampions} onSelect={aSelectHandler}/>);
});
it('should display the champions', () => {
const championImages = wrapper.find('ChampionImage');
expect(championImages.length).toBe(someChampions.length);
});
it('should select a champion when is clicked', () => {
const aatroxImage = wrapper.find('[data-test-id="Aatrox"]');
aatroxImage.simulate('click');
expect(aSelectHandler).toHaveBeenCalled();
});
});
const buildChampion = ({id, name, image}: Champion): Champion => ({
id: id ? id : 'irrelevant id',
name: name ? name : 'irrelevant name',
image: image ? image : 'irrelevant image',
});
Define constants
Champion Selector:
Image
Champion Image
Champion Selector
The containers are a good element to test as a black box
Let's create a containter and check what it has to do
import { mount } from 'enzyme';
import * as React from 'react';
import { ChampionsInfo } from './';
describe('ChampionsInfo', () => {
let championService: ChampionService;
beforeEach(() => {
championService = new ChampionService();
});
it('should load a champions list when ' +
'the component is displayed', () => {
championService.getChampions = jest.fn(() =>
Promise.resolve(someChampions)
);
const wrapper = mount(
<ChampionsInfo
championService={championService}
/>);
wrapper.update();
expect(championService.getChampions).toHaveBeenCalled();
});
});
import * as React from 'react';
import './ChampionsInfo.scss';
import {useEffect, useState} from 'react';
export const ChampionsInfo: React.FC<{
championService: ChampionService
}> = ({championService}) => {
const [champions, setChampions] = useState([]);
useEffect(() => {
championService
.getChampions()
.then(setChampions);
}, []);
return (
<div className="ChampionsInfo">
Hello from ChampionsInfo!
</div>
);
};
ChampionsInfo.displayName = 'ChampionsInfo';
Implement
React Hooks
React
evolves
Enzyme
React-testing-library
We make a mistake no wrapping our libs
export interface ChampionService {
getChampions: () => Promise<Champion[]>;
}
describe('ChampionsInfo', () => {
const aatrox: Champion = { info: '', image: '', id: 'irrelevant 1', name: 'aatrox' };
const kayle: Champion = { info: '', image: '', id: 'irrelevant 2', name: 'kayle' };
const fiora: Champion = { info: '', image: '', id: 'irrelevant 3', name: 'fiora' };
const someChampions: Champion[] = [aatrox, kayle, fiora];
let championService: ChampionService;
beforeEach(() => {
championService = {
getChampions: jest.fn(async () => someChampions),
};
});
it('should load a champions list when the component is displayed', async () => {
const { container } = await render(
<ChampionsInfo championService={championService} />,
);
const championSelector = container.children[0];
const section = championSelector.children[0];
const displayedChampions = section.children;
expect(championService.getChampions).toHaveBeenCalled();
expect(displayedChampions.length).toEqual(someChampions.length);
});
});
interface ChampionsInfoProps {
championService: ChampionService;
}
export const ChampionsInfo: React.FC<ChampionsInfoProps> =
({ championService }) => {
const [champions, setChampions] = useState([] as Champion[]);
useEffect(() => {
championService.getChampions().then(setChampions);
}, [championService]);
return (
<div className="ChampionsInfo">
<ChampionSelector champions={champions} onSelect={() => ''} />
</div>
);
};
ChampionsInfo.displayName = 'ChampionsInfo';
Implement
export interface ChampionService {
getChampions: () => Promise<Champion[]>;
}
describe('ChampionsInfo', () => {
const aatrox: Champion = { info: '', image: '', id: 'irrelevant 1', name: 'aatrox' };
const kayle: Champion = { info: '', image: '', id: 'irrelevant 2', name: 'kayle' };
const fiora: Champion = { info: '', image: '', id: 'irrelevant 3', name: 'fiora' };
const someChampions: Champion[] = [aatrox, kayle, fiora];
let championService: ChampionService;
beforeEach(() => {
championService = {
getChampions: jest.fn(async () => someChampions),
};
});
it('should load a champions list when the component is displayed', async () => {
const { container } = await render(
<ChampionsInfo championService={championService} />,
);
const championSelector = container.children[0];
const section = championSelector.children[0];
const displayedChampions = section.children;
expect(championService.getChampions).toHaveBeenCalled();
expect(displayedChampions.length).toEqual(someChampions.length);
});
});
it('should load a champions list when the component is displayed', async () => { ... });
it('should select a champion and display the info', () => {
const { container } = await render(
<ChampionsInfo championService={championService} />,
);
const aatroxImage = await findByTestId(container, 'aatrox-image');
fireEvent.click(aatroxImage);
const info = await findByTestId(container, 'info');
expect(info.textContent).toEqual(aatrox.info);
});
Implement
export const ChampionsInfo: React.FC<ChampionsInfoProps> = ({ championService }) => {
const [champions, setChampions] = useState([] as Champion[]);
useEffect(() => {
championService.getChampions().then(setChampions);
}, [championService]);
return (
<div className="ChampionsInfo">
<ChampionSelector champions={champions} onSelect={() => {}} />
</div>
);
};
it('should load a champions list when the component is displayed', async () => { ... });
it('should select a champion and display the info', async () => {
const { container } = await render(
<ChampionsInfo championService={championService} />,
);
const aatroxImage = await findByTestId(container, 'aatrox-image');
fireEvent.click(aatroxImage);
const info = await findByTestId(container, 'info');
expect(info.textContent).toEqual(aatrox.info);
});
export const ChampionsInfo: React.FC<ChampionsInfoProps> = ({ championService }) => {
const [champions, setChampions] = useState([] as Champion[]);
useEffect(() => {
championService.getChampions().then(setChampions);
}, [championService]);
const [selectedChampion, setSelectedChampion] = useState({} as Champion);
const selectChampion = (championId: ChampionId) => {
const foundChampion = champions.find((champion) => championId === champion.id);
setSelectedChampion(foundChampion ? foundChampion : ({} as Champion));
};
return (
<div className="ChampionsInfo">
<ChampionSelector champions={champions} onSelect={selectChampion} />
{selectedChampion && <ChampionCard champion={selectedChampion} />}
</div>
);
};
And if we use Redux or Providers?
it('should load a champions list when the component is displayed', async () => { ... });
it('should select a champion and display the info', () => {
const { container } = await render(
<ChampionsInfo championService={championService} />,
);
const aatroxImage = await findByTestId(container, 'aatrox-image');
fireEvent.click(aatroxImage);
const info = await findByTestId(container, 'info');
expect(info.textContent).toEqual(aatrox.info);
});
it('should load a champions list when the component is displayed', async () => { ... });
it('should select a champion and display the info', () => {
const stote = createAMockStore(championService);
const { container } = await render(
<Provider store={store}>
<ChampionsInfoConnected />
</Provider>,
);
const aatroxImage = await findByTestId(container, 'aatrox-image');
fireEvent.click(aatroxImage);
const info = await findByTestId(container, 'info');
expect(info.textContent).toEqual(aatrox.info);
});
Create builder with services and state
Connect to redux
Wrapp the element
If we found a bug when we are coding
We should create a new cases reproduce it
And then fix it
If you love code,
this is for you
If you hate practice...
this will be difficult to you