Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

nock : Node.js HTTP mocking 라이브러리

개발할 때 TDD로 하지 않더라도 유닛테스트를 충실하게 짜는 편이다. 테스트를 짜면 자연히 코드 모듈화에 대해서 신경을 쓰게 되는데 순수한 이론에 따라 완전히 모듈을 외부 자원으로부터 격리하는 좀 좀처럼 쉽지 않고 무척 귀찮은 일이기도 하다. 그래서 웬만한 건 굳이 격리하지 않고 외부 지원을 사용해서 테스트하는 편이지만 외부 자원을 이용하는 게 더 어려운 경우가 종종 있다. 대표적인 경우가 Open API를 연동할 때이다. 소셜 로그인을 구현하거나 API를 쓸 때 토큰 설정도 해야 하는 등 내가 통제하지 못하는 부분의 결과를 받아서 사용해야 하므로 테스트하기가 쉽지 않다.

이런 테스트는 외부 요청을 모킹해서 사용하는 게 편한데 Node.js에서 쓸만한 HTTP 모킹 라이브러리를 알아보다가 발견한 게 Nock이다.

Nock

Nock은 위에 설명한 대로 HTTP 모킹 라이브러리이다. JavaScript에는 대표적인 모킹 라이브러리로 sinon이 존재한다.(사실 sinon은 모킹만 제공하는 건 아니다.) sinon도 매우 좋고 강력한 라이브러리이지만 내가 사용하려는 용도에 비해서 좀 과하게 느껴졌고 브라우저 자바스크립트에서 쓸 때는 별로 못 느꼈는데 Node.js에서 쓰려니 사용하기가 좀 불편하게 느껴졌다.(전에는 ajax 요청을 stub해서 사용하는 것만 주로 사용해서 그럴 수도 있다.)

내가 원한 건 기존에 내가 작성한 코드에서 외부 HTTP 요청을 보내는 코드가 있다. 테스트마다 상황이 약간씩 다르니 일부 테스트에서 이 외부 HTTP 요청을 모킹해서 HTTP 요청이 실제 해당 서버로 날아가는 것이 아니라 모킹한 서버에서 미리 정해놓은 응답을 받아서 테스트하는 것이다. 보통 이러면 외부 HTTP 요청에 응답 값을 테스트하는 게 아니라 테스트한 결과에 대한 로직을 테스트하려는 의도인데 이럴 경우 원하는 응답 값이 오도록 상황을 만드는 게 어렵거나 내가 제어를 할 수 없기 때문이다.

Nock은 딱 이 역할을 해주는 모킹 라이브러리다. 간단히 사용법을 보기 위해 다음 코드를 보자.

// src.js
var https = require('https');

module.exports = function(callback) {
  var options = {
    hostname: 'api.github.com',
    path: '/users/outsideris/events',
    headers: {
      'User-Agent': 'Awesome-Octocat-App'
    }
  };

  https.get(options, function(res) {
    var data = '';
    res.on('data', function(chunk) {
      data += chunk
    });
    res.on('end', function() {
      callback(null, eval(data));
    });
  });;
};

위와 같은 코드가 있다고 해보자. 단순히 https://api.github.com/users/outsideris/events에 GET 요청을 보낸 결과를 반환하는 함수이다. 이를 mocha로 다음과 같이 테스트를 작성할 수 있다.

// test.js
var assert = require('assert');
var github = require('./src');

describe('github', function() {
  it('should retrun timeline', function(done) {
    github(function(err, data) {
      assert.equal(data.length, 30);
      assert.equal(data[0].actor.login, 'outsideris');
      done();
    });
  });
});

이 테스트는 정상적으로 통과한다. 위 요청은 내 타임라인의 최근 30개를 배열로 반환하고 내 계정이므로 계정명도 같이 반환된 것을 볼 수 있다. 여기서 실제 Github에서 데이터를 받지 않고 결과 값을 다른 값을 받아야 한다고 해보자. 물론 이 정도 코드는 HTTP를 모킹하지 않더라도 다른 방법으로 테스트할 수는 있지만 보통 코드에서는 HTTP 요청 부분이 외부에 노출되어 있지 않아서 이를 처리하기 어려운 경우도 많다.

// test.js
var assert = require('assert');
var nock = require('nock');
var github = require('./src');

describe('github', function() {
  before(function() {
    nock('https://api.github.com')
      .get('/users/outsideris/events')
      .reply(200, [
        { actor: {login: 'test_user'}},
        { actor: {login: 'test_user'}}
      ]);
  });
  after(function() {
    nock.restore();
  });

  it('should retrun timeline', function(done) {
    github(function(err, data) {
      assert.equal(data.length, 2);
      assert.equal(data[0].actor.login, 'test_user');
      done();
    });
  });
});

비즈니스 로직은 수정하지 않고 테스트 코드만 수정했다. nock 모듈을 추가하고 before에서 nock으로 HTTP를 모킹하고 다른 테스트에는 영향을 주지 않도록 after에서 모킹한 부분을 롤백했다.

nock('https://api.github.com')
  .get('/users/outsideris/events')
  .reply(200, [
    { actor: {login: 'test_user'}},
    { actor: {login: 'test_user'}}
  ]);

모킹하는 코드만 보면 위와 같이 생겼다. nock객체에 HOST 명을 지정하고 체이닝으로 연결할 수 있다. 그래서 위 코드는 /users/outsideris/events의 GET 요청을 모킹해서 200 OK와 함께 위에 reply에서 두 번째 파라미터로 전달한 객체를 응답으로 돌려보낸다. 그래서 위 테스트 코드를 실행하면 아까와는 달리 모킹한 응답이 반환되고 테스트가 성공한다.

체이닝

nock('https://api.github.com')
  .get('/path/to/caseA')
  .reply(200, [
    { actor: {login: 'test_user'}},
    { actor: {login: 'test_user'}}
  ])
  .post('/path/to/caseA')
  .reply(200, 'success')
  .get('/path/to/caseB')
  .reply(200, {id: 483904, name: 'Outsider'});

nock은 위처럼 체이닝을 할 수 있다. 그래서 여러 가지 경로에 대해서 모킹을 할 수 있고 API가 상당히 직관적이라 쓰기 편하다. HTTP Verb(get, post...)와 reply는 쌍으로 연결된다. 헤더를 지정하고 싶다면 .reply(200, 'success', {'X-My-Headers': 'My Header value'})처럼 세 번째 파라미터로 헤더 객체를 전달하면 된다.

경로 필터링

HTTP를 모킹하는 라이브러리가 대부분 그렇듯이 쿼리스크링을 포함한 HTTP 경로가 일치하는 요청만 모킹한다. 그래서 http://example.com/user?id=aaaaa&token=fjeofjeohttp://example.com/user?id=bbbbb&token=vmldjfld는 다르게 취급한다. 보통 로직을 따면 쿼리스트링에 들어가는 부분을 상황에 따라 달라지므로 모킹하는 단계에서는 이 부분이 꽤 피곤하게 느껴진다. 즉, 쿼리스트링은 상관없이(혹은 일부만) http://example.com/user로 오는 요청은 모두 모킹하고 싶은 경우가 더 일반적인 것 같다. 이런 경우를 위해서 filteringPath를 제공한다.

nock('https://api.github.com')
  .filteringPath(/\?.*/g, '?dummy=true')
  .get('/path/to/caseA?dummy=true')
  .reply(200, [
    { actor: {login: 'test_user'}},
    { actor: {login: 'test_user'}}
  ])

filteringPath는 위처럼 정규식을 사용하고 JavaScript의 replace와 비슷하게 생각하면 된다. 경로에서 정규식으로 검사해서 일치하는 패턴을 두 번째 파라미터의 문자열로 대치해버린다. 경로나 쿼리스트링에서 무시하고 싶은 부분이 있다면 해당 부분을 정규식으로 잡아서 치환하면 들어오는 요청의 해당 부분을 치환한 다음에 모킹하는 URL과 비교를 하게 된다. filteringPath외에도 호스트를 필터링할 수 있는 filteringScope나 요청바디를 치환할 수 있는 filteringRequestBody도 존재한다.


그 밖에도 응답시간에 지연을 주거나 모킹할 회수를 지정하는 등의 기능을 지원하고 있다. 간단히 HTTP 요청을 모킹해서 사용하기에 무척 편해서 앞으로 Node.js 테스트를 작성할 때 많이 사용할 것 같다.

2014/06/28 23:19 2014/06/28 23:19