본문 바로가기

React/실험실

[React] 벨로퍼트와 함께하는 React Testing - TDD

들어가며

벨로퍼트님의 테스팅 튜토리얼을 공부한 내용을 정리하는 글입니다. 

 

TDD ? 

TDD는 테스트를 통해서 개발을 이끌어나가는 형태의 개발론이다. 

먼저 테스트 코드를 작성하고, 구현을 하는 방식이라고 보면 된다. 

 

TDD에는 3가지 절차가 있다. 

실패 

첫 번째 절차로 실패이다. 말 그대로 실패하는 테스트 케이스를 먼저 만드는 것이다. 

상황에 따라 다르지만, 먼저 구현할 기능부터 하나씩 테스트 케이스를 작성한다. 

 

성공

두 번째 절차로 성공이다. 

앞서 작성한 실패하는 테스트 케이스를 통과시키기 위해서 코드를 작성하는 것을 말한다. 

 

리팩토링 

구현한 코드가 테스트를 통과한다면, 중복되는 코드 또는 개선시킬 방법이 있는 코드를 리팩토링 시킨다. 

리팩토링을 했을 때 테스트 케이스가 성공하면 다른 기능을 다시 첫 번째 절차부터 수행한다. 

 

즉,

실패 => 성공 => 리팩토링 => 실패 => 성공 ... 방식으로 개발하는 것이다. 

 

TDD의 장점 

TDD를 진행하면서 테스트 케이스를 작성할 때 작은 단위로 개발하기 때문에 

테스트 코드가 방대해지지 않고, 코드의 모듈화가 잘 이루어진다. 

 

또, TDD를 통해서 자연스럽게 테스트 커버리지가 높아진다. 

기능을 구현할 때 테스트를 먼저 작성하기 때문에 당연한 결과라고 볼 수 있다. 

테스트 커버리지가 높아진다는 것은 리팩토링이 쉬워지고 유지보수도 쉬워진단는 뜻이다.  

 

TDD 연습해보기

배열이 주어졌을 때 최댓값, 최솟값, 평균, 중앙값, 최빈값을 구하는 함수를 구현해보자! 

벨로퍼트님의 게시글에는 바로 정답이 나오기 때문에 이부분은 해당 코드를 보지 않고 

내가따로 작성해보겠다!

 

최댓값

import { max } from "./stats";

describe("stats", () => {
  test("Max Value : [1, 2, 3] = 3", () => {
    const array = [1, 2, 3];

    expect(max(array)).toBe(3);
  });
});

다음과 같이 작성했다. 

당연히 아직 stats에 아무것도 작성하지 않았기 때문에 max가 없다는 오류가 뜨고, 이제 max를 구현하자!

 

export const max = (array) => {
  const sorting = [...array].sort((a, b) => b - a);

  return sorting[0];
};

sort를 사용해서 내림차순으로 정렬한 배열에서 첫 번째 값을 return한다. 

아~ 주 문제없이 동작하는데, 더 쉬운 방법이 있었다... 

 

export const max = (array) => {
  return Math.max(...array);
};

 

최솟값

import { max, min } from "./stats";

describe("stats", () => {
  const array = [1, 2, 3];

  test("Max Value : [1, 2, 3] = 3", () => {
    expect(max(array)).toBe(3);
  });

  test("Min Value : [1, 2, 3] = 1", () => {
    expect(min(array)).toBe(1);
  });
});

최댓값과 크게 다르지 않다. 

 

export const min = (array) => {
  return Math.min(...array);
};

그리고 난 애송이가 아니다. 

max가 있다면 당연히 min이 있을 것이기 때문에 min을 사용해서 작성했다. 

 

평균값

  test("Average : [1, 2, 3] = 2", () => {
    expect(avg(array)).toBe(2);
  });

평균을 구하는 테스트 케이스를 작성했다. 

 

export const avg = (array) => {
  const sum = array.reduce((prev, curr) => prev + curr, 0);
  return sum / array.length;
};

reduce를 활용해서 풀었다. 

그런데 좀 더 깔끔하게 만드는 방법이 있었다. 

 

export const avg = (array) => {
  return array.reduce((prev, curr, _, { length }) => prev + curr / length, 0);
};

(1 + 2 + 3 ) / 3이 1 / 3 + 2 / 3 + 3 / 3과 똑같다는 사실을 생각안하고 있었다. 

이런 방식으로 해결하면 보다 깔끔하게 만들 수 있다.

 

중앙값

describe("stats", () => {
  // ...

  describe("Median", () => {
    const array1 = [1, 2, 3];
    const array2 = [1, 2, 3, 4];

    test("case 1 : [1, 2, 3] = 2", () => {
      expect(median(array1)).toBe(2);
    });

    test("case 2 : [1, 2, 3, 4] = 2.5", () => {
      expect(median(array2)).toBe(2.5);
    });
  });
});

 

중앙값은 배열의 개수에 따라 구하는 방법이 달라진다. 

배열이 홀수개라면 단순하게 가운데 값인 2가 중앙값이지만,

짝수개라면 2 + 3 / 2 인 2.5가 중앙값이 된다. 

 

그래서 describe 안에 다시 describe으로 케이스를 나눠주었다. 

하지만 test 안에서 describe을 사용할 순 없다.

export const median = (array) => {
  const sorting = [...array].sort((a, b) => a - b);

  const center = Math.floor(sorting.length / 2);

  return center % 2
    ? sorting[center]
    : (sorting[center - 1] + sorting[center]) / 2;
};

 

최빈값 

  describe("Mode", () => {
    const array1 = [1, 2, 2, 2, 3];
    const array2 = [1, 2, 3];
    const array3 = [1, 2, 2, 3, 3];

    test("case 1 : [1, 2, 2, 2, 3] = 2", () => {
      expect(mode(array1)).toBe(2);
    });
    test("case 2 : [1, 2, 3]", () => {
      expect(mode(array2)).toBe(null);
    });
    test("case 3 : [1, 2, 2, 3, 3] = [2, 3]", () => {
      expect(mode(array3)).toEqual([2, 3]);
    });
  });

최빈값은 배열에서 빈도가 가장 높은 값을 나타낸다. 이때 상황에 따라 결과가 3가지로 나눠진다. 

 

1. 주어진 값 중 가장 자주 나타난 값이 하나인 경우 가장 높은 값이 최빈값이다. 

[1, 2, 2, 2, 3] => 2

 

2. 모든 값이 빈도가 같은 경우 최빈값은 없다. 

[1, 2, 3] => null

 

3. 빈도가 같은 값이 여러개인 경우, 모두 최빈값이다. 

[1, 2, 2, 3, 3] => [2, 3]

 

export const mode = (array) => {
  const frequency = new Map();
  const set = new Set(array);

  array.forEach((value) => {
    frequency.set(value, frequency.get(value) ? frequency.get(value) + 1 : 1);
  });

  const maxValue = Math.max(...frequency.values());

  const result = [...frequency.keys()].filter(
    (number) => frequency.get(number) === maxValue
  );

  if (result.length === set.size) return null;
  else if (result.length === 1) return result[0];
  else return result;
};

 map과 set을 사용해서 문제를 해결했다. 

여기서 forEach를 대신해서 reduce를 사용하면 더 깔끔하게 해결이 가능하다. 

 

export const mode = (array) => {
  const set = new Set(array);
  const frequency = array.reduce(
    (acc, cur) => acc.set(cur, acc.get(cur) ? acc.get(cur) + 1 : 1),
    new Map()
  );

  const maxValue = Math.max(...frequency.values());

  const result = [...frequency.keys()].filter(
    (number) => frequency.get(number) === maxValue
  );

  if (result.length === set.size) return null;
  else if (result.length === 1) return result[0];
  else return result;
};

정리 

TDD를 사용했을 때 확실히 테스트 코드가 존재하기 때문에 리팩토링이 편하다. 

하지만 아직 이것을 프론트엔드에서 어떤 방식으로 사용하면 좋을지는 감이 잡히지 않는다. 

 

추후, 공부할 내용이 기대된다!

반응형