[JS] 테스트 자동화

MJ·2022년 9월 1일
1

Java Script

목록 보기
29/57
post-thumbnail

테스트 자동화 및 Mocha

테스트란 구현하려고 목표로 삼은 코드가 제대로 동작하는지, 테스트 코드에 대입해서
본문의 코드와 테스트 코드의 결과 값이 일치하는지 테스트하는 과정입니다.

이러한 테스트 과정을 자동화로 구현할 수 있습니다.


1) 테스트가 필요한 이유

  1. 개발자는 f() 함수를 구현하고 있다.
  2. 구현 도중에 f(1) 이라는 함수는 구동이 되지만 f(2) 라는 함수는 구동이 안된다.
  3. f(2) 함수가 동작될 때 까지 코드를 수정하고, 동작이 되면 수정을 마무리한다.

위 예시는 테스트를 사용하지 않고 수동으로 코드를 검사하는 경우 입니다.

개발자는 시행착오 끝에 f(2) 함수의 오류를 검출하고 수정했습니다.
동작이 되어 코드를 더이상 검사하지 않습니다.

만약 코드를 수정하는 과정중에 f(1) 함수에 오류가 발생해서 작동하지 않는다면,
이미 검사는 끝났기 때문에 치명적인 오류가 발생하게 됩니다.

이런 문제를 해결하기 위해서 탄생한 것이 테스트 자동화 입니다.


2) BDD (Behavior Driven Development)

BDD는 테스트(test), 문서(documentation), 예시(example)를 한데 모아놓은 개념입니다.


3) 거듭제곱 함수와 명세서

xn번 곱해주는 함수 pow(x, n)을 구현하고 있다고 가정 해봅시다.
거듭제곱을 사용하지 않고 함수로 구현하는 이유는 BDD를 테스트해보기 위해서입니다.

코드를 작성하기 전에 먼저 해야할 작업이 있습니다. 코드가 무슨일을 하는지 상상한 후
자연어로 표현해야 합니다. 이렇게 만들어진 산출물을 명세서 또는 스펙(spec)이라고 부른다.

❤️ 테스트 코드 작성

describe("pow", function() {

  it("주어진 숫자의 n 제곱", function() {
    assert.equal(pow(2, 3), 8);
  });

});


/* 설명 */
명세서(스펙) 3가지 요소 

describe("title", function() {...} )
구현하고자 하는 기능에 대한 설명이 들어갑니다. 함수 `pow`가 어떤 동작을 하는지에 대한
설명을 "title" 에 작성합니다.

it("유스 케이스 설명", function() {...})
it의 첫 번째 인수로는 특정 유스 케이스에대한 설명이 들어갑니다. 누구나 이해할 수 있도록
자연어로 기재합니다. 두 번째 인수에는 유스 케이스 테스트 함수가 들어갑니다.

assert.equal(value1, value2)
it 블럭 안에서 실제로 함수를 테스트하는 구문이다.
본문에 있는 함수를 호출해서, 인수 값을 전달해 본문의 함수가 정상적으로 구동하는지 테스트 함.
여기서 본문의 함수는 `pow()` 가 됩니다.

assert.equal은, value1과 2의 값이 동등한지 비교합니다. 동등하지 않다면 오류 발생

                            

❗ 유스 케이스(used case)
프로그램이 어떻게 작동하는지에 대한 작동과정을 보여주는 다이어그램 입니다.
한마디로 이 코드가 어떻게 상호작용, 구동 될 것인지에 대한 과정을 알려주는 것

4) 개발 순서

프로젝트가 시작되면 순차적으로 단계를 거쳐가게 됩니다.

1. 명세서 초안 작성 ( 초안에는 기본적인 테스트도 진행됩니다 )
2. 명세서 초안을 검토 후 코드 작성 ( 본문 문서에서 함수 구현 )
3. 코드가 작동하는지 확인하기 위해서 테스트 프레임워크를 사용해서 명세서 실행
Mocha라고 부릅니다. 코드가 잘못 작성되면 에러를 출력하고, 에러를 해결할 때 까지
수정을 반복합니다.

4. 테스트가 종료되면 명세서에서 고려하지 않은 유스케이스를 몇 가지 추가합니다.
5. 유스케이스 테스트 중 실패를 하게 되면, 다시 테스트 프레임워크를 사용합니다.
6. 기능이 완성될 때 까지 3-5번 과정을 반복합니다.

위에서 명세서를 작성한 부분은, 개발 순서의 1번에 해당 됩니다.


5) 스펙 실행하기

  • Mocha :
    핵심 테스트 프레임워크로 describe it과 같은 테스팅 함수와 테스트 실행 관련된
    주요 함수들을 제공합니다.

  • Chai :
    다양한 assertion을 제공해주는 라이브러리입니다.

  • Sinon :
    함수의 정보를 캐내는 데 사용되는 라이브러리로, 내장 함수등을 모방합니다.

세 개의 라이브러리모두 브라우저, 서버 사이드 환경등을 가리지 않고 사용 가능합니다.


❤️ 브라우저 환경을 가정하고 사용

<!DOCTYPE html>
<html>
<head>
  <!-- 결과 출력에 사용되는 mocha css를 불러옵니다. -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
  <!-- Mocha 프레임워크 코드를 불러옵니다. -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
  <script>
    mocha.setup('bdd'); // 기본 셋업
  </script>
  <!-- chai를 불러옵니다 -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
  <script>
    // chai의 다양한 기능 중, assert를 전역에 선언합니다.
    let assert = chai.assert;
  </script>
</head>

<body>

  <script>
    function pow(x, n) {
      /* 본문에 pow()함수는, 테스트 코드와 비교합니다. */
    }
  </script>

  <!-- 테스트(describe, it...)가 있는 스크립트를 불러옵니다. -->
  <script src="test.js"></script>

  <!-- 테스트 결과를 id가 "mocha"인 요소에 출력하도록 합니다.-->
  <div id="mocha"></div>

  <!-- 테스트를 실행합니다! -->
  <script>
    mocha.run();
  </script>
</body>

</html>


/* */
위 코드를 5등분 해서 나누면

(1) head : 테스트에 필요한 서드파티 라이브러리와 스타일을 불러옵니다.

(2) script : 테스트할 함수가 들어갑니다. (pow의 코드)

(3) 테스트 진행 :  테스트할 스크립트 코드가 들어있는 문서 파일 (test.js)을 불러옵니다.
⚠️ 스펙을 작성한 파일명을 입력하세요.

(4) <div id="mocha"> :  모차 실행결과가 출력 됩니다.

(5) mocha.run() : 테스트를 실행해주는 명령어 입니다.

❤️ 결과

 function pow(x, n) {
      /* 코드를 여기에 작성합니다. 지금은 빈칸으로 남겨두었습니다. */
    }

지금은 함수 pow() 본문에서 아무런 코드도 없기 때문에 테스트가 실패합니다.
테스트 파일은 본문의 코딩을 참조해서 그 결과 값과 테스트 결과 값을 확인 하는데,
본문에서는 아무런 코드도 없기 때문에 테스트 파일에서는 undefined를 반환합니다.

pow(2, 3)은 8이 아닌 undefined를 반환하기 때문에 오류 발생


6) 꼼수 사용하기

<script>
    function pow(x, n) {
        return 8;	// 테스트 코딩에서 2의 3제곱은 8이므로, 본문에서도 반환 값을 8로 반환합니다.
    }
  </script>

오류가 발생하지 않는다.
기능 구현이 아니라, 단순하게 테스트 코드에게 값을 넘겨줄 때, 테스트 코드와 동일한 값
8을 반환해줬기 때문에 결과가 동일해서 오류라고 인식하지 못합니다.


7) it 블럭 추가하기

이번에는 꼼수가 아니라 실제로 기능을 구현해서 함수가 제대로 동작하는지 알아보기전에
유스케이스가 하나 이상일 때 스펙에 테스트를 추가하는 방법 2가지를 살펴보겠습니다.

❤️ 기존 it 블럭에 assert 추가하기

describe("pow", function() {
  
  it('주어진 숫자의 n제곱', function() {
    assert.equal(pow(2, 3), 8);
    assert.equal(pow(3, 4), 81);
  });
  
});  


/* */
assert에서 오류가 발생하면 테스트를 하는 it 블럭은 바로 종료됩니다., 하나의 it블럭에 테스트를 해야할 구문이 2개 이상일 때 첫 번째 assert가 실패하면
두 번째 assert의 결과는 알 수 없습니다.

❤️ it 블럭 하나 더 생성

describe("pow", function() {
  
  it('2의 3제곱은 8입니다.', function() {
    assert.equal(pow(2, 3), 8);
  });
  
  it('3의 4제곱은 81입니다.', function() {
    assrt.equal(pow(3, 4), 81);
  });
  
});


/* */
it 블럭을 하나 더 추가하면, it 블럭 1개당 테스트를 1개씩 진행 하므로
첫 번째 it 블럭에 assert 테스트의 실패유무 상관 없이, 두 번째 it 블럭도 테스트를 진행합니다.

따라서 하나의 it 블럭에는 하나의 테스트만 실행하자.
연관이 있는 테스트인 경우에만 여러 개 사용합시다.

8) 기능 구현하기

특정 숫자의 제곱을 구하는 기능을 함수로 구현해서 꼼수가 아니라, 실제로 함수가 작동하는지
테스트를 해보겠습니다.

function pow(x, n) {	// x : 제곱의 대상이 되는 숫자, n : x를 n번 곱할 때 사용되는 숫자
  
  let result = 1;		// 제곱한 x의 값이 저장되는 곳
  
  for(let i = 0; i < n; i++) {	// x를 n번 제곱한다 ( 거듭제곱 연산 수행 )
  	result *= x;				// 2x1, 2x2, 2x4
  }
  return result;				// 제곱의 결과 값 반환
}

기능을 구현함으로써 본문에서 구현한 함수가 테스트를 통해 제대로 된 기능을 수행하는지
확인할 수 있습니다. 지금은 테스트를 수동으로 값을 넣어서 특정 수(n)의 제곱을 연산하는
함수를 테스트 해봤습니다. 이번에는 테스트도 기능을 구현해서 자동으로 특정 수의 제곱을
연산할 수 있게 수정해보겠습니다.


❤️ 테스트 코드 자동화 하기

describe("pow 함수 테스트", function () {

  function powTest(x) { 

    let result = x * x * x;		// 제곱의 결과 값이 저장 됨

    it(`${x} 의 3 제곱은 :: ${result}`, function () {
      assert.equal(pow(x, 3), result);	// pow 함수에 x,3 인자를 전달하고, 반환 값과 result와 비교
    });
  }

    for (let x = 1; x <= 5; x++) {		// x : 제곱의 대상이 되는 숫자
      powTest(x);		// 함수 호출
    }               

});

함수의 기능이 정상적으로 테스트되고 구동하는 것을 확인할 수 있습니다.


9) 중첩 describe

describe를 중첩으로 사용하면 그룹으로 정의할 수 있습니다.

하나의 함수에서 여러 개의 테스트가 필요할 때 메인이 되는 describe 하위에 여러 개의
describe를 생성함으로써 함수 하나로 여러 개를 테스트할 수 있습니다.

describe("pow 함수 테스트", function () {

  describe("x의 3제곱 테스트", function () {

    function powTest(x) { // x 값을 인자로 받음

      let result = x * x * x;

      it(`${x} 의 3 제곱은 :: ${result}`, function () {
        assert.equal(pow(x, 3), result);
      });

    }

    for (let x = 1; x <= 5; x++) {
      powTest(x);
    }

  });

  describe("x의 4 제곱 테스트", function () {

    function powTest(x) { // x 값을 인자로 받음

      let result = x * x * x * x;

      it(`${x} 의 4 제곱은 :: ${result}`, function () {
        assert.equal(pow(x, 4), result);
      });

    }

    for (let x = 1; x <= 5; x++) {
      powTest(x);
    }

  });

});


/* */
기존에 x에 대한 3제곱을 테스트하는 코드와 새로이 테스트 진행을 원하는 x에 대한 4제곱을
테스트할 수 있는 describe를 만들 수 있습니다.


10) before, after와 beforeEach, afterEach 차이점

before 함수는 전체 테스트가 실행되기 전에 실행되고, after 함수는 전체 테스트가
실행된 후에 실행됩니다. beforeEach는 매번 it 블럭이 실행되기 전에 실행되고,
afterEach 함수는 it 블럭이 실행된 이후에 실행 됩니다.

함수명내용
before전체 테스트가 실행되기 전에 실행된다.
after전체 테스트가 실행된 이후에 실행된다.
beforeEachit 블럭이 실행되기 전에 실행 된다.
afterEachit 블럭이 실행된 이후에 실행된다.
describe("test", function() {

  before(() => alert("테스트를 시작합니다 - 테스트가 시작되기 전"));
  after(() => alert("테스트를 종료합니다 - 테스트가 종료된 후"));

  beforeEach(() => alert("단일 테스트를 시작합니다 - 각 테스트 시작 전"));
  afterEach(() => alert("단일 테스트를 종료합니다 - 각 테스트 종료 후"));

  it('test 1', () => alert(1));
  it('test 2', () => alert(2));

});


/* 실행 순서 */ 
테스트를 시작합니다 - 테스트가 시작되기           (before)
단일 테스트를 시작합니다 - 각 테스트 시작          (beforeEach)
1
단일 테스트를 종료합니다 - 각 테스트 종료          (afterEach)
단일 테스트를 시작합니다 - 각 테스트 시작          (beforeEach)
2
단일 테스트를 종료합니다 - 각 테스트 종료          (afterEach)
테스트를 종료합니다 - 테스트가 종료된             (after)


❗ before/beforeEach와 after/afterEach는 대부분 초기화 용도로 사용됩니다.
카운터 변수를 0으로 만들거나, 테스트가 바뀔 (테스트의 그룹이 바뀔 때 마다) 해줘야 하는
작업이 있다면, 이 함수를 이용해서 값을 초기화 할 수 있습니다.



스펙 확장하기

위 명세서에서 특정 수(x)에 대한 제곱(n)을 테스트 했습니다. 만약 n이 숫자형이 아니고
다른 자료형이거나, 음수라면 Javascript에서는 NaN 값을 반환 할 것입니다.

n이 조건에 맞지 않는 값일 때 함수가 nan을 반환하는지에 대한 검사를 하기 위한
테스트를 작성해보겠습니다.

describe("pow", function() {

  // ...

  it("n이 음수일 때 결과는 NaN입니다.", function() {
    assert.isNaN(pow(2, -1));
  });

  it("n이 정수가 아닐 때 결과는 NaN입니다.", function() {
    assert.isNaN(pow(2, 1.5));
  });

});

기존에 n이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에, 새로 추가한
테스트는 실패합니다. 이제 실패한 테스트를 통과할 수 있게 코드를 수정해보겠습니다.


❤️ 정수가 아니거나 음수인 경우 NaN 반환

function pow(x, n) {		// x : 제곱의 대상, n : x를 제곱하는 수
  
  if (n < 0) return NaN;	// n이 음수라면 NaN 반환
  if (Math.round(n) != n) return NaN;	// (*) n이 소수점이라면 NaN 반환

  let result = 1;

  for (let i = 0; i < n; i++) {
    result *= x;
  }

  return result;
}


/* */
(*)
if (Math.round(n) != n) return NaN; 
math.round()는 소수점 이하를 반올림함.
n 값에 1.5가 들어오면, 1.5는 가장 가까운 짝수로 반올림 되므로 2가 됨
2 = 1.5 같지 않으므로, 조건이 참되어 NaN을 반환한다.

테스트가 통과되었습니다. 숫자n은 0보다 작은(음수)수와, 소수점이 있다면 NaN 값을 반환 합니다.



정리

BDD 에서는 스펙을 먼저 작성하고 이후에 코드를 작성합니다. 코드 작성이 끝난 이후에는
스펙과 실제 구현할 코드 둘 다 확보할 수 있습니다.

스펙의 용도는 크게 3가지 입니다.

1. 테스트 : 함수가 의도하는 동작을 제대로 수행하고 있는지 비교한다.
2. 문서 : 함수가 어떤 동작을 수행하고 있는지 설명해준다.
( describe와 it에 설명이 들어감 )
3. 예시 : 실제 동작하는 예시를 이용해서 함수를 어떻게 사용하고 있는지 알려준다.


명세서를 작성하면, 코드의 오류를 단번에 파악할 수 있고, 개선해 나갈 수 있다.
안정적인 코드 작성을 위해서 명세서 작성은 옵션이 아니라 필수로 사용합시다.


chai가 제공하는 다양한 assertion

assert.equal(value1, value2)

value1과 value2의 값이 동등한지 비교합니다. ( value1 == value2 )

assert.isNaN(value)

value가 NaN 값인지 확인 합니다.

assert.strictEqual(value1, value2)

value1와 value2의 값이 일치한지 비교합니다 ( value1 === value2 )

assert.isTrue(value)

값이 참과 일치한지 확인 합니다.

assert.isFalse(value)

값이 거짓과 일치한지 확인 합니다.


더 많은 assert는 이곳 에서 확인 바랍니다.

profile
프론트엔드 개발자가 되기 위한 학습 과정을 정리하는 블로그

0개의 댓글