ex) 컴포넌트가 지니고 있는 props, state를 확인하고, 컴포넌트의 내장 메서드를 직저 호출하기도 한다.
1. CRA 생성
$ yarn create react-app react-enzyme-test
# 혹은 npx create-react-app react-enzyme-test
2. 라이브러리 설치
$ yarn add enzyme enzyme-adapter-react-16
# 또는 npm install --save enzyme enzyme-adapter-react-16
3. src/setupTest.js에 다음과 같은 코드 추가
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-17';
configure({ adapter: new Adapter() });
4. src/Profile.js에는 다음과 같은 코드를 작성
import React from 'react';
const Profile = ({ username, name }) => {
return (
<div>
<b>{username}</b>
<span>({name})</span>
</div>
);
};
export default Profile;
5. app.js에서 Profile컴포넌트를 렌더링
import React from 'react';
import Profile from './Profile';
function App() {
return (
<div>
<Profile username="houya" name="박정호" />
</div>
);
}
export default App;
1. Enzyme에서 스냅샷 테스팅을 하기 위해 enzyme-to-json 이라는 라이브러리를 설치
$ yarn add enzyme-to-json
2.package.json에는 'jest'설정 추가
{
...
},
"jest": {
"snapshotSerializers": ["enzyme-to-json/serializer"]
}
}
3. Profile.test.js에 다음과 같은 코드 추가
import React from 'react';
import { mount } from 'enzyme';
import Profile from './Profile';
describe('<Profile />', () => {
it('matches snapshot', () => {
const wrapper = mount(<Profile username="velopert" name="김민준" />);
expect(wrapper).toMatchSnapshot();
});
});
4. yarn test를 하게 되면 test가 성곡적으로 pass되는 것을 확인 할 수 있고, src에 __snapshots__/Profile.test.js.snap/ 라는 파일이 생성
-> 말 그대로 코드의 스냅샷이 찍혀서 저장되었고, 이제 이 코드와 다른 형식의 코드가 test되면 에러를 발생시킨다.
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Profile /> matches snapshot 1`] = `
<Profile
name="박정호"
username="houya"
>
<div>
<b>
houya
</b>
<span>
(
박정호
)
</span>
</div>
</Profile>
`;
5. 만약 Profile.js에 다음과 같이 !를 추가할 경우 스냅샷의 코드와 다르므로 에러가 난다.
-> 만약 코드를 변경하고 스냅샷의 업데이트를 원한다면 콘솔창에 u를 입력하면 스냅샷에도 !가 찍힌 상태로 변경되고 pass가 출력된다.
import React from 'react';
const Profile = ({ username, name }) => {
return (
<div>
<b>{username}!</b>
<span>({name})</span>
</div>
);
};
export default Profile
import React from 'react';
import { mount } from 'enzyme';
import Profile from './Profile';
describe('<Profile />', () => {
it('matches snapshot', () => {
const wrapper = mount(<Profile username="houya" name="박정호" />);
expect(wrapper).toMatchSnapshot();
});
it('renders username and name', () => {
const wrapper = mount(<Profile username="houya" name="박정호" />);
expect(wrapper.props().username).toBe('houya');
expect(wrapper.props().name).toBe('박정호');
});
});
import React from 'react';
import { mount } from 'enzyme';
import Profile from './Profile';
describe('<Profile />', () => {
...
it('renders username and name', () => {
const wrapper = mount(<Profile username="houya" name="박정호" />);
...
const boldElement = wrapper.find('b');
expect(boldElement.contains('houya')).toBe(true);
const spanElement = wrapper.find('span');
expect(spanElement.text()).toBe('(박정호)');
});
});
1. 다음과 같은 코드가 있다.
<src/Counter.js>
import React, { Component } from 'react';
class Counter extends Component {
state = {
number: 0
};
handleIncrease = () => {
this.setState({
number: this.state.number + 1
});
};
handleDecrease = () => {
this.setState({
number: this.state.number - 1
});
};
render() {
return (
<div>
<h2>{this.state.number}</h2>
<button onClick={this.handleIncrease}>+1</button>
<button onClick={this.handleDecrease}>-1</button>
</div>
);
}
}
export default Counter;
2. test 파일에는 mount 대신 shallow라는 함수를 사용한다.
-> shallow는 컴포넌트 내부에 또다른 리액트 컴포넌트가 있다면 이를 렌더링 하지 않는다.
만약 Counter내에 다른 컴포넌트인 Profile컴포넌트가 존재한다.
<src/Counter.js>
import React, { Component } from 'react';
class Counter extends Component {
...
render() {
return (
<div>
...
<Profile></Profile>
</div>
);
}
}
export default Counter;
shallow함수를 사용할 경우 스냅샷
-> shallow에서의 최상위 요소는 div
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Counter /> matches snapshot 1`] = `
<div>
<h2>
0
</h2>
<button
onClick={[Function]}
>
+1
</button>
<button
onClick={[Function]}
>
-1
</button>
<Profile
name="김민준"
username="velopert"
/>
</div>
`;
mount함수를 사용할 경우 스냅샷
-> Profile의 내부 내용까지 전부 렌더링
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Counter /> matches snapshot 1`] = `
<Counter>
<div>
<h2>
0
</h2>
<button
onClick={[Function]}
>
+1
</button>
<button
onClick={[Function]}
>
-1
</button>
<Profile
name="houya"
username="velopert"
>
<div>
<b>
velopert
</b>
<span>
(
박정호
)
</span>
</div>
</Profile>
</div>
</Counter>
`;
3. 컴포넌트 안에 내장 메서드를 호출할 때는 instance()함수를 호출하여 인스턴스를 조회 후 메서드 호출
wrapper.instance().handleIncrease();
4. 컴포넌트의 state를 조회할 때는 state()함수 사용
expect(wrapper.state().number).toBe(0);
1. 버튼 클릭 이벤트를 시뮬레이트하여 기능이 잘 작동하는지 확인
<counter.test.js>
import React from 'react';
import { shallow } from 'enzyme';
import Counter from './Counter';
describe('<Counter />', () => {
it('matches snapshot', () => {
...
});
it('has initial number', () => {
...
});
it('increases', () => {
...
});
it('decreases', () => {
...
});
it('calls handleIncrease', () => {
// 클릭이벤트를 시뮬레이트하고, state 를 확인
const wrapper = shallow(<Counter />);
const plusButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '+1'
);
plusButton.simulate('click');
expect(wrapper.state().number).toBe(1);
});
it('calls handleDecrease', () => {
// 클릭 이벤트를 시뮬레이트하고, h2 태그의 텍스트 확인
const wrapper = shallow(<Counter />);
const minusButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '-1'
);
minusButton.simulate('click');
const number = wrapper.find('h2');
expect(number.text()).toBe('-1');
});
});
2. findWhere()함수를 이용하여 버튼 태그를 선택
<findWhere()함수 사용>
const plusButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '+1'
);
const minusButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '-1'
);
<find()함수 사용>
const buttons = wrapper.find('button');
const plusButton = buttons.get(0); // 첫번째 버튼 +1
const minusButton = buttons.get(1); // 두번째 버튼 -1
3. onClick한 것이 대한 변화에 대해서 알기
-> 버튼에 이벤트를 시뮬레이트할 때에는 원하는 엘리먼트를 찾아 simulate()함수 사용
plusButton.simulate('click');
minusButton.simulate('click');
-> simulate()함수의 첫번째 파라미터에는 이벤트 이름, 두번째 파라미터에는 이벤트 객체를 넣을 수 있다. 만약 onChange를 사용해 입력되는 내용을 확인하기 위해 다음과 같이 작성
input.simulate('change', {
target: {
value: 'hello world'
}
});
1. HookCounter.js 와 App.js 작성
<HookCounter.js>
import React, { useState, useCallback } from 'react';
const HookCounter = () => {
const [number, setNumber] = useState(0);
const onIncrease = useCallback(() => {
setNumber(number + 1);
}, [number]);
const onDecrease = useCallback(() => {
setNumber(number - 1);
}, [number]);
return (
<div>
<h2>{number}</h2>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
};
export default HookCounter;
<app.js>
import React from 'react';
import HookCounter from './HookCounter';
function App() {
return (
<div>
<HookCounter />
</div>
);
}
export default App;
<HookCounter.test.js>
import React from 'react';
import { mount } from 'enzyme';
import HookCounter from './HookCounter';
describe('<HookCounter />', () => {
it('matches snapshot', () => {
const wrapper = mount(<HookCounter />);
expect(wrapper).toMatchSnapshot();
});
it('increases', () => {
const wrapper = mount(<HookCounter />);
let plusButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '+1'
);
plusButton.simulate('click');
plusButton.simulate('click');
const number = wrapper.find('h2');
expect(number.text()).toBe('2');
});
it('decreases', () => {
const wrapper = mount(<HookCounter />);
let decreaseButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '-1'
);
decreaseButton.simulate('click');
decreaseButton.simulate('click');
const number = wrapper.find('h2');
expect(number.text()).toBe('-2');
});
});
참조: https://learn-react-test.vlpt.us/#/02-tdd-introduction
https://velog.io/@citron03/React-18%EC%97%90%EC%84%9C-ReactDOM.render%EC%99%80-createRoot