Hapi 테스트 코드

프론트도 마찬가지지만 백엔드 서버를 개발할수록 테스트가 중요성을 실감한다. 서비스는 서버 혼자서 동작하는 것이 아니라 모바일, 웹 브라우져 등의 클라이언트와 함께 동작해야 하기 때문에 다양한 시나리오에 대응할 수 있어야 한다. 또한 한 번 발생한 버그는 재발하지 않는다는 것을 보장해야 마음 놓고 코드를 수정할 수 있다. 때문에 테스트 뿐만 아니라 테스트 규모가 커짐에 따라 이에 대한 테스트 자동화에 대해서도 고민하지 않을 수 없다.

이번 글은 Hapi를 이용한 Api 서버 개발시 테스트 자동화가 그 주제다.

  • Hapi에서 제공하는 유닛테스트 모듈인 Lab
  • 검증모듈 Code,
  • 이를 활용한 REST Api 단에서의 테스트 코드 작성법,
  • 그리고 테스트 결과의 문서화까지의 테스트 과정에 대해 보자.

라우팅 테스트

유닛 테스트라고 하면 클래스, 함수 단위 등 하나의 코드 덩어리를 테스트하는 것을 말한다. Api 서버에서도 클래스와 함수가 있기 때문에 그러한 모듈단위의 유닛테스트가 가능하다. 그러나 본 글에서는 유닛테스트가 아닌 프로토콜 단위의 테스트 코드를 다룬다. API 서버의 궁극적인 목적은 프로토콜을 호출했을 때 예상되는 결과로 응답하는 것이기 때문에 이러한 프로토콜 단위의 테스트가 중요하다고 생각한다.

지금까지 작성한 코드의 폴더구조를 살펴보자. 라우팅 단위로 폴더를 작성하고, 각 폴더에는 index.js *.ctrl.js, *.valid.js가 각각 존재한다. 테스트를 위해 *.spec.js 파일을 추가하자. 다시 한 번 각 파일의 역할을 설명하면 아래와 같다.

  • index.js: 라우팅 설정
  • *.ctrl.js: 로직 구현
  • *.valid.js: 파라매터 검증
  • *.spec.js: 테스트 코드

이전 포스트에서 구현한 /user 프로토콜에 대한 테스트 코드를 구현해 보자.

Hapi 유닛테스트 모듈인 Lab 과 검증 모듈 Code 를 사용해서 구현한다. Hapi에서는 유닛 테스트에 언급하는 Test Suite를 "experiment", Test Case는 "test"라고 부른다. 중첩 experiment를 가질 수 있고 테스트 전략에 따라서 테스트 케이스를 구조화 할 수 있다.

"use strict"

var Code = require("code") // 검증 모듈
var Lab = require("lab") // 유닛 테스트 모듈
var lab = (exports.lab = Lab.script())
var path = require("path")

// 서버 모듈
// 서버 객체를 할당한 부분에서 module.exports = server 코드를 추가하여
// 서버 객체를 노출시켜서 사용해야 한다.
var app = require(path.join(__dirname, "../../index.js"))

// 테스트 suite
lab.experiment("/users", function () {
  // 테스트 케이스
  lab.test("GET", function (done) {
    var opts = {
      method: "GET",
      url: "/users",
    }

    // 서버 구동
    app.inject(opts, function (res) {
      // Code 검증 모듈로 결과값을 검증한다.
      Code.expect(res.statusCode).to.be.equal(200)
      Code.expect(res.result).contain("users")
      Code.expect(res.result.users).to.be.an.array()

      // 비동기 코드이므로 done() 콜백함수로 종료를 알린다.
      done()
    })
  })
})

위 코드에서는 우리가 구현한 서버 객체를 require() 함수로 불러온다. 기존의 서버 생성 코드에 server 객체를 노출시키는 코드 추가하자.

var Hapi = require("hapi")

/* 서버 객체 생성 및 설정 */

// 서버 객체를 노출한다.
module.exports = server

작성한 테스트 코드를 아래 명령어로 실행해보자. 콘솔 화면을 통해 테스트 결과를 확인할 수 있다.

$ node_modules/lab/bin/lab users.spec.js

깔끔한 테스트를 위한 작업

Api 서버 테스트를 여러번 수행할수록 서버에는 테스트 데이터가 쌓이게 된다. 예를 들어 POST 프로토콜을 테스트 할 경우 데이터베이스에 데이터를 저장한다면 데이터가 계속 쌓이게 된다. 어차피 개발서버에서 테스트 한다면 상관없을 수도 있으나 테스트 후에는 그 흔적이 남지 않아야 한다고 생각한다. 이번 테스트 뿐만 아니라 다른 테스트에 영향을 미칠수 있기 때문이다.

Lab 모듈에서는 before()after() 함수를 지원한다. test()로 테스트 케이스를 진행하기 전에 뭔가 작업을 하고 테스트 종료후 마무리 작업을 위한 함수다. before()에선 POST 프로토콜로 테스트 데이터를 입력하고 after()에서는 DELETE 프로토콜로 테스트 데이터를 삭제한다.   test()에서는 GET, PUT 프로토콜을 테스트한다. before()/after()experiment()에서 각 각 한번만 실행된다. 매 테스트마다 실행하기 위한 beforeEach()/afterEach()함수도 지원한다.

"use strict"

var Code = require("code")
var Lab = require("lab")
var lab = (exports.lab = Lab.script())
var path = require("path")
var app = require(path.join(__dirname, "../../index.js"))

lab.experiment("/users", function () {
  // 테스트 데이터
  var tester = {
    name: "Unit Tester",
  }

  // 테스트 전에 post로 테스트 자원을 입력하는 프로토콜을 호출한다.
  lab.before(function (done) {
    var opts = {
      method: "POST",
      url: "/users",
      payload: {
        name: tester.name,
      },
    }
    app.inject(opts, function (res) {
      Code.expect(res.statusCode).to.be.equal(201)
      tester.id = res.result.users.indexOf(tester.name)
      done()
    })
  })

  // 테스트 종료 후 테스트 자원을 삭제하는 프로토콜을 호출한다.
  lab.after(function (done) {
    var opts = {
      method: "DELETE",
      url: "/users?id=" + tester.id,
    }
    app.inject(opts, function (res) {
      Code.expect(res.statusCode).to.be.equal(200)
      done()
    })
  })

  lab.test("GET", function (done) {
    var opts = {
      method: "GET",
      url: "/users",
    }
    app.inject(opts, function (res) {
      Code.expect(res.statusCode).to.be.equal(200)
      Code.expect(res.result).contain("users")
      Code.expect(res.result.users).to.be.an.array()

      // before()에서 입력한 테스트 데이터를 확인한다.
      Code.expect(res.result.users[tester.id]).to.be.equal(tester.name)

      done()
    })
  })

  lab.test("GET users/{id}", function (done) {
    var opts = {
      method: "GET",
      url: "/users/" + tester.id,
    }
    app.inject(opts, function (res) {
      Code.expect(res.statusCode).to.be.equal(200)

      // before()에서 입력한 테스트 데이터를 확인한다.
      Code.expect(res.result.user).to.be.equal(tester.name)
      done()
    })
  })
})

만약 테스트가 많을 경우 한 테스트 케이스만 실행할 필요가 있다. 반대로 특정 테스트는 스킵하고 싶을 때가 있는데 lab.test() 함수의 두 번째 파라메터로 {only: Boolean, skip: Boolena} 객체를 넘겨주면 가능하다.

테스트 파일이 많아지면 매번 명령어로 실행하기 불가능하다. 리눅스에 많이 사용하는 Makefile를 활용하면 이를 손쉽게 관리할 수 있다.

all: users auth

users:
  node_modules/lab/bin/lab app/routes/users/*.spec.js

auth:
  node_modules/lab/bin/lab app/routes/auth/*.spec.js

/users 프로토콜을 테스트할 때는 make user로 간단히 실행한다. make 명령어는 전체 테스트를 실행한다. Makefile은 프로젝트 루트폴더에 관리하는 것이 좋고, 스페이스가 아니라 반드시 '탭'을 사용해야 한다는 점을 주의하자.

테스트 결과

Lab 모듈은 훌륭한 테스트 결과 문서를 제공한다. Lab 명령어 실행시 -r 옵션은 리포트 형식을 정하는 옵션이다. -r html 옵션을 줘서 html 형식으로 테스트 결과를 출력하고 -o report.html 옵션을 추가하여 파일로 저장할 수 있다. Makefile을 아래처럼 수정해 보자.

users:
  node_modules/lab/bin/lab -o report.html -r html app/routes/users/*.spec.js

Grunt로 자동화

지금까지의 작업을 Grunt로 자동화 해보자. 우리는 grunt test 단 한줄의 명령어로 테스트 실행, 리포트 생성 및 생성된 리포트 열람의 작업을 수행할 수 있다. 아래 명령어로 grunt 관련 모듈을 설치한다.

npm install --save-dev grunt grunt-lab grunt-open

프로젝트 루트 폴더에 Gruntfile.js를 생성하고 아래 코드를 작성한다.

module.exports = function (grunt) {
  grunt.initConfig({
    // lab 명령어 관련 설정
    lab: {
      files: ["app/**/*.spec.js"], // 모든 테스트 파일을 수행한다.
      nodeEnv: "development",
      DEBUG: "*",
      color: true,
      verbose: true,
      timeout: 7000,
      parallel: false,
      reportFile: "report.html", // 리포트 파일을 생성한다.
      reporter: "html",
      coverage: true,
    },

    // 생성한 리프포트 파일을 크롬 브라우저로 열람한다.
    open: {
      report: {
        path: "report.html",
        app: "Google Chrome",
      },
    },
  })

  grunt.loadNpmTasks("grunt-lab")
  grunt.loadNpmTasks("grunt-open")

  grunt.registerTask("test", ["lab", "open:report"])
}

이제 grunt test 명령어를 수행해보자. 작성한 모든 테스트가 실행되고, report.html에 결과를 저장한 뒤, 크롬 브라우져가 열릴 것이다.

전체 코드: https://github.com/jeonghwan-kim/hapi_study/tree/09_test