Next.js 와 Redux 테스트(Jest, Enzyme)를 해보자.
## 추가 2019.08.14
Enzyme 3.10.0 에서 컴포넌트 스냅샷이 안되는 경우가 발생하는데, shallow 혹은 mount한 컴포넌트에 .debug()를 추가해주면 정상적으로 스냅샷 된다.
이전 포스팅에서는 React의 컴포넌트들을 테스트 했다.
NextJS 역시 테스팅에서의 방법은 다를 바 없는데, 대신 테스팅을 위해 몇가지 설정을 해줘야 하는 것이 있다.
또한 Redux 테스트에 대해서 알아보도록 하자.
Next 테스트 보일러 플레이트
npx create-next-app --example with-jest project-name
사실 다음과 같이 설치 해주면 귀찮은 설정들이 모두 해결 된다.
손으로 설정하더라도 다음 프로젝트의 구조를 거희 따라가기 때문에, 만들어진 프로젝트 구조를 살펴보기만 하자.
1. jest 설정
사실상 CommonJS 문법만 잘 따르면, 별 설정이 필요 없다.
2. react-test-renderer
본격적으로 컴포넌트를 가져 오기 위해서 문제가 되는 부분이 있다.
React를 CommonJS 형식으로 export 하지 않은 이상은 require()로는 가져올 수 없다.
고로 import문을 사용하기 위해 바벨을 사용해야만 한다.
공식문서에는 다음과 같은 도구를 설치하라고 한다.
babel-jest 와 @babel/core가 바벨 설정파일을 위해 작동한다.
presets 같은 경우에는 next에서 자체적으로 제공하는 presets 을이용하면 대체로 다 동작하는것 같다.
{
"presets": ["next/babel"]
}
< .babelrc >
설정해주고 테스트 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
index.test.js
import React from "react";
import renderer from "react-test-renderer";
import App from "../pages/index.js";
describe("With Snapshot Testing", () => {
it('App shows "Hello world!"', () => {
const component = renderer.create(<App />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
index.js
export default () => (
<div>
<style jsx>{`
p {
color: red;
}
`}</style>
<p>Hello World!</p>
</div>
)
|
3. Enzyme 사용
React와 다른 점은 환경설정을 직접 세팅해주어야 한다는 점이다.
module.exports = {
setupFiles: ['<rootDir>/jest.setup.js'],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/']
}
< jest.config.js >
import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
configure({ adapter: new Adapter() })
< jest.setup.js >
이 때 config의 이름과 셋팅한 setup의 이름에 유의해야 한다.
testPathIgnorePatterns 옵션은 test시 watch 하지 않을 디렉토리의 목록이다.
1
2
3
4
5
6
7
8
|
describe("With Enzyme", () => {
it('App shows "Hello world!"', () => {
const app = shallow(<App />);
expect(app.find("p").text()).toEqual("Hello World!");
});
});
|
제대로 설정되었다면 정상적으로 작동 할 것이다.
컴포넌트 테스트
이전 React에서는 컴포넌트 테스트를 할 때 스냅 샷을 이용했는데, 개인적으로 폴더구조도 복잡해지고 코드가 직관적이지 못하다는 느낌을 받아서 간단하게 해보고자 한다.
스냅샷을 해야 겠다면 이전 포스팅을 확인하길 바란다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { shallow } from "enzyme"
import Header from "./index"
describe("헤더 컴포넌트", () => {
it("에러 없이 헤더 컴포넌트를 정상 렌더링 합니다.", () => {
const component = shallow(<Header />)
const wrapper = component.find(".headerComponent")
expect(wrapper.length).toBe(1)
})
it("에러 없이 헤더의 제목을 정상 렌더링 합니다.", () => {
const component = shallow(<Header />)
const wrapper = component.find(".headerText")
expect(wrapper.length).toBe(1)
})
it("에러 없이 헤더의 로고를 정상 렌더링 합니다.", () => {
const component = shallow(<Header />)
const wrapper = component.find(".headerLogo")
expect(wrapper.length).toBe(1)
})
})
|
간단하게 컴포넌트를 찾아 length로 확인하였다.
다만 반복적인 코드가 많아 다음과 같이 리팩토링 해볼 수 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import { shallow } from "enzyme"
import Header from "./index"
import { findByTestAtrribute } from "../../text_utils/index"
const setUp = (props = {}) => {
const component = shallow(<Header {...props} />)
return component
}
describe("헤더 컴포넌트", () => {
let component
beforeEach(() => {
component = setUp()
})
it("에러 없이 헤더 컴포넌트를 정상 렌더링 합니다.", () => {
const wrapper = findByTestAtrribute(component, "headerComponent")
expect(wrapper.length).toBe(1)
})
it("에러 없이 헤더의 제목을 정상 렌더링 합니다.", () => {
const wrapper = findByTestAtrribute(component, "headerText")
expect(wrapper.length).toBe(1)
})
it("에러 없이 헤더의 로고를 정상 렌더링 합니다.", () => {
const wrapper = findByTestAtrribute(component, "headerLogo")
expect(wrapper.length).toBe(1)
})
})
|
첫번째로 12번째 줄의 component이다.
매 케이스마다 실행 되므로 beforeEach에 component를 매번 생성해준다.
두번째로 setUp 메소드인데,
component에 속성을 자유자재로 넣어 가며 테스팅 하기 쉽게 만들어져 있다.
마지막으로 findByTestAtrribute메소드이다.
이것은 원본파일을 볼 필요가 있다.
1
2
3
4
5
6
7
|
<header data-test="headerComponent">
<div data-test="headerText">Header</div>
<img
data-test="headerLogo"
src="https://t3.daumcdn.net/thumb/R720x0/?fname=http://t1.daumcdn.net/brunch/service/user/Aay/image/t5G9QrwYFAOD3fif7IQAa6D0Ppg"
/>
</header>
|
header 컴포넌트의 내용이다.
이전에는 className으로 데이터를 찾았지만, 이렇게 될 경우에 className에 종속되는것이 늘어나므로
data-test라는 속성을 대신 부여했다.
1
2
3
4
5
|
export const findByTestAtrribute = (component, attr) => {
const wrapper = component.find(`[data-test="${attr}"]`)
return wrapper
}
|
finByTestAtrribute는 wrapper를 리턴한다. 이 때 대상 컴포넌트와 data-test의 속성값을 통해 찾는다.
컴포넌트 Props 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
import React from "react"
import { shallow } from "enzyme"
import Headline from "./index"
import { findByTestAtrribute } from "../../test_utils/index"
const setUp = (props = {}) => {
const component = shallow(<Headline {...props} />)
return component
}
describe("헤드라인 컴포넌트", () => {
describe("속성을 받는 컴포넌트.", () => {
let wrapper
beforeEach(() => {
const props = {
header: "Test Header",
description: "Test Description"
}
wrapper = setUp(props)
})
it("에러 없이 컴포넌트가 렌더링 됩니다.", () => {
const component = findByTestAtrribute(wrapper, "HeadlineComponent")
expect(component.length).toBe(1)
})
it("에러 없이 헤더가 렌더링 됩니다.", () => {
const header = findByTestAtrribute(wrapper, "header")
expect(header.length).toBe(1)
expect(header.text()).toEqual("Test Header")
})
it("에러 없이 세부사항이 렌더링 됩니다.", () => {
const description = findByTestAtrribute(wrapper, "description")
expect(description.length).toBe(1)
expect(description.text()).toEqual("Test Description")
})
})
describe("속성을 받지 않는 컴포넌트", () => {
let wrapper
beforeEach(() => {
wrapper = setUp()
})
it("컴포넌트 렌더링에 실패합니다.", () => {
const component = findByTestAtrribute(wrapper, "HeadlineComponent")
expect(component.length).toBe(0)
})
})
})
|
위와 별 다를 건 없지만
속성 값을 넣는 방법
그리고 속성 값을 통해서 렌더링 된 값을 .text()로 비교하는 법에 대해서 봐두면 좋겠다.
속성을 받지 않았을 떄는 null을 반환하게 되어 있기 때문에 component의 길이가 0이다.
PropsType 테스트
개인적으로는 PropsType까지 테스트 해줘야 하나 싶지만 혹시 모르니까 정리해보도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
|
const App = ({ Component }) => {
const dummy = [
{ fName: "김", lName: "첨지", age: 24, onlineStatus: true },
{ fName: "지", lName: "점토", age: 28, onlineStatus: false }
]
return (
<div>
<div>
<Header />
<section className="main">
<Headline user={dummy} header="Header" description="Click the button to render posts" />
|
Headline에선 다음과 같이 세가지 prop를 받는다.
props테스트 하기 위해서는 "check-prop-types" 라는 패키지가 추가적으로 필요하다.
1
2
3
4
5
6
7
|
import checkPropTypes from "check-prop-types"
export const checkProps = (component, expectedProps) => {
const propsErr = checkPropTypes(component.propTypes, expectedProps, "props", component.name)
return propsErr
}
|
패키지를 그냥 쓰기에는 checkPropTypes에 세번째와 네번째 인자는 항상 중복된다.
그렇기 때문에 다음과 같이 함수로 만들어 사용하면 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
describe("PropTypes 체크", () => {
it("에러 없음", () => {
const expectedProps = {
header: "Test Header",
description: "Test Description",
user: [
{
fName: "Test fName",
lName: "Test lName",
age: 24,
onlineStatus: false
}
]
}
const propsErr = checkProps(Headline, expectedProps)
expect(propsErr).toBeUndefined()
})
})
|
checkPropTypes는 에러가 있을시 에러를 반환하고 아닐 경우에는 아무것도 반환하지 않는 특성을 이용하여
.toBeUndefined()를 이용했다.
Reducer 테스트
간단하게 포스트를 불러오는 Reducer를 만들었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { createAction, handleActions } from "redux-actions";
export const initialState = {
posts: []
};
export const GET_POSTS = "GET_POSTS";
export const getPostsAction = createAction(GET_POSTS, payload => payload);
export default handleActions(
{
[GET_POSTS]: (state, action) => {
return {
...state,
posts: action.payload
};
}
},
initialState
);
|
이에 대한 테스트 코드는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import { GET_POSTS, getPostsAction } from "./reducer";
import postReducer from "./reducer";
describe("Posts Reducer", () => {
it("default state가 리턴된다.", () => {
const newState = postReducer(undefined, {});
expect(newState).toEqual({
posts: []
});
});
it("props 값에 따라 새로운 state가 리턴된다.", () => {
const posts = [
{ title: "Test1 " },
{ title: "Test2 " },
{ title: "Test3 " }
];
const newState = postReducer(undefined, getPostsAction(posts));
expect(newState).toEqual({
posts: posts
});
});
});
|
핵심은 Reducer를 직접 다루어 비교 한다는 점인거 같다.
원하는 state값과 action값을 직접 넣어 리턴할 결과값을 toEqual로 직접 비교한다.
첫번째 경우에는 어떠한 액션도 발생되지 않은 초기 state값을,
두번째 경우에는 액션 발생 후에 posts값의 리턴값을 각각 테스트 한다.
Redux를 적용한 컴포넌트 테스트
현재 컴포넌트에 Redux를 적용할 때, redux hooks 를 사용한다면 문제가 된다.
그렇기 때문에 이전과 같은 테스트 포맷으로 사용하려면 mapState를 이용해야 한다.
redux hooks를 사용하려면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
const ShareButton = props => {
const dispatch = useDispatch();
const getPostsRequest = () => {
dispatch(getPostsAction());
};
const { buttonText } = props;
return (
<div>
<button onClick={getPostsRequest} data-test="buttonComponent">
{buttonText}
</button>
</div>
);
};
export default ShareButton;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
describe("ShareButton 컴포넌트", () => {
describe("에러 없이 렌더링한다", () => {
let wrapper;
beforeEach(() => {
const props = {
buttonText: "Example Button Text"
};
const store = configureStore();
wrapper = mount(
<Provider store={store}>
<ShareButton {...props} />
</Provider>
);
});
it("Should Render a Button", () => {
const button = findByTestAtrribute(wrapper, "buttonComponent");
expect(button.length).toBe(1);
});
});
});
|
첫번째로 shallow 대신 mount를 사용하였다.
mount 는 shallow와 다르게 현재 컴포넌트가 다른 컴포넌트와 연결되어 있다면 깊이 만큼 컴포넌트를 가져온다.
두번째로 Provider로 감싸주었다.
아마 감싸지 않았다면 Provider로 감싸라는 에러가 뜰 것이다. 이 때, store를 담아 주어야 하는데
redux와 연결해주는 index 컴포넌트에 store 리턴 함수를 export 하여 사용해준다.
이 때, store에 전달 받지 못하는 몇몇 파라미터들은 예외처리 해주자.
Redux-saga 와 비동기 테스트
https://medium.com/@ktajpuri/easiest-way-to-test-asynchronous-redux-sagas-with-jest-f47c821393da
해당 테스트는 다음 예제를 참고 했습니다.
아직 saga테스트 중에서 현제 프로젝트에 적합한 마땅히 납득이 가는 것도, 잘 작동하는것도 못 찾고 있다.
위의 테스트가 그나마 작동을 하는데, 살펴보자.
1
2
3
4
5
6
7
8
9
10
|
it("saga 테스트.", () => {
const iterator = addPost();
expect(iterator.next().value).toEqual(call(addPostAPI));
expect(iterator.next().value).toEqual(
put({
type: GET_POSTS_SUCCESS,
payload: undefined
})
);
});
|
핵심은 제너레이터의 특징을 이용한 것이다.
이떄 next()의 반환 값으로 yield 에 어떤 함수나 변수가 걸려있는지, 인자는 무엇인지 등의 정보를 리턴한다.
이를 직접 메소드와 toEqual과 비교한다.
주의해야 할 점은
두번째 expect인데, payload가 undefined 이다.
iterator의 두번째 next()에서 payload로 값을 넘겨줄 방법이 없기 때문인데, 위의 테스트 예제에서는
Error로 가지 않고 정상적으로 SUCCESS를 실행하는지에 대해서만 따지면 되기 때문이라고 한다.
Redux Redux-saga 데이터 테스트
이제 REQUEST를 실행하면, 사가를 통해 SUCCESS가 실행하는 것을 증명 했으므로, 데이터만 증명 하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
it("요청 결과 값을 정확히 받는다..", async () => {
const expectedDummy = {
posts: [
{
userId: 1,
id: 1,
title:
"sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
body:
"quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
userId: 1,
id: 2,
title: "qui est esse",
body:
"est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
},
{
userId: 1,
id: 3,
title: "ea molestias quasi exercitationem repellat qui ipsa sit aut",
body:
"et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
}
]
};
const requestData = await addPostAPI();
const newState = postReducer(undefined, {
type: GET_POSTS_SUCCESS,
payload: requestData
});
expect(newState).toEqual(expectedDummy);
});
|
cs |
SUCCESS가 실행되는 것이 증명되엇기 때문에
SUCCESS를 action 시키고 결과를 실제 axios로 요청한 값과 비교해보면 된다.
마무리
스냅샷 할 때보다 훨씬 간단하고 일관적으로 테스팅 할 수 있었다.
다만 마지막에 saga 관련 테스트는 reducer와 커플링 되어 있어(saga에서 증명하고 reducer에서 다시 데이터를 테스트 해야하는 것은) 좋은 테스트는 아니라는 생각이 든다.
좀 더 시간이 지나서 다시 saga를 테스트 할 좋은 방법을 찾아보는 것이 필수적이다.