개발할 때 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=fjeofjeo
와 http://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 테스트를 작성할 때 많이 사용할 것 같다.
깔끔하네요 ㅋㅋ
typo : 장성한 -> 작성한
맞춤법검사기를 돌려도 너의 인간 맞춤법검사기는 매번 못피해가는구나.. 땡큐.. ㅋㅋ