React And Test Driven Development: Difference between revisions
(21 intermediate revisions by the same user not shown) | |||
Line 28: | Line 28: | ||
5/ When no input is given to the map chooser function it returns a default image file name | 5/ When no input is given to the map chooser function it returns a default image file name | ||
6/ When a bad input is given to the map chooser function, it returns a default image file name | 6/ When a bad input is given to the map chooser function, it returns a default image file name | ||
</syntaxhighlight> | |||
With this done they identified 4/ as a requirement to write the test for. So created mapChooser. | |||
<syntaxhighlight lang="js"> | |||
import mapChooser from '../mapChooser'; | |||
describe("mapChooser", function(){ | |||
it("returns an image based on input given to it", function() { | |||
let expected = "portland.jpg"; | |||
let actual = mapChooser("portland"); | |||
expect(actual).toEqual(expected); | |||
}); | |||
it("returns an default image when no input is given", function() { | |||
let expected = "default.jpg"; | |||
let actual = mapChooser(""); | |||
expect(actual).toEqual(expected); | |||
}); | |||
}); | |||
</syntaxhighlight> | |||
And finally the code. | |||
<syntaxhighlight lang="js"> | |||
function mapChooser(locationName){ | |||
if (!locationName) { | |||
locationName = "default"; | |||
} | |||
let imageName = locationName + ".jpg"; | |||
return (imageName); | |||
} | |||
export default mapChooser; | |||
</syntaxhighlight> | |||
This took several iterations where the simplest implementation was to return "portland". | |||
=Need for React= | |||
==Imperative vs. Declarative== | |||
For a treasure map <br /><br /> | |||
'''Imperative''' Is the process to achieve something, Start at the dot, Walk to the Lake, Turn right and continue for 5km, Turn left and walk 2 km. | |||
<br> | |||
'''Declarative''' Is what it is. Go to X | |||
<br> | |||
==Virtual DOM== | |||
React has two virtual DOMs. A copy of the actual DOM and the new DOM. React calculates what needs to change and applies it appropriately. | |||
==React Development Process== | |||
Here is how to approach React development | |||
*Create a component hierarchy | |||
*Build a Static Version | |||
*Identify the minimal UI State | |||
*Identify where the state should live | |||
*Add Inverse Data Flow (Passing State upwards) | |||
For lot of information on this go to https://reactjs.org/docs/thinking-in-react.html<br> | |||
<br> | |||
'''Remember''' One-way data flow. | |||
*We don't modify props passed to components | |||
*Modify data via functions passed to the component as props | |||
==Build up the UI== | |||
They then focused on the app UI todo list | |||
<syntaxhighlight lang="txt"> | |||
There should be a button for each store location | |||
Each store button should display the name of the location | |||
There should be a map | |||
There should be a header above the buttons | |||
Clicking a button passes a value to a function | |||
</syntaxhighlight> | |||
The components tests were created using enzyme. This is the first example of enzyme at the time of this page and demonstrates how to check if the UI renders. | |||
<syntaxhighlight lang="js"> | |||
import React from 'react'; | |||
import {shallow} from 'enzyme'; | |||
import StoreLocator from '../StoreLocator'; | |||
describe("StoreLocator", function () { | |||
let mountedStoreLocator; | |||
beforeEach(() => { | |||
mountedStoreLocator = shallow(<StoreLocator />); | |||
}); | |||
it('renders without crashing', () => { | |||
let mountedStoreLocator = shallow(<StoreLocator />); | |||
}); | |||
it('renders a header', () => { | |||
const headers = mountedStoreLocator.find('Header'); | |||
expect(headers.length).toBe(1); | |||
}); | |||
it('renders a map', () => { | |||
const maps = mountedStoreLocator.find('Map'); | |||
expect(maps.length).toBe(1); | |||
}) | |||
}); | |||
describe('chooseMap', ()=> { | |||
it('updates this.state.currentMap using the location passed to it', ()=>{ | |||
let mountedStoreLocator = shallow(<StoreLocator />); | |||
let mockEvent = {target:{value:'testland'}}; | |||
mountedStoreLocator.instance().chooseMap(mockEvent); | |||
expect(mountedStoreLocator.instance().state.currentMap).toBe('testland.png'); | |||
}) | |||
}); | |||
</syntaxhighlight> | |||
=Refactoring with React= | |||
==Minimal State== | |||
On the React page above but to think about state consider. | |||
*Is it not passed from the parent | |||
*Does it change over time | |||
*Is it not possible to compute based on other state of props | |||
==Inverse Date Flow== | |||
Events for a react project flow up (inverse). We use callbacks to update State in a React Application as React uses one-way data binding. | |||
React uses one-way data binding. | |||
==Events== | |||
The course should how to test events were omitted when using enzyme on the button and mocking the event in jest. | |||
<syntaxhighlight lang="js"> | |||
import React from 'react'; | |||
import {shallow} from 'enzyme'; | |||
import Button from '../Button'; | |||
describe("Button",function() { | |||
let mountedButton; | |||
beforeEach(()=>{ | |||
mountedButton = shallow(<Button />); | |||
}); | |||
it('renders without crashing', () => { | |||
let mountedButton = shallow(<Button />); | |||
}); | |||
it('renders a button', () => { | |||
const button = mountedButton.find('button'); | |||
expect(button.length).toBe(1); | |||
}); | |||
it('call a function passed to it when clicked', ()=>{ | |||
const mockCallBack = jest.fn(); | |||
const mountedButtonWithCallback = shallow(<Button handleClick={mockCallBack} />); | |||
mountedButtonWithCallback.find('button').simulate('click'); | |||
expect(mockCallBack.mock.calls.length).toEqual(1); | |||
}); | |||
}); | |||
</syntaxhighlight> | |||
=Testing React Components= | |||
==Lifecyle Methods== | |||
We nice to put the hooks in at this point but hey. | |||
*Mount/Unmount | |||
**Constructor | |||
**Render | |||
**ComponentDidMount | |||
**CompointWillUnmount | |||
*Updating | |||
**ShouldComponentUpdate | |||
**Render | |||
**ComponentDidUpdate | |||
The ComponentDidMount is the first place where it is safe to modify the component and is where you would put network requests. | |||
==Mocking an Network Request (Axios)== | |||
===The Code=== | |||
Here is the code we are trying to test which is an Axios call inside component. | |||
<syntaxhighlight lang="js"> | |||
import React, {Component} from 'react'; | |||
import Header from '../components/Header'; | |||
import Button from '../components/Button'; | |||
import Map from '../components/Map'; | |||
import mapChooser from '../mapChooser'; | |||
import axios from 'axios'; | |||
class StoreLocator extends Component { | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
currentMap: 'none.png', | |||
shops: [] | |||
}; | |||
this.chooseMap = this.chooseMap.bind(this); | |||
} | |||
async componentDidMount(){ | |||
let response = await axios.get('http://localhost:3000/data/shops.json'); | |||
this.setState({ | |||
shops:response.data.shops | |||
}) | |||
} | |||
... | |||
</syntaxhighlight> | |||
===The Mock of Axios=== | |||
This is an example of how to mock Axios with Jest. Make sure you put this in the appropriate place in a __mocks__ folder. | |||
<syntaxhighlight lang="js"> | |||
const axiosMock = jest.genMockFromModule('axios'); | |||
let mockResponse = { | |||
data: {"shops": | |||
[{ | |||
"location": "test location", | |||
"address" : "test address" | |||
}] | |||
} | |||
}; | |||
axiosMock.get.mockImplementation(req); | |||
function req() { | |||
return new Promise(function(resolve) { | |||
axiosMock.delayTimer = setTimeout(function() { | |||
resolve(mockResponse); | |||
}, 100); | |||
}); | |||
} | |||
module.exports = axiosMock; | |||
</syntaxhighlight> | |||
===The Test Case=== | |||
This is the usage of the mock above. An example of async function using mocks. | |||
<syntaxhighlight lang="js"> | |||
describe("StoreLocator", function () { | |||
let mountedStoreLocator; | |||
beforeEach(() => { | |||
mountedStoreLocator = shallow(<StoreLocator />); | |||
}); | |||
it('calls axios.get in #componentDidMount', ()=>{ | |||
return mountedStoreLocator.instance().componentDidMount().then(()=>{ | |||
expect(axios.get).toHaveBeenCalled(); | |||
}) | |||
}); | |||
it('calls axios.get with correct url', ()=>{ | |||
return mountedStoreLocator.instance().componentDidMount().then(()=>{ | |||
expect(axios.get).toHaveBeenCalledWith('http://localhost:3000/data/shops.json'); | |||
}) | |||
}); | |||
it('updates state with api data', () => { | |||
return mountedStoreLocator.instance().componentDidMount().then(()=> { | |||
expect(mountedStoreLocator.state()).toHaveProperty('shops', | |||
[{ | |||
"location": "test location", | |||
"address" : "test address" | |||
}] | |||
); | |||
}) | |||
}); | |||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 00:53, 12 July 2021
Introduction
Requirements
On the course we got requirements
- Display map
- Select shop
- Marker show location
- User can search
- Detect user location
- Get Directions
- Scalable
Prototype
A prototype was developed on paper which created a web page with buttons for the first two sites.
Introducing TDD
TDD follows the three processes. Test, Code, Refactor. The module demonstrated how to start building the App by create sections of the App to components. E.g. Component Header. They split the containers and components where components were stateless. Tests were written to make sure the components rendered.
Need for TDD
With TDD you write the test and then the supporting code. It walked through what to test which I liked as a list known but nice to be reminded of.
- Does it render
- Does it render correctly
- Does it handle events correctly
- Do conditionals work
- Are edge cases handled.
Next they broken down ideas for the prototype requirements above.
1/ There should be a button for each store location
2/ Each store button should display the name of the location
3/ Clicking the button passes the value to the function
4/ A map chooser function returns an image file name based on input given to it
5/ When no input is given to the map chooser function it returns a default image file name
6/ When a bad input is given to the map chooser function, it returns a default image file name
With this done they identified 4/ as a requirement to write the test for. So created mapChooser.
import mapChooser from '../mapChooser';
describe("mapChooser", function(){
it("returns an image based on input given to it", function() {
let expected = "portland.jpg";
let actual = mapChooser("portland");
expect(actual).toEqual(expected);
});
it("returns an default image when no input is given", function() {
let expected = "default.jpg";
let actual = mapChooser("");
expect(actual).toEqual(expected);
});
});
And finally the code.
function mapChooser(locationName){
if (!locationName) {
locationName = "default";
}
let imageName = locationName + ".jpg";
return (imageName);
}
export default mapChooser;
This took several iterations where the simplest implementation was to return "portland".
Need for React
Imperative vs. Declarative
For a treasure map
Imperative Is the process to achieve something, Start at the dot, Walk to the Lake, Turn right and continue for 5km, Turn left and walk 2 km.
Declarative Is what it is. Go to X
Virtual DOM
React has two virtual DOMs. A copy of the actual DOM and the new DOM. React calculates what needs to change and applies it appropriately.
React Development Process
Here is how to approach React development
- Create a component hierarchy
- Build a Static Version
- Identify the minimal UI State
- Identify where the state should live
- Add Inverse Data Flow (Passing State upwards)
For lot of information on this go to https://reactjs.org/docs/thinking-in-react.html
Remember One-way data flow.
- We don't modify props passed to components
- Modify data via functions passed to the component as props
Build up the UI
They then focused on the app UI todo list
There should be a button for each store location
Each store button should display the name of the location
There should be a map
There should be a header above the buttons
Clicking a button passes a value to a function
The components tests were created using enzyme. This is the first example of enzyme at the time of this page and demonstrates how to check if the UI renders.
import React from 'react';
import {shallow} from 'enzyme';
import StoreLocator from '../StoreLocator';
describe("StoreLocator", function () {
let mountedStoreLocator;
beforeEach(() => {
mountedStoreLocator = shallow(<StoreLocator />);
});
it('renders without crashing', () => {
let mountedStoreLocator = shallow(<StoreLocator />);
});
it('renders a header', () => {
const headers = mountedStoreLocator.find('Header');
expect(headers.length).toBe(1);
});
it('renders a map', () => {
const maps = mountedStoreLocator.find('Map');
expect(maps.length).toBe(1);
})
});
describe('chooseMap', ()=> {
it('updates this.state.currentMap using the location passed to it', ()=>{
let mountedStoreLocator = shallow(<StoreLocator />);
let mockEvent = {target:{value:'testland'}};
mountedStoreLocator.instance().chooseMap(mockEvent);
expect(mountedStoreLocator.instance().state.currentMap).toBe('testland.png');
})
});
Refactoring with React
Minimal State
On the React page above but to think about state consider.
- Is it not passed from the parent
- Does it change over time
- Is it not possible to compute based on other state of props
Inverse Date Flow
Events for a react project flow up (inverse). We use callbacks to update State in a React Application as React uses one-way data binding. React uses one-way data binding.
Events
The course should how to test events were omitted when using enzyme on the button and mocking the event in jest.
import React from 'react';
import {shallow} from 'enzyme';
import Button from '../Button';
describe("Button",function() {
let mountedButton;
beforeEach(()=>{
mountedButton = shallow(<Button />);
});
it('renders without crashing', () => {
let mountedButton = shallow(<Button />);
});
it('renders a button', () => {
const button = mountedButton.find('button');
expect(button.length).toBe(1);
});
it('call a function passed to it when clicked', ()=>{
const mockCallBack = jest.fn();
const mountedButtonWithCallback = shallow(<Button handleClick={mockCallBack} />);
mountedButtonWithCallback.find('button').simulate('click');
expect(mockCallBack.mock.calls.length).toEqual(1);
});
});
Testing React Components
Lifecyle Methods
We nice to put the hooks in at this point but hey.
- Mount/Unmount
- Constructor
- Render
- ComponentDidMount
- CompointWillUnmount
- Updating
- ShouldComponentUpdate
- Render
- ComponentDidUpdate
The ComponentDidMount is the first place where it is safe to modify the component and is where you would put network requests.
Mocking an Network Request (Axios)
The Code
Here is the code we are trying to test which is an Axios call inside component.
import React, {Component} from 'react';
import Header from '../components/Header';
import Button from '../components/Button';
import Map from '../components/Map';
import mapChooser from '../mapChooser';
import axios from 'axios';
class StoreLocator extends Component {
constructor(props) {
super(props);
this.state = {
currentMap: 'none.png',
shops: []
};
this.chooseMap = this.chooseMap.bind(this);
}
async componentDidMount(){
let response = await axios.get('http://localhost:3000/data/shops.json');
this.setState({
shops:response.data.shops
})
}
...
The Mock of Axios
This is an example of how to mock Axios with Jest. Make sure you put this in the appropriate place in a __mocks__ folder.
const axiosMock = jest.genMockFromModule('axios');
let mockResponse = {
data: {"shops":
[{
"location": "test location",
"address" : "test address"
}]
}
};
axiosMock.get.mockImplementation(req);
function req() {
return new Promise(function(resolve) {
axiosMock.delayTimer = setTimeout(function() {
resolve(mockResponse);
}, 100);
});
}
module.exports = axiosMock;
The Test Case
This is the usage of the mock above. An example of async function using mocks.
describe("StoreLocator", function () {
let mountedStoreLocator;
beforeEach(() => {
mountedStoreLocator = shallow(<StoreLocator />);
});
it('calls axios.get in #componentDidMount', ()=>{
return mountedStoreLocator.instance().componentDidMount().then(()=>{
expect(axios.get).toHaveBeenCalled();
})
});
it('calls axios.get with correct url', ()=>{
return mountedStoreLocator.instance().componentDidMount().then(()=>{
expect(axios.get).toHaveBeenCalledWith('http://localhost:3000/data/shops.json');
})
});
it('updates state with api data', () => {
return mountedStoreLocator.instance().componentDidMount().then(()=> {
expect(mountedStoreLocator.state()).toHaveProperty('shops',
[{
"location": "test location",
"address" : "test address"
}]
);
})
});