YeonjuOHYE / javascript

0 stars 0 forks source link

javascript TDD #23

Open YeonjuOHYE opened 3 years ago

YeonjuOHYE commented 3 years ago

목차

출처

1. TDD (Test-Driven-Development))란

TDD 란 무엇인가 :

소프트웨어 개발 프로세스 Production code 전에 테스트는 먼저 작성

테스트의 필요성

스크린샷 2020-12-06 오후 1 35 33 스크린샷 2020-12-06 오후 1 35 59

Life cycle of TDD

스크린샷 2020-12-06 오후 1 56 05 스크린샷 2020-12-06 오후 1 57 54

Benefits

Myths (근거 없는 믿음)

2. unit test

TDD는 unit 테스트를 강제(force)한다. TDD 는 unit test를 이용하여 코드의 작은 부분을 테스트

unit tes(단위 테스트 )란

Unit test 프레임 워크

3. TDD 와 react 그리고 디자인 패턴

unit test 를 위해 => 관심사의 분리, Single Responsibility Principle

container + presenter 패턴 그리고 redux

예제 코드 : 할 일의 목록 관리하는 List

List.js

import React from "react";

import Item from "./Item";

export default function List({ tasks, onClickDelete }) {
  if (tasks.length === 0) {
    return <p>할 일이 없어요!</p>;
  }

  return (
    <ol>
      {tasks.map((task, index) => (
        <Item key={index} task={task} onClickDelete={onClickDelete} />
      ))}
    </ol>
  );
}

List.test.js

import React from "react";

import { render, fireEvent } from "@testing-library/react";
import { shallow, configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import List from "./List";

describe("List", () => {
  const handleClickDelete = jest.fn();

  function renderList(tasks) {
    return render(<List tasks={tasks} onClickDelete={handleClickDelete} />);
  }

  context("with tasks", () => {
    const tasks = [
      { id: 1, title: "할일 #1" },
      { id: 2, title: "할일 #2" },
    ];

    it("renders tasks", () => {
      const { getByText } = renderList(tasks);

      expect(getByText(/할일 #1/)).not.toBeNull();
      expect(getByText(/할일 #2/)).not.toBeNull();
    });

    it('renders "완료" button to delete a task', () => {
      const { getAllByText } = renderList(tasks);

      const buttons = getAllByText("완료");

      fireEvent.click(buttons[0]);

      expect(handleClickDelete).toBeCalledWith(1);
    });
  });

  context("without tasks", () => {
    it("render no tasks message", () => {
      const tasks = [];

      const { getByText } = renderList(tasks);

      expect(getByText(/할 일이 없어요/)).not.toBeNull();
    });
  });
});

ListContainer.js

import React from 'react';

import { useDispatch, useSelector } from 'react-redux';

import List from './List';

import {
  deleteTask,
} from './actions';

export default function ListContainer() {
  const { tasks } = useSelector((state) => ({
    tasks: state.tasks,
  }));

  const dispatch = useDispatch();

  function handleClickDeleteTask(id) {
    dispatch(deleteTask(id));
  }

  return (
    <List
      tasks={tasks}
      onClickDelete={handleClickDeleteTask}
    />
  );
}

ListContainer.test.js

import React from 'react';

import { useSelector } from 'react-redux';

import { render } from '@testing-library/react';

import ListContainer from './ListContainer';

import tasks from '../fixtures/tasks';

jest.mock('react-redux');

describe('ListContainer', () => {
  useSelector.mockImplementation((selector) => selector({
    tasks,
  }));

  it('renders tasks', () => {
    const { getByText } = render((
      <ListContainer />
    ));

    expect(getByText(/아무 것도 하지 않기/));
  });
});

reducer.js

const initialState = {
  tasks: [],
};

export default function reducer(state = initialState, action) {
  if (action.type === 'setTasks') {
    const { tasks } = action.payload;
    return {
      ...state,
      tasks,
    };
  }

  if (action.type === 'deleteTask') {
    const { tasks } = state;

    return {
      ...state,
      tasks: tasks.filter((task) => task.id !== action.payload.id),
    };
  }

  return state;
}

reducer.test.js

import reducer from './reducer';

import {
  setTasks,
  deleteTask,
} from './actions';

import tasks from '../fixtures/tasks';

describe('reducer', () => {
  describe('setTasks', () => {
    it('changes tasks array', () => {
      const initialState = {
        tasks: [],
      };

      const state = reducer(initialState, setTasks(tasks));

      expect(state.tasks).not.toHaveLength(0);
    });
  });

  describe('deleteTask', () => {
    context('with existed task ID', () => {
      it('remove the task from tasks', () => {
        const state = reducer({
          tasks: [
            { id: 1, title: 'Task' },
          ],
        }, deleteTask(1));

        expect(state.tasks).toHaveLength(0);
      });
    });

    context('without existed task ID', () => {
      it("doesn't work", () => {
        const state = reducer({
          tasks: [
            { id: 1, title: 'Task' },
          ],
        }, deleteTask(100));

        expect(state.tasks).toHaveLength(1);
      });
    });
  });
});

4. snapshot 테스트

Enzyme 을 이용한 snapshot test, snapshot은 텍스트 파일

configure({ adapter: new Adapter() });
describe("<List />", () => {
  it("matches snapshot", () => {
    const wrapper = shallow(<List tasks={[]} />);
    expect(wrapper.debug()).toMatchSnapshot();
  });
});
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<List /> matches snapshot 1`] = `
"<p>
  할 일이 없어요!
</p>"
`;
configure({ adapter: new Adapter() });
describe("<List />", () => {
  const tasks = [
    { id: 1, title: "할일 #1" },
    { id: 2, title: "할일 #2" },
  ];
  it("matches snapshot", () => {
    const wrapper = shallow(<List tasks={tasks} />);
    expect(wrapper.debug()).toMatchSnapshot();
  });
});

테스트 결과

스크린샷 2020-12-07 오후 10 48 45

5. SDD (Story-Driven-Development)

Storybook이란

스토리북(Storybook)은 한 문장으로 정의가 어려울 정도로 다양한 방식으로 사용되고 있는 UI 컴포넌트 개발 도구 .단순히 회사의 UI 라이브러리를 내부 개발자들을 위해 문서화(documentation)하기 위해서 사용할 수 있고, 외부 공개용 디자인 시스템(Design System)을 개발하기 위한 기본 플랫폼으로도 사용할 수 있다.

SDD

스토리 코드를 작성하며 UI 업데이트하는 것! test 관점에서 storybook은 수동 테스트를 쉽게 도와준다. 예를 들어, storybook이 없다면 로그인 => 시나리오 만족 => 화면 이라는 귀찮은 프로세스를 거쳐 (수동)테스트를 하지만 storybook 을 이용하면 바로 원하는 화면으로 갈 수 있다. 이 를 위해 ui의 다양한 케이스에 대한 story(storybook의 단위)를 작성하고 UI를 작성하면 원하는 화면을 한 번에 볼 수 있을 뿐만 아니라 이 구현 과정에서 props들이 자연스럽게 분리가 되게 되는 효과를 얻을 수 있다.

import React from "react";

import List from "./List";

export default {
  title: "Example/List",
  component: List,
};

//story1. 할 일이 없을 때
export const EmptyTemplate = () => <List tasks={[]} />;

//story2. 할 일이 있을 때
export const TodoList = () => (
  <List
    tasks={[
      { id: 1, title: "할일 #1" },
      { id: 2, title: "할일 #2" },
      { id: 2, title: "할일 #3" },
    ]}
  />
);
스크린샷 2020-12-07 오후 11 11 29 스크린샷 2020-12-07 오후 11 11 51

storyshots

storybook은 story snapshot 기능을 제공

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Example/List Empty Template 1`] = `
<p>
  할 일이 없어요!
</p>
`;

exports[`Storyshots Example/List Todo List 1`] = `
<ol>
  <li>
    할일 #1
    <button
      onClick={[Function]}
      type="button"
    >
      완료
    </button>
  </li>
  <li>
    할일 #2
    <button
      onClick={[Function]}
      type="button"
    >
      완료
    </button>
  </li>
</ol>
`;

storyshots-puppeteer

puppeteer는 접속한 페이지를 스크린샷을 찍거나 PDF로 제공한느 tool storyshots-puppeteer 는 puppeteer의 이러한 기능을 이용하여 위의 텍스트 파일로 snapshot을 뜨는 Enzyme과 storyshots 달리 이미지 파일로 snapshot을 제공

스크린샷 2020-12-07 오후 11 27 27 스크린샷 2020-12-07 오후 11 27 35 스크린샷 2020-12-07 오후 11 30 07

6. 요약 및 결론

react 에서 TDD 를 효율적으로 하기 위해서는 unit 테스트를 위한 '관심사의 분리' 원칙하에서 코드를 작성하는 것이 좋고 이를 구현할 수 있는 디자인 패턴으로는 'container + presenter 패턴 그리고 redux 사용' 이 있다. container 와 redux은 적극적으로 TDD를 적용하며 작성하는 것이 좋고 UI에 해당하는 presenter 컴포넌트들은 로직이 없기 때문에 TDD보다는 화면 시나리오에 따라 컴포넌트를 개발하도록 돕는 SDD을 적용하여 개발하는 것이 더 좋다고 판단된다. SDD을 통해 작성된 story 들을 storyshot이나 storyshots-puppeteer 등을 이용하여 자동으로 테스트를 할 수 있기 때문에 시간적인 이득을 볼 수도 있다.