Component Test를 작성하여 훨씬 더 간단하고 유지 관리하기 쉽고 다른 구성 요소와 함께 사용하고 재사용하기 쉬운 UI 구성 요소를 만들 수 있습니다.
이 과정에서 테스트를 작성하고, 테스트를 통과하는 코드를 작성하고, 작성한 코드를 리팩토링하는 과정을 반복하게 됩니다.
이번 글에서는 todo list 코드를 일부 작성해보겠습니다.
1. Setup
Next.js 11.0.1을 사용하고 있습니다.
npm install --save-dev cypress @cypress/webpack-dev-server @cypress/react html-webpack-plugin@4 webpack@4 webpack-dev-server@3
2. Opening Cypress
npx cypress open
해당 명령을 이용하여 cypress test runner를 동작시키면 cypress.json 등을 생성합니다.
3. Configuration
cypress/plugins/index.js
- next.js plugin을 추가합니다.
const injectDevServer = require('@cypress/react/plugins/next');
module.exports = (on, config) => {
injectDevServer(on, config);
return config;
};
cypress.json
- component tests는 components 폴더에 작성하겠습니다.
{
"component": {
"componentFolder": "components",
"testFiles": "**/*spec.{js,jsx,ts,tsx}"
}
}
next.config.js
- next.js의 weback 버전을 4버전으로 낮춥니다. 현재 5버전 사용시 cypress component testing이 정상작동하지 않는 부분이 일부 있습니다.
module.exports = {
webpack5: false,
};
4. Writing Component Test
위의 설정을 모두 끝냈다면 이제 테스트를 작성할 차례입니다.
/index 페이지에 “Welcome to Next.js” 텍스트가 존재하는지 테스트하는 코드를 작성해보겠습니다.
// components/index.spec.jsx
import React from "react";
import { mount } from "@cypress/react";
import IndexPage from "../pages/index";
it("should work", () => {
mount(<IndexPage />);
cy.contains("Welcome to Next.js");
});
component testing mode 실행
npx cypress open-ct
4. How to write
테스트를 진행하려면 테스트 가능한 코드를 작성해야합니다. 테스트 가능한 코드를 작성하기 위해서는 관심사를 분리해야합니다. 다음과 같이 세가지 항목으로 나누어 볼 수 있습니다.
- Display/UI Components
- Side effects (I/O, network, disk, etc.)
- Program logic/business rules
각각을 presenter, container, reducer라고 하겠습니다.
5. Example: Todo List
테스트를 작성하고, 테스트를 통과하는 코드를 작성하고, 작성한 코드를 리팩토링하는 과정을 반복해보겠습니다.
우선 어떤 식으로든 테스트를 통과하는 코드를 작성합니다.
// todo-list.spec.jsx
import React from 'react';
import { mount } from '@cypress/react';
import { TodoList } from './toto-list';
describe('TodoList Component', () => {
const todos = [
{ id: 1, title: '돈코츠 라멘 주문하기', isDone: false },
{ id: 2, title: '교자 주문하기', isDone: false },
{ id: 3, title: '생맥주 주문하기', isDone: false },
];
it('contains correct number of todos', () => {
mount(<TodoList todos={todos} />);
cy.get('[data-cy=todo-list]').children().should('have.length', todos.length);
});
it('contains correct titles', () => {
mount(<TodoList todos={todos} />);
cy.get('[data-cy=todo-list]')
.children()
.each(($el, index) => {
cy.wrap($el).should('contain.text', todos[index].title);
});
});
// todo-list.tsx
import React from "react";
export interface TodoListProps {
todos: any[];
}
export const TodoList = ({ todos }: TodoListProps) => {
return (
<ul data-cy="todo-list">
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
};
테스트를 통과했습니다. 이번에는 todo를 분리하여 별도의 컴포넌트로 만들고 type을 명확하게 수정합니다.
// todo.tsx
import React from "react";
export interface TodoProps {
id: number;
title: string;
isDone: boolean;
}
export const Todo = ({ title, isDone }: TodoProps) => {
const className = isDone ? "done" : "";
const style = isDone
? { color: "lightgray", textDecoration: "line-through" }
: undefined;
return (
<li className={className} style={style} data-cy="todo">
{title}
</li>
);
};
변경된 내용에 맞춰 todo-list 컴포넌트도 수정합니다.
// todo-list.tsx
import React from "react";
import { Todo, TodoProps } from "components/todo";
export interface TodoListProps {
todos: TodoProps[];
}
export const TodoList = ({ todos }: TodoListProps) => {
return (
<ul data-cy="todo-list">
{todos.map((todo) => (
<Todo key={todo.id} {...todo} />
))}
</ul>
);
};
테스트를 진행하면서 계속 사용하게될 정적 데이터는 cypress/fixtures
폴더에서 관리하면 여러 테스트에서 편리하게 이용할 수 있습니다.
// cypress/fixtures/todos.json
[
{ "id": 1, "title": "돈코츠 라멘 주문하기" },
{ "id": 2, "title": "교자 주문하기" },
{ "id": 3, "title": "생맥주 주문하기" }
]
이번에는 todo-list의 로직을 처리할 reducer를 추가하겠습니다.
import { TodoListProps } from "components/todo-list";
export type State = {
todos: TodoListProps["todos"];
};
export type Action = {
type: "READ" | "CREATE" | "UPDATE" | "DELETE";
payload: any;
};
export default function reducer(state: State, action: Action) {
switch (action.type) {
case "READ":
return { ...state, todos: action.payload };
case "CREATE":
return { ...state, todos: [action.payload, ...state.todos] };
case "UPDATE":
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload.id ? action.payload : todo
),
};
case "DELETE":
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
};
default:
return state;
}
}
이번에는 reducer를 빠르게 작성하고 테스트를 작성했습니다. 테스트 코드 작성하고 테스트를 통과할 수 있는 코드 작성한다
라는 순서를 무조건 적으로 지키는 것은 비효율적이라고 생각합니다. 상황에 맞춰 작성하고 과정을 반복하는 것이 더 효과적이라는 생각입니다.
import React from "react";
import { mount } from "@cypress/react";
import { TodoList } from "./todo-list";
describe("TodoList Component", () => {
beforeEach(function () {
cy.fixture("todos.json").as("todos");
this.deleteTodo = cy.stub();
this.addTodo = cy.stub();
});
it("contains correct number of todos", function () {
mount(
<TodoList
todos={this.todos}
addTodo={this.addTodo}
deleteTodo={this.deleteTodo}
/>
);
cy.get("[data-cy=todo-list]")
.children()
.should("have.length", this.todos.length);
});
it('renders "완료" button', function () {
mount(
<TodoList
todos={this.todos}
addTodo={this.addTodo}
deleteTodo={this.deleteTodo}
/>
);
cy.get("[data-cy=todo-list]")
.children()
.each(($el) => {
cy.wrap($el).find("button").should("contain.text", "완료");
});
});
});
변경 내용에 맞춰서 다시 todo, todo-list를 수정하고 테스트하는 과정을 반복합니다. 코드는 링크에서 확인할 수 있습니다.