최근에 올린 nock에 대한 글과 마찬가지로 객체나 함수의 다른 의존성을 줄여야 테스트하기 편하다. 의존성 주입 같은 방식으로 의존성을 외부에서 제어할 수 있게 구조를 만들 수도 있기는 하지만 Node.js에서 이렇게 하기는 좀 쉽지 않았다.(JavaScript에서 가장 깔끔한 DI는 Angular.js라고 생각하는데 프레임워크 차원으로 사용해야 하기도 하고 Angular.js는 브라우저 클라이언트 용이라...)
proxyquire
app.js
의 유닛테스트를 작성한다고 할 때 app.js
가 depencency.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.js
를 require()
로 불러와서 사용하였지만, 이번에는 대신 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.getRandomInteger
는 random.js
에서 임의의 정수를 반환하고 random.getRandomArbitrary
만 오버라이드된 함수를 사용하게 된다. 테스트에 필요한 함수나 값만 stub 해서 사용할 수 있고 필요할 때마다 proxyquire
를 사용해서 새로운 객체를 만들므로 다른 require
에는 영향을 주지 않아서 테스트를 더 깔끔하게 작성할 수 있다.
여기서는 기본적인 사용방법만 설명했으므로 좀 더 자세한 내용은 proxyquire의 README 문서를 참고해야 한다.
Comments