[Jest][유닛 테스트] (2) 테스트 코드로 디버깅해보자! 예약 날짜 비활성화 오류, 테스트 코드로 해결하기

🧐 유닛 테스트, 왜 진행하기로 했을까?

프로젝트에서 제대로 만들었다고 생각했지만 예상치 못하게 또다른 오류를 발견한 부분이 있었다.

이전에도 한번 오류를 해결했다고 생각했는데, 또 발견하게 되다니..
상황에 따라 예상치 못한 오류가 있을 수 있는 부분이라고 생각했고, 이 참에 아예 테스트를 작성한 뒤 명확하게 로직 자체를 검사해가면서 확실하게 의도한 대로 동작하도록 만들어보기로 결심했다.

💀 문제였던 부분, 예약기간 선택하기 기능

스테이 메모리는 숙박 장소를 골라 예약할 수 있는 숙박플랫폼 프로젝트였다.
원하는 숙박장소를 선택해 상세페이지에서 예약 기간을 선택할 수 있다.

구현 시 다음과 같은 조건을 만족해야 했다.

예약 기간 선택 시 예약 불가 날짜를 포함할 수 없음

사용한 라이브러리는 에어비앤비에서 만든 react-dates인데, 이 라이브러리를 통해 비활성화한 날짜는 클릭할 수 없다. 하지만 아래와 같이 해당 날짜를 포함해 예약 기간을 선택할 수는 있기 때문에, 이 부분을 별도로 로직을 추가해주어야 했다.


선택한 날짜 이후 예약 불가능한 날짜 전까지만 체크아웃 날짜 선택 가능

그래서 체크인 날짜를 선택했을 때, 예약 불가 날짜가 있다면 해당 날짜 전까지만 체크아웃날짜를 선택할 수 있도록 비활성화 처리를 했다.


그런데 이렇게 안되는 경우가 발생했다.


내가 만들었지만... 내가 봐도 희한하다..🥲

명확한 테스트 코드 없이 개발하면 이렇게 되는 걸까... TDD의 필요성을 알게 된 계기이기도 했다.

테스트 케이스를 명확하게 검사하지 않고 일일히 각 케이스를 눌러보지 않는한 발견하기 힘든 오류였다.

이건 기회다!

아무래도 구현한 함수의 로직에 문제가 있는 것 같았다.
안 그래도 해보고 싶었던 참에..! 이 기회에 테스트 코드를 작성해 검사하고 문제를 찾아내 해결해보기로 했다.

자, 그러면 이 오류를 해결하는 과정을 TDD로 해보자!


😃 테스트 구성하기

당장 필요한 부분, 할 수 있는 부분부터!

처음엔 컴포넌트 단위로 테스트를 진행하고 싶었고, 컴포넌트에서 어떤 날짜를 선택하든지 로직을 테스트하고 싶다는 막연한 생각을 했는데, 그러다보니 어디서부터 어떻게 시작해야할지 감이 안 잡혔다.
일단은 현재 해결하고자 하는 부분에 집중해 테스트할 범위를 좁히기로 했다. 에러를 해결하는데 초점을 맞춰보자.

이 테스트 코드를 완성하고 에러를 해결한 뒤에, 시간적으로 여유가 있고 필요성을 느낀다면 그 범위를 확장하기로 결정했다.


1. 현재 테스트할 범위 명확히 하기

라이브러리에서 체크인 날짜를 선택하면 focusedInput props가 'endDate'으로 바뀌고, 선택한 날짜는 startDate에 저장된다. 이 startDate을 기준으로 비활성화할 날짜를 계산하게 된다.

불특정한 날짜를 클릭하도록 만들면 고려해야할 사항이 많아지고 동일한 인풋과 아웃풋을 관리하기 힘들어질 것이기 때문에, 이 범위에 한해서 필요한 변수값은 고정하기로 했다.

따라서 이 값들은 미리 고정해두었다.

  const startDate = '2022-03-26';
  const focusedInput = 'endDate';

원래는 컴포넌트 단위의 테스트를 진행할 예정이었지만, 컴포넌트별로 연결된 기능을 테스트하기보다는 이 에러를 해결하기 위해 해당 컴포넌트 내부의 각 함수들의 로직을 테스트하게 되어 단위 테스트가 되었다.

처음 받아올 예약 불가능한 날짜는 다음과 같다.

2022-03-29
2022-03-30
2022-04-01
2022-04-04
2022-04-05

그리고 선택할 날짜는 2022-03-26이다.
이 날짜를 선택하면 선택 가능한 날짜는 모두

2022-03-26
2022-03-27
2022-03-28

이고, 나머지 날짜는 모두 비활성화되어야 한다.


선택한 날짜 이전 날짜는 비활성화 하지 않는다.

아래와 같이 선택한 날짜 이전 날짜를 다시 선택할 경우 다시 체크인 날짜가 선택이 되고, 그 다음에 체크아웃날짜를 선택할 수 있다.
따라서 이 경우는 예약 기간을 선택할 때 문제가 되지 않기 때문에 비활성화 하지 않기로 했다.

이전 날짜를 다시 선택하거나, 달력 창을 다시 껐다 켰을 때 기간을 재설정할 수 있다.


2. 예약 불가 날짜 받아오기

mock함수를 만들어 리턴할 값을 고정해주었다.
프로미스형태로 fetch해 데이터를 받아온다고 가정하고, 실제 컴포넌트 코드처럼 사용할 변수 unAvailableDates에 넣어주었다.

  it('예약 불가 날짜 정보 받아오기', async () => {
    getUnavaliableDate = jest
      .fn()
      .mockResolvedValue([
        '2022-03-29',
        '2022-03-30',
        '2022-04-01',
        '2022-04-04',
        '2022-04-05',
      ]);

    unAvailableDates = await getUnavaliableDate();

    expect(unAvailableDates).toEqual([
      '2022-03-29',
      '2022-03-30',
      '2022-04-01',
      '2022-04-04',
      '2022-04-05',
    ]);
  });

3. 선택한 체크인 날짜 이후 첫번째 예약 불가 날짜 찾기

mockImplmentation()메서드를 사용해 함수를 그대로 옮겨왔다.
여기에서 unAvailableDates state 값을 사용하기 때문에 위에서 똑같이 변수를 만들어주었다.

startDate은 2022-03-26이므로, 받아온 예약 불가날짜들을 순회하면서 26일 이후의 첫번째로 가장 가까운 예약 불가날짜인 29일을 반환해야 한다.

    checkFirstUnAvailableDates = jest.fn().mockImplementation(() => {
      for (let i = 0; i < unAvailableDates.length; i++) {
        if (moment(unAvailableDates[i]).isAfter(startDate)) {
          return unAvailableDates[i];
        }
      }
    });
  it('선택한 체크인 날짜 26일 이후 첫번째 예약 불가 날짜 29일 찾기', () => {
    expect(checkFirstUnAvailableDates()).toBe('2022-03-29');
  });

4. 선택한 체크인 날짜와 첫번째 예약 불가날짜 사이 구간찾기

이 부분은 조금 더 고민이 필요했다.
사용한 달력 라이브러리에 props로 전달해주는 함수는 라이브러리내에서 조금 특별하게 작동했기 때문이다.

어떻게 작동하는지 조금 더 자세하게 정리하면...

라이브러리 isDisabled 기능

라이브러리에 isDisabled props로 전달해주는 함수는 달력이 표시된 후 자동으로 차례로 렌더링된 날짜들을 인자로 전달받아 호출되고, true가 반환되면 해당 날짜는 비활성화된다.

라이브러리를 통째로 실행하기엔 여러 어려움이 있고 테스트원칙에도 맞지 않다고 생각했으므로, 이 기능을 대체할 별도의 목업 함수를 만들어주었다.

비활성화 여부를 검사할 함수

isDisabled props로 넘겨주는 disableDates 함수는 그대로 가져왔다.
비활성화할 날짜라면 true를, 하지 않고 선택 가능하게 둘 날짜라면 false를 반환한다.

날짜가 선택한 날짜 이후의 첫번째 예약 불가 날짜보다 뒤에 있다면 비활성화하도록 true를 반환한다.

    disableDates = jest.fn().mockImplementation(momentDate => {
      const date = momentDate.format('YYYY-MM-DD');
      if (focusedInput === 'startDate') {
        return unAvailableDates.includes(date) ? true : false;
      } else if (focusedInput === 'endDate') {
        if (checkFirstUnAvailableDates()) {
          return unAvailableDates.includes(date) ||
            moment(date).isAfter(checkFirstUnAvailableDates())
            ? true
            : false;
        }
      }
    })

isDisabled 기능을 대체할 mock 함수

mockIsDiabledFunctionProps()는 이 isDisabled 함수 props로 전달된 함수를 라이브러리와 유사한 형태로 실행시키기 위해 임의로 만든 함수이다.
필요한 만큼 moment 객체 형태로 각 날짜들을 차례로 인자를 넣어 disabledDates라는 함수를 실행시켜준다.

원래는 불리언 값만 반환하면 라이브러리에서 알아서 비활성화되지만, 여기에서는 테스트 결과를 직관적으로 검사하기 위해 비활성화되지 않은, 즉 선택 가능한 날짜만 배열에 넣어 리턴해주도록 했다.

    mockIsDisabledFunctionProps = jest.fn().mockImplementation(() => {
      const startDate = '2022-03-26';
      let enabledDates = [];
      for (let i = 0; i < 31; i++) {
        const momentDate = moment(startDate).add(i, 'days');
        if (!disableDates(momentDate)) {
          enabledDates.push(momentDate.format('YYYY-MM-DD'));
        }
      }
      return enabledDates;
    });
  });
  it('선택한 체크인 날짜와 첫번째 예약 불가날짜 사이 구간 26~28일만 선택 가능하도록 하기', () => {
    expect(mockIsDisabledFunctionProps()).toStrictEqual([
      '2022-03-26',
      '2022-03-27',
      '2022-03-28',
    ]);
  });

테스트 항목은 다음과 같다.
1. 예약 불가 날짜를 받아온 뒤,
2. 선택한 체크인 날짜 이후의 첫 예약 불가 날짜를 찾고,
3. 해당 구간을 제외하고 비활성화하는지 테스트한다.
(테스트를 수월하게 하기 위해 여기서는 비활성화하지 않을 날짜들만 받아서 테스트한다.)

  it('예약 불가 날짜 정보 받아오기', async () => {
    unAvailableDates = await getUnavaliableDate();

    expect(unAvailableDates).toEqual([
      '2022-03-29',
      '2022-03-30',
      '2022-04-01',
      '2022-04-04',
      '2022-04-05',
    ]);
  });

  it('선택한 체크인 날짜 26일 이후 첫번째 예약 불가 날짜 29일 찾기', () => {
    expect(checkFirstUnAvailableDates()).toBe('2022-03-29');
  });

  it('선택한 체크인 날짜와 첫번째 예약 불가날짜 사이 구간 26~28일만 선택 가능하도록 하기', () => {
    expect(mockIsDisabledFunctionProps()).toStrictEqual([
      '2022-03-26',
      '2022-03-27',
      '2022-03-28',
    ]);
  });
});

테스트해보자!

의도한 대로 모든 테스트를 통과했다.


응?


😦 어라? 문제가 없다?

테스트를 진행해보니 로직 자체에는 문제가 없었다.
그런데.. 왜 이 로직대로 구현한 실제 코드에서는 다른 결과가 나오는 것일까?
구현한 함수 로직에 문제가 없다면 다른 부분에서 테스트에서 고려하지 못한 문제가 있었던 것일 것이다.

실제 코드와 달리 테스트에서 고정해두어 실제와 다를 수 있는 값은 다음과 같았다.

  • startDate
  • focusedInput
  • 최초로 받아온 예약 불가 날짜

그리고 시도해본 것중 하나에서 실마리를 찾았다.


❗️ 실제 api 응답값과 mock함수 결과값 비교하기

보편적으로 쓰일 수는 없는 방법일 것이라 생각하지만, 현재상황에서는 실제 코드와 동일한 조건으로 인풋값을 설정해두었기 때문에 가능했다.

일단 실제 api 호출 값과 설정한 값이 동일한지 비교해보았다.

찾았다!

테스트에서는 순서대로 날짜를 넣어주었는데,

    getUnavaliableDate = jest
      .fn()
      .mockResolvedValue([
        '2022-03-29',
        '2022-03-30',
        '2022-04-01',
        '2022-04-04',
        '2022-04-05',
      ]);

실제 호출값은 정렬이 되어 있지 않았다.

결과값을 똑같지만, 실제 함수에서는 정렬이 날짜순서대로 되지 않았던 것..!
첫번째 예약 불가 날짜를 찾을 때 for문은 단순히 인덱스 별로 순회하기 때문에 가장 첫번째 인덱스에 위치한 날짜를 리턴했고, 그것이 2022-04-04였다. 그래서 그 이전 29일부터 4일까지의 날짜들은 이미 비활성화된 날짜를 제외하고는 비활성화되지 않았던 것이다.

api 호출 시 결과값을 정렬해 상탯값에 넣어주도록 바꾸어주었다.

      const res = await fetch(unAvailableDatesUrl(stayIdParams, today));
      const resJson = await res.json();
      const undates = resJson.data.date.sort();
      setUnavailableDates(undates);

이제는 원하는대로 26일을 클릭했을 때 비활성화된 29일 이전까지 선택 가능하도록 나머지 날짜가 잘 비활성화 된다!


😃 느낀 점

조금은 아쉬웠다.

작성한 테스트 항목 중 하나를 통과하지 못한다면, 로직을 직접 수정해 고쳐보는 재미가 있었을 것 같은데... 직접 찾아가며 처음으로 작성한 테스트인데 로직이 아닌 다른 부분에서 문제를 해결해 조금 싱거웠다.

그래도 테스트의 중요성과 장점에 대해 알게 되었다.

처음부터 문제였던 부분을 꼼꼼히 확인했으면 더 좋았겠다는 생각이 들었지만,
반대로 테스트 코드를 작성함으로서 사소한 오류를 막고 로직을 확실하게 검증할 수 있다는 생각이 들었다.

이전에는 당연히 날짜 순서대로 반환된다고 생각했고, 결과를 콘솔에 찍어보면서도 이 부분을 잘 캐치하지 못해 오류 해결에 애를 먹었다.

그리고 함수 로직 자체가 문제일 것이라는 생각하고 있었는데, 이 로직만 분리해 테스트해본 뒤 문제가 있는지 여부를 명확히 확인하고 나니 디버깅할 범위를 점차 좁혀나갈 수 있었다.

더 해보고 싶은 것은,

컴포넌트 단위의 테스트!
사실 위에서 언급했듯이 원래는 컴포넌트 단위의 테스트를 진행할 생각이었지만, 컴포넌트별로 연결된 기능을 테스트하기에는 어려움이 있었고 이 에러를 해결하기 위해 해당 컴포넌트 내부의 각 함수들의 로직을 테스트하게 되어 단위 테스트가 되었다.

이 프로젝트에서 중요한 부분인 쿼리스트링을 관리하는 부분은 유닛테스트 보다는 컴포넌트 단위로 테스트를 진행하는 것이 더 적절할 것 같기 때문에, 시간이 된다면 도전해보고 싶다.

좋은 웹페이지 즐겨찾기