Outsider's Dev Story

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

proxyquire : Node.js의 require 의존성을 오버라이드할 수 있는 라이브러리

최근에 올린 nock에 대한 글과 마찬가지로 객체나 함수의 다른 의존성을 줄여야 테스트하기 편하다. 의존성 주입 같은 방식으로 의존성을 외부에서 제어할 수 있게 구조를 만들 수도 있기는 하지만 Node.js에서 이렇게 하기는 좀 쉽지 않았다.(JavaScript에서 가장 깔끔한 DI는 Angular.js라고 생각하는데 프레임워크 차원으로 사용해야 하기도 하고 Angular.js는 브라우저 클라이언트 용이라...)

proxyquire

app.js의 유닛테스트를 작성한다고 할 때 app.jsdepencency.js를 사용하고 있다면 테스트 코드가 복잡하게 되는 경우가 있다. 간단하다면 depencency.js를 사용하게 해서 테스트하는 게 가능하지만 때로는 너무 복잡해지거나 테스트할 수 없어지는 경우도 있다. 유닛테스트를 충실하게 작성했다면 depencency.js의 테스트코드를 작성했을 것이므로 이러면 app.js의 테스트코드는 app.js의 로직만 테스트하는 게 더 적절하다.

전에도 테스트할 때 이러한 불편함을 해결하려고 했었는데 생각보다 쉽지 않았다. Node.js는 require()를 이용해서 다른 모듈을 불러오는데 require를 할 때 Node.js가 자체적으로 캐싱을 해버리기 때문에 의존성 모듈을 조작할 경우 다른 테스트까지 영향을 미쳐서 처리하기가 어려웠다.

proxyquire는 이름에서 유추할 수 있듯이 require를 프락시 해주는 모듈이다. 테스트하는 객체가 require하는 모듈을 프락시할 수 있어서 테스트할 때 유용하다.

사용방법

다음과 같이 임의의 수를 반환하는 함수가 있다고 해보자.

// src/random.js
module.exports = {
  getRandomArbitrary: function(min, max) {
    console.log(Math.random() * (max - min) + min);
    return Math.random() * (max - min) + min;
  },
  getRandomInteger: function() {
    return Math.ceil(Math.random() * 100);
  }
};

이 모듈은 아래 target.js에서 사용하고 있다.

// src/target.js
var random = require('./random');

module.exports = {
  attachRandomNumber: function(str) {
    return str + random.getRandomArbitrary(0, 100);
  },
  attachRandomInteger: function(str) {
    return str + random.getRandomInteger();
  }
};

attachRandomNumber()함수는 전달한 문자열 뒤에 임의의 수를 이어 붙이는 함수이다. 이를 테스트하려고 다음과 같은 테스트 코드를 작성할 수 있다.

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

describe('target', function() {
  it('should attach random number', function() {
    // given
    var str = 'something';
    // when
    var result = target.attachRandomNumber(str);
    // then
    assert.equal(result, str + '0');
  });
});

이 테스트는 통과하지 않는다.(운이 좋으면 가끔은...) 이 테스트코드는 작성하기가 어려운데 random.js에서 임의의 수를 반환하기 때문에 최종 문자열이 테스트할 때마다 달라진다. 여기서 테스트하려는 것은 문자열 뒤에 임의의 수가 붙었는지를 보려는 것이지 임의의 수가 나왔는지는 확인할 필요가 없는데 이 때문에 테스트하기가 무척 어렵다. 이러면 proxyquire를 이용해서 객체를 stub으로 바꿔치기할 수 있다.

var assert = require('assert');
var proxyquire =  require('proxyquire');

describe('target', function() {
  var randomStub = {},
      target = proxyquire('../src/target', {
        './random': randomStub
      });
  randomStub.getRandomArbitrary = function() {
    return 0;
  };

  it('should attach random number', function() {
    // given
    var str = 'something';
    // when
    var result = target.attachRandomNumber(str);
    // then
    assert.equal(result, str + '0');
  });
});

여기서 중요한 부분은 5번 라인 부분이다. random모듈을 바꾸려는 것이므로 이를 위한 stub 객체를 만든다. 기존 테스트에서는 target.jsrequire()로 불러와서 사용하였지만, 이번에는 대신 proxyquire()를 이용해서 불러왔다. proxyquire()의 두 번째 파라미터로 stub 할 객체를 지정하면 된다. 이 객체에서 킷값이 require()하는 경로이고 값 부분에 stub 객체를 지정하면 된다. 주의점은 킷값의 경로가 일치해야 한다는 점이다. target.js에서 require('./random')와 같이 사용하고 있기 때문에 여기서도 './random': randomStub와 같이 한다. 이어서 stub 객체에서 대체할 함수를 작성해서 randomStub.getRandomArbitrary부분과 같이 오버라이드하면 된다. 이제 getRandomArbitrary()함수가 항상 0을 반환하므로 문제없이 테스트할 수 있다.

앞에서 random.js에 함수를 2개 만들어 넣었는데 proxyquire는 기존 객체를 오버라이드하는 방식이기 때문에 지정하지 않은 값은 원래 객체의 것을 그대로 사용하게 된다. 그래서 아래처럼 오버라이드 하지 않은 random.getRandomInteger를 사용하는 target.attachRandomInteger에 대한 테스트코드를 작성하면 다음과 같이 작성해야 한다.

var assert = require('assert');
var proxyquire =  require('proxyquire');

describe('target', function() {
  var randomStub = {},
      target = proxyquire('../src/target', {
        './random': randomStub
      });
  randomStub.getRandomArbitrary = function() {
    return 0;
  };

  it('should attach random number', function() {
    // given
    var str = 'something';
    // when
    var result = target.attachRandomNumber(str);
    // then
    assert.equal(result, str + '0');
  });

  it('should attach integer number', function() {
    // given
    var str = 'something';
    // when
    var result = target.attachRandomInteger(str);
    // then
    var regexp = new RegExp('^' + str + '[0-9]{1,3}$');
    assert.ok(regexp.test(result));
  });
});

위 테스트는 모두 통과하는데 random.getRandomIntegerrandom.js에서 임의의 정수를 반환하고 random.getRandomArbitrary만 오버라이드된 함수를 사용하게 된다. 테스트에 필요한 함수나 값만 stub 해서 사용할 수 있고 필요할 때마다 proxyquire를 사용해서 새로운 객체를 만들므로 다른 require에는 영향을 주지 않아서 테스트를 더 깔끔하게 작성할 수 있다.

여기서는 기본적인 사용방법만 설명했으므로 좀 더 자세한 내용은 proxyquire의 README 문서를 참고해야 한다.

2014/06/29 23:36 2014/06/29 23:36