Outsider's Dev Story

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

Node.js에서 rewire를 이용한 의존성 주입

테스트를 작성할 때 테스트를 쉽게 작성하고 다른 요소에 영향을 받지 않도록 테스트를 격리해야 할 필요가 있다. 외부 리소스를 격리해야 하는 경우도 있고 소스가 다른 소스에 의존성을 가지는 경우 테스트 대상만 격리하는 경우도 있다. Node.js에서 이러한 요구사항을 지원해 주는 라이브러리가 여러 가지 있는데 HTTP를 모킹해야 하는 경우에는 nock을 쓰고 소스의 의존성을 모킹하는 경우에는 proxyquire를 쓰고 있었다. proxyquire를 잘 쓰고 있었지만, 최근에 rewire로 갈아탔다.

rewire

rewire는 작년 playnode 컨퍼런스에서 겨미겨미님의 발표에서 처음 알게 되었다. 그 이전에는 몰랐던 모듈이라 호기심이 생겼지만, 기존에 proxyquire를 잘 쓰고 있었기에 갈아타지 않다가 proxyquire로는 안되는 부분을 해결하기 위해서 사용했다가 편해서 완전히 rewire로 갈아탔다. rewire는 유닛테스트를 위해서 소스에 의존성을 주입해주는 모듈이다.

사용법을 알기 위해서 간단한 예제를 보자.

// src/a.js
module.exports = {
  random: function() {
    return Math.floor(Math.random()*100);
  }
};
// src/b.js
var a = require('./a');

module.exports = {
  addPrefix: function() {
    return 'prefix-' + a.random();
  }
};

위처럼 a.jsb.js 두 개의 파일이 있다고 해보자. a.js에는 임의의 수를 반환하는 random이라는 함수가 있고 b.js에는 이 random함수로 임의의 수를 받아서 앞에 접두사 prefix-를 붙여서 반환하는 addPrefix가 있다.(기능 자체는 의미가 없고 예제를 위한 코드이다.)

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

describe('Test', function() {
  it('prefix test', function() {
    assert.equal(b.addPrefix(), 'prefix-XX');
  });
});

addPrefix를 테스트하려면 위와 같은 테스트 코드를 작성해야 하는데 이 코드는 제대로 테스트를 할 수가 없다.

$ mocha

  Test
    1) prefix test

  0 passing (10ms)
  1 failing

  1) Test prefix test:

      AssertionError: 'prefix-93' == 'prefix-XX'
      + expected - actual

      -prefix-93
      +prefix-XX

      at Context.<anonymous> (test/b.test.js:6:12)

당연한 얘기지만 위 테스트를 실행한 결과에서 보듯이 prefix-뒤에 붙는 숫자가 실행마다 매번 달라지므로 비교하는 것이 불가능하다. 여기서는 간단한 예제 코드이므로 정규식 등으로 비교할 수는 있지만 a.jsrandom에서 받은 값에 접두사가 붙었는지(테스트하려는 로직) 확인하는 것은 불가능하다.

proxyquire와 마찬가지로 rewire는 이럴 때 사용한다.

// test/b.test.js
var assert = require('assert');
var rewire = require("rewire");
var b = rewire('../src/b');

describe('Test', function() {
  before(function() {
    b.__set__('a', {
      random: function() { return 11; }
    });
  });

  it('prefix test', function() {
    assert.equal(b.addPrefix(), 'prefix-11');
  });
});

rewire모듈을 설치한 뒤 테스트 코드에서 rewire를 불러오고 4번 줄처럼 테스트코드를 require() 대신 rewire로 불러온다. require와 비슷하므로 사용이 어렵지 않다. rewire로 가져오면 자동으로 __get____set__이라는 전용 메서드가 생기는 데 이 함수를 이용해서 b.js에서 사용하는 의존성을 바꿔치기할 수 있다. b.js에서 a라는 모듈을 사용하고 있으므로 8번 라인처럼 a를 새로운 객체로 바꿔치기하고 이 객체에서 random이라는 함수가 무조건 11이라는 숫자를 반환하도록 작성했다. 이제 테스트를 실행하면 다음과 같이 a.jsrandom함수 대신 테스트 코드에서 바꿔치기한 random함수가 실행되어 원하는 비교를 할 수 있다.

$ mocha

  Test
    ✓ prefix test

  1 passing (7ms)

__set__함수는 바꿔치기한 객체를 원래로 되돌리는 함수를 반환하므로 이 변수를 저장했다가 다음과 같이 함수를 실행하면 바꿔치기한 객체를 원래의 상태로 만들 수 있다.

describe('Test', function() {
  var rollback;
  before(function() {
    rollback = b.__set__('a', {
      random: function() { return 11; }
    });
  });

  after(function() {
    rollback();
  })

  it('prefix test', function() {
    assert.equal(b.addPrefix(), 'prefix-11');
  });
});

위처럼 잠시만 테스트를 위해서 객체를 바꿔치기한 후에 원래대로 돌리려면 다음과 같이 __with__를 사용할 수도 있다. 이 경우에는 __with__ 함수 내에서만 모킹이 적용되고 원래대로 돌아온다.

describe('Test', function() {
  it('prefix test', function() {
    b.__with__({
      a: {
        random: function() { return 11; }
      }
    })(function() {
      assert.equal(b.addPrefix(), 'prefix-11');
    });
  });
});

이 정도 기능은 proxyquire로도 할 수 있지만 proxyquire에 비해 사용방법이 훨씬 간단하고 편하다. 추가적인 예제를 하나 더 보자.

// src/b.js
var randomNumber = Math.floor(Math.random()*100);

module.exports = {
  addPrefix: function() {
    return 'prefix-' + randomNumber;
  }
};

이번에는 다른 코드에 의존성이 있는 것이 아니라 소스코드의 로컬 변수로 선언된 경우이다. 여기서는 간단한 임의의 수이지만 실제 코드에서는 API 키이거나 리소스의 위치, 상수 등 다양한 값을 이렇게 로컬 변수로 선언하게 된다. 사실 proxyquire는 이런 변수는 바꿔치기할 수가 없어서 테스트 포기하거나 테스트를 위해 변수를 억지로 외부로 노출해야 한다.

describe('Test', function() {
  before(function() {
    b.__set__('randomNumber', 11);
  });

  it('prefix test', function() {
    assert.equal(b.addPrefix(), 'prefix-11');
  });
});

하지만 rewire에서는 앞의 예제와 같이 소스코드의 로컬변수를 바로 바꿔치기 하면 된다. 테스트할 때 이 부분은 엄청나게 강력하다.

(function () {
    var someVariable;
})()

문서에 나와 있듯이 위처럼 함수 안에 있는 변수는 바꿔치기하는 것이 불가능하다.

2016/01/26 23:22 2016/01/26 23:22