Jello's development blog

Jello's development blog

Node.js Express 테스트 하기

들어가며

최근에 토이 프로젝트의 서버를 Node.js의 Express로 만들었는데, 전에 Django로 진행했던 프로젝트에서와는 다르게, Node.js에서 테스트 코드를 작성하는 것은 또 다른 도전이었다. 처음으로 Node.js에서의 테스트 코드를 작성하면서 내 나름대로 찾아보고 고심한 뒤에 얻게된 것을 정리해보려고 한다.

Mocha, Should.js

MochaShould.js는 각 각 node.js의 테스트 프레임워크, 라이브러리이다. 이 글에서는 Express를 Mocha와 Should.js를 이용하여 테스트하는 방법을 설명할 것이다.
Should.js, Mocha에 관한 글


테스트

TDD 방식으로 유닛 테스트 코드를 작성하기 전에, Sinon.js를 이용하여 Sequelize ORM을 mocking하는 작업이 필요하므로, express의 라우터를 먼저 작성하여 어떤 로직으로 돌아가는지 살펴볼 필요가 있다. 물론 mocking을 알게 된 뒤에는 자신이 행하던 개발방식에 따라서 코드의 우선순위를 조정해야 한다.

먼저 List의 id를 받아서 Sequelize로 가져온 뒤에 사용자에게 보내주는 간단한 라우터를 작성해보자.

var getList = function(req, res) {
    db.list.findById(req.params.id).then(function (list) {
        res.json(list);
    }).catch(function(err) {
        res.status(400).send(err.message);
    });
};

router.get('/', getList);

module.exports = {
    getList: getList
};

예전에 나는 라우터를 작성할 때에, Anonymous function으로 바로 router.get 함수에 포함시켜 버리곤 했다. 아래처럼 말이다.

router.get('/', function(req, res) {
    db.list.findById(req.params.id).then(function (list) {
        res.json(list);
    }).catch(function(err) {
        res.status(400).send(err.message);
    });
});

하지만 이러한 방식은 일회성으로, 함수를 한 번 밖에 사용하지 못하게 되어 테스트 코드 작성 시에 라우터 함수를 가져올 수 없게 된다. 따라서 코드를 테스트에 재사용하기 위해서는 처음의 코드처럼 변수로 만들어서 export 해줘야한다.

테스트 코드를 작성해보자. Mocha에서는 다음과 같은 테스트 구조를 지원한다.

describe('name', function() {
  before(function() {
    // excuted before test suite
  });

  after(function() {
    // excuted after test suite
  });

  beforeEach(function() {
    // excuted before every test
  });

  afterEach(function() {
    // excuted after every test
  });
  
  describe('#example', function() {
    it('this is a test.', function() {
      // write test logic
    });
  });
});

before은 테스트가 시작하기 전에, after은 모든 테스트가 끝난 후에, beforeEachafterEach는 각 테스트 시작 직전, 직후에 실행된다. 따라서 Mocha는 before -> (beforeEach -> it -> afterEach 반복) -> after 순으로 테스트를 수행한다.

각 테스트를 실행시키기 전에, 라우터에 파라미터로 넘겨 줄 reqres를 인위적으로 만들어야(mock) 한다. node-mocks-http라는 패키지가 가짜 request와 response를 만들어준다.

beforeEach(function() {
    req = httpMocks.createRequest();
    res = httpMocks.createResponse({
        eventEmitter: myEventEmitter
    });
});

response를 생성할 때에, eventEmitter라는 옵션을 넣어주면, response가 데이터를 보낼 때(send(), json() 등) 해당 eventEmitter가 ‘end’라는 이벤트를 호출한다. 반드시 넣어줘야 비동기로 처리되는 라우팅 함수의 종료 시점을 알 수 있다. eventEmitterevents패키지에 포함되어 있다.

또한, 우리는 테스트가 수행되면서 라우터의 DB I/O의 발생으로 속도가 느려지는 것을 원하지 않는다. 따라서 여러 가지 함수들 또한 흉내내주어야(mock) 한다. 함수를 흉내내는 것은 sinon이라는 패키지를 사용하면 된다.

var sandbox;
before(function() {
    sandbox = sinon.sandbox.create();
    sandbox.stub(db.list, 'findById', function() {
        return new Promise(function(resolve, reject) {
            resolve({
                id: 1,
                name: "test list"
            });
        });
    });
});
after(function() {
    sandbox.restore();
});

sinon.js는 sandbox라는 가상의 공간을 제공하고, 우리는 그 sandbox안에 stub으로 흉내낼 함수의 이름과 내용을 정의할 수 있다. 위의 코드는 db.listfindById라는 함수를, 정해진 오브젝트를 resolve하는 promise를 반환하는 함수로 흉내내도록 한 것이다. 따라서 db.list.findById라는 함수는 restore()를 만나기 전까지는, 무조건 promise의 결과로, id가 1이고, name이 test list인 객체를 반환하게 된다. 이제 테스트의 알맹이인 it함수를 작성해보자.

it("should send the number of rows and lists' info", function(done) {
    res.on('end', function() {
        var resData = JSON.parse(res._getData());

        res.statusCode.should.be.equal(200);
        resData.should
            .be.instanceOf(Object)
            .and.have.properties(['id', 'name']);
        resData.id.should.be.equal(1);
        resData.name.should.be.equal("test list");
        done();
    });

    list.getList(req, res);
});

node-mocks-http 패키지와 eventEmitter를 이용해서 만들어진 response에 end이벤트 리스너를 걸어주었다. resgetList함수 안에서 데이터나 status를 클라이언트에게 send하는 함수가 나오면 end이벤트를 발생시킨다. 즉 getList의 경우에는, 에러가 났을 시 res.send(err.message), 정상적으로 작동 시에 res.json(list)에서 end이벤트가 발생된다. 따라서 이벤트 리스너 안에 assertion 코드를 작성하여야 한다. 그렇지 않으면 javascript의 비동기 특성 상 getList함수가 끝나지도 않았는데 실행될 가능성이 높기 때문이다.

it함수에 파라미터로 준 익명 함수의 done을 살펴보자. 이는 테스트를 async(비동기)로 실행할 경우에 파라미터로 줄 수 있는 선택사항이다. 익명 함수에 done을 파라미터로 주면, it함수는 done()을 만나기 전까지 테스트를 종료시키지 않는다. 만약 done()을 작성해주지 않는다면 테스트는 끝나지 않고 done()을 만날때까지 기다리다가 시간이 초과되어 실패 테스트가 되어버린다.

시간이 초과되어 실패한 테스트

이는 it 뿐만 아니라 before, after, beforEach, afterEach에도 해당된다.

테스트 결과

테스트는 package.json을 설정한 뒤에, npm test로 실행한다.(Mocha로 테스트 실행하기) 다음은 테스트 결과의 예이다.

테스트 결과의 예

초록색 체크표시나 붉은 글씨로 된 것이 it에 작성된 테스트의 이름이고, 상위 레벨로 된 흰색 글씨가 describe에 작성된 테스트 묶음 이름이다. 테스트를 목적에 따라 잘 나누어서 체계적으로 관리할 수 있다.