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

1

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를 수정하고 테스트하는 과정을 반복합니다. 코드는 링크에서 확인할 수 있습니다.

Reference

hokyun.rhee's profile image

hokyun.rhee

2021-07-25 16:30

Read more posts by this author