View

이 글은 인프런의 견고한 JS 소프트웨어 만들기를 학습하며 작성한 글입니다.
강좌에서는 Jasmine을 사용했지만 글에서는 Jest를 사용했습니다.

배경

강좌에서 학습한 내용과 직접 만들어본 예제를 강좌에서 소개한 TDD의 단계에 따라 구현해나가는 과정을 정리했습니다.

TDD의 3단계

1단계 : 적색 단계

Unit Test 코드를 작성합니다. Unit Test 코드는 준비(arrange), 실행(act), 단언(assert) 패턴을 따릅니다. 기능 구현 전 상태이므로 테스트가 무조건 실패하며 이를 적색 단계로 표현합니다.

2단계 : 녹색 단계

1단계에서 작성한 Unit Test 코드가 성공할 수 있도록 최소한의 기능을 구현합니다. Unit Test가 모두 통과하며 이를 녹색 단계로 표현합니다.

3단계 : 청색 단계

2단계에서 작성한 코드는 1단계의 Unit Test를 통과하기 위해서만 작성된 최소 구현이므로 리팩토링을 통해 코드의 품질을 높이는 과정을 청색 단계로 표현합니다.

 

Jest의 기본 문법 (Jest)
https://mmong-mong.tistory.com/8
 

[TDD] Jest 시작하기 & 기본 문법

배경 TDD를 학습하며 Jest의 기본 문법을 정리한다. Jest Facebook에서 만든 테스팅 프레임워크 "Delightful Javascript Testing" https://jestjs.io/ 시작하기 jest 설치 $ npm run i -D jest babel 설정 $ yarn..

mmong-mong.tistory.com


구현 목표

  • hex 값을 input에 입력 한 후 변경을 누르면 백그라운드 색이 변경되어야한다.
  • 초기화를 클릭하면 input의 입력값과 백그라운드 색이 초기화되어야한다.

 

적색 단계

간단한 마크업과 테스트 코드, 모듈의 인터페이스를 구현합니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>
      <input id="input-hex" />
      <input id="btn-change" type="button" value="변경" onclick="onClickChange()" />
      <input id="btn-reset" type="button" value="초기화" onclick="onClickReset()" />
    </div>
  </body>
  <script>
  
  </script>
  <style>
    div {
      display: flex;
      justify-content: center;
      align-items: center;
      height: calc(100vh - 20px);
    }
    #input-hex {
      height: 30px;
    }
    input[type='button'] {
      height: 37px;
      width: 75px;
      margin-left: 5px;
    }
  </style>
</html>
/**
 * @jest-environment jsdom
 */
import BgColorSwitcher from "./BgColorSwitcher.js";

describe("Background Color Switcher", () => {
  describe("getInputValue", () => {
    test("입력값을 확인할 수 있다.", () => {
      // 준비
      const value = "#bbbbbb";

      // 실행
      const inputValue = BgColorSwitcher.getInputValue();

      // 단언
      expect(inputValue).toBe(value);
    });
  });
const BgColorSwitcher = {
  getInputValue() {
    return "";
  },
};

export default BgColorSwitcher;

위의 경우 getInputValue에선 아무 값도 return해주고 있지 않기 때문에 테스트는 실패합니다.

다음 단계에서 테스트를 통과할 수 있는 코드를 작성해봅니다.

 

녹색 단계

const BgColorSwitcher = {
  getInputValue() {
    const inputEle = document.getElementById('input-hex');
    return inputEle.value;
  },
};

export default BgColorSwitcher;

이 경우 테스트는 통과합니다. 하지만 input 태그의 id 값이 변경되면 BgColorSwitcher의 코드를 변경해야하는 의존성이 생겼습니다. 

다음과 같이 리팩토링하여 의존성을 분리할 수 있습니다.

 

청색 단계

const BgColorSwitcher = function (inputEle) {
  return {
    getInputValue() {
      return inputEle.value;
    },
  };
};
export default BgColorSwitcher;

BgColorSwitcher함수는 input 태그를 인자로 받아 그 태그를 inputEle라는 변수명으로 들고 있는 클로저 함수가 포함된 객체를 리턴합니다. BgColorSwitcher가 변경됨에 따라 테스트 코드도 아래와 같이 변경합니다.

/**
 * @jest-environment jsdom
 */
import BgColorSwitcher from "./BgColorSwitcher.js";

describe("Background Color Switcher", () => {
  describe("getInputValue", () => {
    test("입력값을 확인할 수 있다.", () => {
      const bgColor = "#bbbbbb";
      const inputEle = document.createElement("input");
      inputEle.value = bgColor;
      const switcher = BgColorSwitcher(inputEle);
      
      const expected = switcher.getInputValue();

      expect(expected).toBe(inputEle.value);
    });
  });
});

 

여기까지는 input에 입력한 값만 확인할 수 있었습니다.

본격적으로 배경색이 변경되는 동작을 테스트 코드로 작성해보겠습니다. 작성은 적색 단계부터 다시 시작합니다. 

 


적색 단계

/**
 * @jest-environment jsdom
 */
import { rgbStringToHex } from "./helper";
import BgColorSwitcher from "./BgColorSwitcher.js";

describe("Background Color Switcher", () => {
  const bgColor = "#bbbbbb";
  const target = document.createElement("body");
  const inputEle = document.createElement("input");

  const switcher = BgColorSwitcher(inputEle);

  beforeEach(() => {
    inputEle.value = bgColor;
  });

  describe("getInputValue", () => {
    test("입력값을 확인할 수 있다.", () => {
      const expected = switcher.getInputValue();

      expect(expected).toBe(inputEle.value);
    });
  });

  describe("changeBgColor", () => {
    test("입력한 값으로 특정 dom의 background color를 변경할 수 있다.", () => {
      const color = switcher.getInputValue();
      switcher.changeBgColor(target, color);

      /*
      dom 엘리먼트에 inline으로 삽입된 hex color값은 rgb 값으로 변환되기 때문에
      hex값과의 비교를 위해선 rgb값을 다시 hex값으로 변환해주어야합니다.
      */
      const expected = rgbStringToHex(target.style.backgroundColor);

      expect(expected).toBe(bgColor);
    });
  });
});

beforeEach를 활용하여 중복되는 코드를 제거했습니다.

changeBgColor는 배경색을 변경해야하는 타겟 엘리먼트와 변경할 색을 hex 값으로 받습니다. 아직 changeBgColor를 구현하지 않았기 때문에 테스트 코드는 실패합니다.

 

녹색 단계

const BgColorSwitcher = function (inputEle) {
  return {
    getInputValue() {
      return inputEle.value;
    },
    changeBgColor(ele, color) {
      ele.style.backgroundColor = color;
    },
  };
};

export default BgColorSwitcher;

이로써 테스트를 통과하게 되었습니다.

 

청색 단계

생략합니다.

 


최종 버전

버튼에 이벤트 핸들러를 바인딩해주는 기능과 input을 초기화시켜주는 기능까지 구현한 코드는 아래와 같습니다.

 

/**
 * @jest-environment jsdom
 */
import { rgbStringToHex } from "./helper";
import BgColorSwitcher from "./BgColorSwitcher.js";

describe("Background Color Switcher", () => {
  const bgColor = "#bbbbbb";
  const target = document.createElement("body");
  const inputEle = document.createElement("input");

  const switcher = BgColorSwitcher(inputEle);

  beforeEach(() => {
    inputEle.value = bgColor;
  });

  describe("getInputValue", () => {
    test("입력값을 확인할 수 있다.", () => {
      const expected = switcher.getInputValue();

      expect(expected).toBe(inputEle.value);
    });
  });

  describe("changeBgColor", () => {
    test("입력한 값으로 특정 dom의 background color를 변경할 수 있다.", () => {
      const color = switcher.getInputValue();
      switcher.changeBgColor(target, color);

      /*
      dom 엘리먼트에 inline으로 삽입된 hex color값은 rgb 값으로 변환되기 때문에
      hex값과의 비교를 위해선 rgb값을 다시 hex값으로 변환해주어야합니다.
      */
      const expected = rgbStringToHex(target.style.backgroundColor);

      expect(expected).toBe(bgColor);
    });

    describe("bindHandler", () => {
      test("element에 이벤트 핸들러를 등록할 수 있다.", () => {
        const btnEle = document.createElement("input");
        btnEle.type = "button";
        const handler = jest.fn(() => true);

        switcher.bindHandler(btnEle, "click", handler);
        btnEle.click();

        expect(handler).toHaveBeenCalled();
        expect(handler).toHaveReturnedWith(true);
      });
    });

    describe("resetInput", () => {
      test("입력값을 초기화할 수 있다.", () => {
        const currentValue = switcher.getInputValue();
        expect(currentValue).toBe(bgColor);

        switcher.resetInput();
        const expected = switcher.getInputValue();

        expect(expected).toBe("");
      });
    });
  });
});
const BgColorSwitcher = function (inputEle) {
  return {
    getInputValue() {
      return inputEle.value;
    },
    changeBgColor(ele, color) {
      ele.style.backgroundColor = color;
    },
    bindHandler(ele, event, handler) {
      ele.addEventListener(event, handler);
    },
    resetInput() {
      inputEle.value = "";
    },
  };
};

export default BgColorSwitcher;

 

BgColorSwitcher를 활용하여 구현한 최종 코드입니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>
      <input id="input-hex" />
      <input id="btn-change" type="button" value="변경" />
      <input id="btn-reset" type="button" value="초기화" />
    </div>
  </body>
  <script type="module">
    import BgColorSwitcher from './BgColorSwitcher.js';

    const inputEle = document.getElementById('input-hex');
    const changeBtnEle = document.getElementById('btn-change');
    const resetBtnEle = document.getElementById('btn-reset');
    const bodyEle = document.getElementsByTagName('body')[0];

    const switcher = BgColorSwitcher(inputEle);

    switcher.bindHandler(changeBtnEle, 'click', () => {
      const color = switcher.getInputValue();
      switcher.changeBgColor(bodyEle, color);
    });

    switcher.bindHandler(resetBtnEle, 'click', () => {
      switcher.resetInput();
      switcher.changeBgColor(bodyEle, 'transparent');
    });
  </script>
  <style>
    div {
      display: flex;
      justify-content: center;
      align-items: center;
      height: calc(100vh - 20px);
    }
    #input-hex {
      height: 30px;
    }
    input[type='button'] {
      height: 37px;
      width: 75px;
      margin-left: 5px;
    }
  </style>
</html>

 

결론

강의에서는 Counter 예제를 사용했었는데 그대로 따라하면 강의에서 가르쳐주는 TDD를 제대로 습득하지 못할 거 같아 직접 예제를 만들어서 실습해봤다. TDD로 하는 것보다는 기능 구현부터 하는게 익숙하기 때문에 BgColorSwitcher의 인터페이스도 처음에는 getInputValue, onChangeBtnHandler 등으로 구성했었다. 

하지만 강의의 흐름대로 테스트 코드부터 작성하다보니 onChangeBtnHandler와 같은 인터페이스는 적절하지 않다는 걸 깨달을 수 있었다. 의존성 분리와 TDD의 3단계 구현 과정에 대해 이해할 수 있었던 좋은 경험이었다. 

 

전체 코드는 https://github.com/woriwori/bg-color-switcher/blob/main/README.md 에서 확인하실 수 있습니다.
인프런 강의에서 제공한 코드는 https://github.com/jeonghwan-kim/lecture-develop-fe-with-tdd 입니다. 
Share Link
reply