숙박플랫폼 리팩토링 - 커스텀 훅 테스트,useQueryString 관심사 분리

GY·2022년 3월 25일
0

Test

목록 보기
6/7

커스텀 훅에 핵심 로직이 포함되어 있었으므로 꼭 테스트 코드를 작성해둘 필요를 느꼈다.

E2E테스트로 잘 작동하는지 사용성 테스트는 완료했지만,
코드의 유지보수 측면에서 작동여부 뿐 아니라 로직 자체를 테스트할 수 있는 코드를 꼭 작성해두는 것이 좋을 것 같았다.

여기에 더해, 리팩토링의 필요성을 다시 느끼게 되었던 이유도 있었다.

  • useQueryString 내부에도 너무 많은 로직이 포함되어 있으며, 관심사의 분리가 제대로 이루어져 있지 않다.
  • 타입별로 useQuerySTring,Arr, Object가 나뉘어져 있다.

그래서 이렇게 만들고 싶었다.

  • useQueryString에서는 배열, 객체 모든 타입을 처리할 수 있으며, 쿼리스트링을 관리하는 로직만 포함한다.
  • 원래 각각의 useQueryString커스텀훅에 포함되어 있던 로직은 다시 관심사를 분리하여 각각 useCheckbox, useUpdateState로 만든다.

useQueryString 하나로 합치기

useCheckBox, use

테스트가 어려웠던 이유

바로 useLocation등의 훅이 포함되어 있었기 때문에 통째로 래퍼 컴포넌트로 감싸 렌더링 한 다음, 그 안에서 커스텀 훅을 테스트해보기는 어려웠다.

따라서 mock하여 useLocation등을 구현해 함수의 로직 자체를 테스트하고, 이를 통해 케이스를 검증해가며 원하는대로 다시 리팩토링해보고자 했다.

어이구.. 조잡해라...!

에러!

어라..? 왜 테스트도 마쳤는데, 에러가 생겼을까?

테스트 인풋값은 배열만 테스트했는데, 실제로 넣은 값은 객체 형태로 안에 배열이 포함되어 있었기 때문이다.

이전

 const isChecked = property => {
    return state.includes(property);
  };

  const isCheckedAll = () => {
    return data.every(obj => state.includes(obj.name));
  };

  const handleCheckedAll = () => {
    let updatedList = [];

    if (!isCheckedAll()) {
      data.forEach(obj => {
        updatedList.push(obj.name);
      });
    }
    setState(updatedList);
  };
  return { isChecked, isCheckedAll, handleCheckedAll };
}

테스트

/**
 * @jest-environment jsdom
 */

import { TYPE_DATA } from '../constants';

describe('체크박스 테스트', () => {
  let handleCheckedAll;
  let isCheckedAll;
  let isChecked;
  let state;
  let stateKey;
  let stateValue;
  let data;

  beforeEach(() => {
    state = {
      category: ['guesthouse'],
    };
    data = TYPE_DATA.category;

    stateKey = Object.keys(state)[0];
    stateValue = state[stateKey];

    isChecked = property => {
      return stateValue.includes(property);
    };

    isCheckedAll = () => {
      return data.every(obj => stateValue.includes(obj.name));
    };

    handleCheckedAll = () => {
      let updatedList = [];

      if (!isCheckedAll()) {
        data.forEach(obj => {
          updatedList.push(obj.name);
        });
      }

      const updatedState = {
        [stateKey]: updatedList,
      };

      return updatedState;
    };
  });

  it('isChecked(): 체크 여부 테스트', () => {
    state = {
      category: ['guesthouse'],
    };
    expect(isChecked('guesthouse')).toBe(true);
  });

  it('isCheckedAll: 모든 항목 체크 여부 테스트', () => {
    state = {
      category: ['guesthouse', 'hotel'],
    };
    stateValue = state[stateKey];
    expect(isCheckedAll()).toEqual(true);
  });

  it('handleCheckAll(): 전체 선택 기능 테스트 ', () => {
    state = {
      category: ['guesthouse'],
    };
    expect(handleCheckedAll()).toEqual({
      category: ['guesthouse', 'hotel'],
    });
  });
});

function useCheckBox(state, setState, data) {
  const stateKey = Object.keys(state)[0];
  const stateValue = state[stateKey];

  const isChecked = property => {
    return stateValue.includes(property);
  };

  const isCheckedAll = () => {
    return data.every(obj => stateValue.includes(obj.name));
  };

  const handleCheckedAll = () => {
    let updatedList = [];

    if (!isCheckedAll()) {
      data.forEach(obj => {
        updatedList.push(obj.name);
      });
    }

    const updatedState = {
      [stateKey]: updatedList,
    };

    setState(updatedState);
  };
  return { isChecked, isCheckedAll, handleCheckedAll };
}

export default useCheckBox;

key 값을 스트링으로 받아오고 싶지 않다.

string값으로 입력하고 싶지 않았다.

  const handleChange = e => {
    const { name } = e.target;
    updateState('category', name);
  };

테스트

/**
 * @jest-environment jsdom
 */

describe('객체와 배열 상태 변경 테스트', () => {
  let updateState;
  let testObj;
  let testObjWithArr;
  let state;
  let stateKey;

  beforeEach(() => {
    state = {
      category: ['guesthouse', 'hotel'],
    };
    stateKey = Object.keys(state)[0];

    updateState = (value, key = stateKey) => {
      if (Array.isArray(state[key])) {
        let updatedArray = [];
        let updatedState = [];
        if (!state[key].includes(value)) {
          updatedArray = [...state[key], value];
        } else {
          updatedArray = [...state[key]].filter(el => {
            return el !== value;
          });
        }
        updatedState = {
          [key]: updatedArray,
        };
        return updatedState;
      } else {
        let updatedState = {};
        updatedState = {
          ...state,
          [key]: value,
        };
        return updatedState;
      }
    };
    testObj = {
      checkin: '2022-03-22',
      checkout: '2022-03-26',
    };

    testObjWithArr = {
      category: ['guesthouse'],
    };
  });

  it('state가 객체일 떄 전달받은 key와 value로 상태변경', () => {
    state = testObj;
    expect(updateState('2022-03-24', 'checkin')).toEqual({
      checkin: '2022-03-24',
      checkout: '2022-03-26',
    });
  });

  it('변경할 state가 배열일 때 중복되는 값이 없으면 전달받은 value 추가', () => {
    state = testObjWithArr;
    stateKey = Object.keys(state)[0];

    expect(updateState('hotel')).toEqual({
      category: ['guesthouse', 'hotel'],
    });
  });

  it('변경할 state가 배열일 때 중복되는 값이 있으면 전달받은 value 삭제', () => {
    state = testObjWithArr;
    stateKey = Object.keys(state)[0];

    expect(updateState('guesthouse')).toEqual({
      category: [],
    });
  });
});

function useUpdateState(state, setState) {
  const stateKey = Object.keys(state)[0];
  const updateState = (value, key = stateKey) => {
    if (Array.isArray(state[key])) {
      let updatedArray = [];
      let updatedState = [];
      if (!state[key].includes(value)) {
        updatedArray = [...state[key], value];
      } else {
        updatedArray = [...state[key]].filter(el => {
          return el !== value;
        });
      }
      updatedState = {
        [key]: updatedArray,
      };
      setState(updatedState);
    } else {
      let updatedState = {};
      updatedState = {
        ...state,
        [key]: value,
      };
      setState(updatedState);
    }
  };
  return { updateState };
}

export default useUpdateState;

느낀점

불편한 점

mock 함수로 만들다 보니 테스트를 통과하고 나면 다시 자바스크립트 파일을 수정해야 했다. 만약 커스텀 훅 자체를 가져와 테스트할 수 있다면 더 좋을텐데...

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글