[프리코스 2주차] 혼자 공부하는 크루들을 위한 지식의 동기화

pengooseDev·2023년 11월 3일
18
post-thumbnail

혼자 프리코스를 달려가는 크루들에게

일전에도 이야기했지만, 필자는 17년지기 친구와 프리코스를 함께 달려가고 있다.

친구에게 코드리뷰를 받아볼 것을 권했지만, 아무래도 본인이 부족하다는 느낌 때문에 선뜻 리뷰를 부탁하기 어려운가보다.

작년의 나도 동일한 이유로 코드리뷰를 진행하지 못했던 생각이 났다.
커뮤니티 활동을 하지 않더라도 정보의 격차가 너무 심하게 벌어지지 않게 2주차 미션의 코드리뷰를 진행하며, 공통적으로 남겼던 피드백을 정리해보았다.

혼자 달려가는 프리코스 크루들 전부 화이팅이다. 🥳


목차

1. 테스트코드 리팩터링 
	- beforEach
    - describe
    - each
    - given/when/then
    - 포매팅
    - description convention
2. 기능명세서와 TDD
3. 테스트코드 환경에서의 ESLint
4. 커스텀에러

1. 테스트코드도 코드다.

포비가 한 말이다.

테스트코드도 불필요한 기능이 있지 않은지, 리팩터링 할 수 있는 요소가 있는지 가꾸고 돌봐야한다는 의미이다.

간단한 계산기 예제로 리팩터링 과정을 살펴보자.

원본코드

test('초기 상태의 value는 0이어야 한다', () => {
  const calculator = new Calculator();
  
  expect(calculator.value).toBe(0);
});

test('value를 증가시킨 후의 값은 1이어야 한다', () => {
  const calculator = new Calculator();
  calculator.add(1);
  
  expect(calculator.value).toBe(1);
});

test('value에서 1을 빼면 값은 -1이어야 한다', () => {
  const calculator = new Calculator();
  calculator.subtract(1);
  
  expect(calculator.value).toBe(-1);
});

자주 보이는 코드이다.
아마 반복되는 로직에서 불편함을 느낀 크루들이 많을 것이다.

아래와 같이 해결할 수 있다.


1. beforEach 활용하기

beforeEach 함수는 각 테스트가 실행되기 전에 실행되는 함수이다.
반복되는 코드가 줄어들어 각 테스트케이스의 의도가 더 명확히 드러난다.

describe('계산기 테스트', () => {
  let calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  test('초기 상태의 value는 0이어야 한다', () => {
    expect(calculator.value).toBe(0);
  });

  test('value에 1을 더하면 값은 1이어야 한다', () => {
    calculator.add(1);
    expect(calculator.value).toBe(1);
  });

  test('value에서 1을 빼면 값은 -1이어야 한다', () => {
    calculator.subtract(1);
    expect(calculator.value).toBe(-1);
  });
});

2. 중첩된 describe를 이용해 테스트 세분화하기

테스트케이스를 하나만 넣는 것은 조금 아쉽다.
add 메서드를 조금 더 자세히 테스트해보자

이때 describe를 중첩해서 사용하면, 코드를 보는 크루들에게 테스트에 대한 컨텍스트를 더 자세히 공유할 수 있다.

describe('계산기 테스트', () => {
  let calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  // ...codes

  describe('add 메서드', () => {
    test('value에 1을 더하면 값은 1이어야 한다', () => {
      calculator.add(1);
      expect(calculator.value).toBe(1);
    });

    test('value에 2를 더하면 값은 2가 되어야 한다', () => {
      calculator.add(2);
      expect(calculator.value).toBe(2);
    });

    test('value에 -1을 더하면 값은 -1이 되어야 한다', () => {
      calculator.add(-1);
      expect(calculator.value).toBe(-1);
    });

    test('value에 0을 더해도 값은 변하지 않아야 한다', () => {
      calculator.add(0);
      expect(calculator.value).toBe(0);
    });

    test('value에 100을 더하면 값은 100이 되어야 한다', () => {
      calculator.add(100);
      expect(calculator.value).toBe(100);
    });
  });

  // ...codes
});

3. each를 이용한 중복 테스트

2번의 예제는 자세하지만 테스트가 너무 길어질 우려가 있다. jest에서 제공하는 test.each 메서드를 사용해서 리팩터링을 진행해보자.

describe('계산기 테스트', () => {
  let calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  // ...codes

  describe('add 메서드', () => {
    // 테스트케이스를 배열로 추가한 뒤
    const addCases = [
      { input: 1, expected: 1 },
      { input: 2, expected: 2 },
      { input: -1, expected: -1 },
      { input: 0, expected: 0 },
      { input: 100, expected: 100 },
    ]; 
    
    // test.each를 활용해 각 테스트케이스를 확인한다.
    test.each(addCases)(
      '주어진 값을 value에 더합니다',
      ({ input, expected }) => {
        expect(calculator.add(input)).toBe(expected);
      },
    );
  });

  // ...codes
});

4. test에 given/when/then 컨벤션 적용하기

프리코스의 테스트는 given / when / then 패턴을 적용한 코드를 제공한다.
만들어줬으면 쓰자!

given : 주어진 데이터
when : 실행하는 순간
then : 예상되는 결과

  describe('add 메서드', () => {
    // given
    const addCases = [
      { input: 1, expected: 1 },
      { input: 2, expected: 2 },
      { input: -1, expected: -1 },
      { input: 0, expected: 0 },
      { input: 100, expected: 100 },
    ]; 
    
    test.each(addCases)(
      '주어진 값을 value에 더합니다',
      ({ input, expected }) => {
		// when
        const result = calculator.add(input);
        
        //then
        expect(result).toBe(expected);
      },
    );
  });

5. 포매팅을 활용하여 잘 담아내기

현재 테스트의 description이 너무 성의가 없다.
물론, 의도한 것이다. 포매팅을 적용해보자.

  describe('add 메서드', () => {
    // given
    const addCases = [
      { input: 1, expected: 1 },
      { input: 2, expected: 2 },
      { input: -1, expected: -1 },
      { input: 0, expected: 0 },
      { input: 100, expected: 100 },
    ]; 
    
    // test.each를 활용해 각 테스트케이스를 확인한다.
    test.each(addCases)(
      '계산기의 value에 $input를 더하면 $expected가 되어야 한다.',
      ({ input, expected }) => {
		// when
        const result = calculator.add(input);
        
        //then
        expect(result).toBe(expected);
      },
    );

$input, $expected가 무엇인지 처음에 의아할 수 있다.
테스트를 돌리면 아래처럼 문자열 포매팅이 적용되어 실제 대입되는 변수가 렌더링된다.


6. description convention 적용하기

계산기의 value에 $input를 더하면 $expected가 되어야한다.라는 테스트 description은 테스트를 어느정도 잘 설명한다.

하지만, 테스트 규모가 커질 경우 description이 과연 테스트를 잘 반영하고 있는가?에 대해 고민하는 코스트는 점점 커지고 실수가 반복된다.

그래서 description convention을 만들어보았고 이번주 미션부터 적용해보고자한다.
필자가 정한 description convention은 다음과 같다.

  • 두 문장으로 이루어질 것.
  • 쉼표로 구분할 것.
  • 첫 문장은 given과 when으로 이루어 질 것.
  • 두 번째 문장은 then으로 이루어 질 것.

적용해보자.

    test.each(addCases)(
      '인자로 $input를 전달하는 경우, $expected를 반환한다.',
      //     given    when         then
      ({ input, expected }) => {
        const result = calculator.add(input);
        
        expect(result).toBe(expected);
      },
    );

최종 결과!

날짜를 파싱하는 포메터의 경우 다음과 같이 사용할 수 있다 :)

describe('Formatter.DateFormatter', () => {
  const testCases = [
    { input: '2023-06-15T11:45:00Z', expected: '2023.06.15 20:45:00' },
    { input: '2023-02-28T23:59:59Z', expected: '2023.03.01 08:59:59' },
    { input: '2024-02-29T12:00:00Z', expected: '2024.02.29 21:00:00' },
    { input: '2023-04-01T23:45:00Z', expected: '2023.04.02 08:45:00' },
    { input: '2023-07-01T00:00:00Z', expected: '2023.07.01 09:00:00' },
    { input: '2025-12-25T07:30:00Z', expected: '2025.12.25 16:30:00' },
    { input: '2023-01-01T23:30:30Z', expected: '2023.01.02 08:30:30' },
    { input: '2023-12-31T16:00:00Z', expected: '2024.01.01 01:00:00' },
  ];

  describe('포매팅 형식 테스트(YYYY.MM.DD HH:MM:SS)', () => {
    test.each(testCases)(
      '인자로 $input가 주어지면, $expected를 반환한다.',
      ({ input, expected }) => {
        expect(Formatter.DateFormatter(input)).toBe(expected);
      },
    );
  });
});


2. 기능명세서. 그리고 TDD

세 개의 요구사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 commit하는 방식으로 진행한다.

1주차의 요구사항이었던 "기능명세서"가 발전하였다. 이제는 기능명세서 단위로 commit을 진행할 것을 요구하고있다.

즉, 기능명세서에 작성한 내용으로 TC + 구현을 해야하고, commit로그가 일치해야한다는 것이다.

TDD

요구사항을 만족하기 위해, 다음과 같은 Flow로 미션을 진행해볼 수 있다.

기능명세서 작성 => (내부가 비어있어서 실패하는)TestCode 작성 => 코드 구현 => TC 내부 작성 및 통과 => 기능명세서와 동일하게 commit

이는 TDD에서 요구하는 방법론이다. 다만, 필자는 위 방식을 적용하였다가 TC를 마지막에 구현하는 방식으로 회귀하였다. 아직은 실력이 부족한 탓이다. 이전 기능명세서 관련 글에 0단계(테스트코드 작성)가 추가되었다고 생각하면 편하다.


3. 테스트코드와 ESLint 공존하기

ESLint 때문에 테스트코드에 빨간줄이 죽죽 그어지는 경험을 해보았을 것이다.
이에 eslint를 폴더 내에서 무시하는 등의 설정이 종종 보였는데, ESLint 설정 파일의 overrides 속성을 사용하여 해결할 수 있다.

  overrides: [
    {
      files: ['경로'],
      rules: {
        // rules 재설정
        'max-lines-per-function': 'off',
        'max-depth': 'off',
      },
    },
  ],

4. 두근두근 빼빼로데이. 수제 에러 만들기

  • 모든 에러는 [ERROR] 메시지로 시작해야한다.

위 조건을 만족하기 위해 모든 에러 메시지에 [ERROR]를 붙였는가?
이제 남이 만든 에러를 사먹지말고 집에서 직접 만들어보자.

CustomError 만들기

class CustomError extends Error {
  constructor(message, name) {
    super(`[ERROR] ${message}`);
    this.name = name;
  }
}

export default CustomError;

> MDN

아래와 같은 식으로 각 Layer별 서로 다른 커스텀에러를 반환할 수도 있다.

import ERROR from '../constants/error.js';
import MessageFormat from '../utils/messageFormat.js';

class CustomError extends Error {
  constructor(message, name) {
    super(MessageFormat.error(message));
    this.name = name || this.constructor.name;
  }

  static InputView(message) {
    return new CustomError(message, ERROR.name.inputView);
  }

  static Car(message) {
    return new CustomError(message, ERROR.name.car);
  }

  static RacingGame(message) {
    return new CustomError(message, ERROR.name.racingGame);
  }
}

export default CustomError;

5. 반 집의 승리

> 미생 : 묵묵히 나의 길을 가는 것도 용기다

반 집으로 바둑을 지게되면, 이 많은 수들이 다 뭐였나 싶었다.
작은 사활 다툼에서 이겨봤자, 기어이 패싸움을 이겨봤자.
결국 지게 된다면 그게 다 무슨 소용인가 싶었다.

하지만.
반 집으로라도 이겨보면, 다른 세상이 보인다.
이 반 집의 승부가 가능하게, 상대에 대항해 살아준 돌들이 고맙고.
조금씩이라도 삭감에 들어간 한 수, 한 수가 귀하기만 하다.

순간 순간의 최선이 반 집의 승리를 가능케 하는 것이다.

묵묵히 달려가는 프리코스 크루들 전부 화이팅이다. 🥳👍

6개의 댓글

comment-user-thumbnail
2023년 11월 4일

좋은 글 감사합니다 ~ CustomError를 다른 분의 코드에서 많이 확인할 수 있는데 정말 유익한 방법인 것 같네요! 🙂

1개의 답글
comment-user-thumbnail
2023년 11월 8일

캬 좋은 글 이네여 잘 보고 갑니다~

1개의 답글
comment-user-thumbnail
2023년 11월 13일

도움 받고 갑니다.

1개의 답글