NodeJS에서 가장 많이 사용하는 테스트 프레임웍, Mocha

서버와 웹프론트를 개발하면서 테스트는 항상 아쉽다. 프로젝트 초기에는 TDD 방법론으로 개발을 시작한다.

  1. 실패하는 테스트 유닛을 만든다.
  2. 함수 로직을 만든다.
  3. 테스트에 성공한다.

각 기능을 테스트 가능한 단위로 쪼개어 개발하는 과정 자체에 매력을 느낀다. 아마도 테스트 유닛을 통과하기만 하면 된다는 심리적 안정감도 선호에 영향을 주는 것 같다. 테스트 없이 개발하면 그물망 없는 막타워를 뛰어 내리는 심정이다. 코드 한 줄 한 줄이 부담스럽다.

그러나 요구사항이 수시로 변경되고 프로젝트 일정이 눈에 보이기 시작하면 테스트 코드 없이 바로 로직 구현을 하는 경우가 태반이다. 물론 신속히 개발할 수 있지만 나중에 유지보수 때 문제가 발생한다. 유지보수라고 하지만 새로운 기능을 요구할 때도 있다. 기존 로직에 영향을 주지 않고 코드를 작성해야 하는데 테스트 코드없이 구현하다보면 사이드 이펙트가 여기 저기서 터진다. 결국엔 유지보수 기간 중에 기존 코드에 대한 테스트 코드를 다시 작성하는 사태가 벌어진다. 일이 겁나 많아 진다.

NodeJS로 서버를 개발하면서 Mocha는 가장 많이 애용하는 테스트 툴이다. 노드의 인기가 한참인 지금에서야 포스팅을 작성한다는게 늦은 것 같기도 하다. 그러나 정리하는 차원으로 Mocha 사용법에 대해 알아보자.

설치

$ npm install mocha --save-dev

package.json에 스크립트를 추가해 두면 편리하다. 테스트 코드는 .spec.js 파일로 작성하고 이 파일들에 대해 감시 옵션(-w)을 추가해서 실행한다. 파일이 변경될 때마다 자동으로 테스트 러너가 실행될 것이다.

"scripts": {
  "test": "node_modules/.bin/mocha $(find ./ -name '*.js') --recursive -w"
},

테스트 코드

describe()으로 테스트 suite을 만들고 그 안에 it()으로 테스트 코드를 작성한다. descirbe()은 중첩해서 사용할 수 있다.

describe("Test suite", function () {
  it("should be ok", function () {
    assert.equal(true, false)
  })
})

테스트는 물론 실패한다.

Test suite
  1) should be ok

0 passing (1ms)
1 failing

1) Test suite should be ok:

    AssertionError: true == false
    + expected - actual

    -true
    +false

후커

테스트 코드 전/후에 후커를 실행할 수 있다. before()/after()는 테스트 suite 시작 전/후 한 번씩 실행된다. beforeEach()/afterEach()는 테스트 suite 안에 정의한 모든 테스트 코드 실행 전/후마다 실행된다. 테스트에 필요한 자료를 데이터베이스에 입력하거나 삭제하는 등 테스트 코드 실행 전/후에 실행할 로직 작성시 사용한다.

describe("Test suite", function () {
  var arr

  before("Create the array", function () {
    arr = [0, 1, 2]
  })

  after("Destory the array", function () {
    arr = undefined
  })

  it("should be ok", function () {
    assert.equal(arr[0], 0)
  })
})

후커 사용시 동작에 유념할 필요가 있다. before()는 각 테스트 슈트(describe)와 테스트 케이스(it)마다 한번씩 호출되는 반면 beforeEach()는 각 테스트 케이스마대 매번 실행되는 점이 다르다. 아래 코드를 보고 결과를 한번 예측해 보자.

describe("suite1", () => {
  before("before1", () => console.log("before1")) // 1
  beforeEach("beforeEach1", () => console.log("beforeEach1")) // 3, 5

  describe("suite2", () => {
    before("before2", () => console.log("before2")) // 2
    beforeEach("beforeEach2", () => console.log("beforeEach2")) // 4, 6

    it("test1", () => console.log("test1"))
    it("test2", () => console.log("test2"))
  })
})

결과는 주석에서 실행순서를 달아 놓았지만 아래와 같다.

suite1
  before1
  suite2
    before2
    beforeEach1
    beforeEach2
    test1
      ✓ test1
    beforeEach1
    beforeEach2
    test2
      ✓ test2

Exclusive

테스트 코드를 작성한만큼 실행시간이 길어지는 것은 당연하다. 모든 테스트를 구동하지 않고 하나의 테스트 유닛만 실행하고 싶은 경우 각 함수에 only() 메써드를 사용하면 된다.

describe("...", function () {
  // 오직 이 테스트만 수행됨
  describe.only("Only this test suite will be run", function () {
    it("...", function () {})
  })

  describe("...", function () {
    it("...", function () {})

    it("...", function () {})
  })
})

only() 대신 skip() 메서드를 사용하면 해당 테스트 suite만 실행하지 않고 넘어간다.

비동기

자바스크립에는 비동기 코드가 많다. 이를 테스트하기 위해 done() 함수를 파라메터로 넘겨 사용한다. 아래는 1초 후에 동작하는 비동기 코드를 테스트한 경우다. assert한 뒤 파라메터로 넘어온 done() 콜백함수를 실행하면 테스트가 종료된다.

describe("Test suite", function () {
  it("should be ok", function (done) {
    setTimeout(function () {
      assert.equal(0, 0)

      done() // 비동기 테스트 종료
    }, 1000)
  })
})

모카는 기본적으로 2초 내로 테스트를 완료하도록 한다. 만약 이를 초과하면 아래와 같은 에러 메시지를 출력할 것이다.

Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

timeout() 메소드는 테스트 수행 시간을 설정하는데 사용한다.

describe("Test suite", function () {
  // 이 테스트 suite을 5초 내로 수행함
  this.timeout(5000)

  it("should be ok", function (done) {
    setTimeout(function () {
      // 3초후 비동기 코드가 실행됨
      assert.equal(0, 0)
      done()
    }, 3000)
  })
})