개발할 때 TDD로 하지 않더라도 유닛테스트를 충실하게 짜는 편이다. 테스트를 짜면 자연히 코드 모듈화에 대해서 신경을 쓰게 되는데 순수한 이론에 따라 완전히 모듈을 외부 자원으로부터 격리하는 좀 좀처럼 쉽지 않고 무척 귀찮은 일이기도 하다. 그래서 웬만한 건 굳이 격리하지 않고 외부 지원을 사용해서 테스트하는 편이지만 외부 자원을 이용하는 게 더 어려운 경우가 종종 있다. 대표적인 경우가 Open API를 연동할 때이다. 소셜 로그인을 구현하거나 API를 쓸 때 토큰 설정도 해야 하는 등 내가 통제하지 못하는 부분의 결과를 받아서 사용해야 하므로 테스트하기가 쉽지 않다.
이런 테스트는 외부 요청을 모킹해서 사용하는 게 편한데 Node.js에서 쓸만한 HTTP 모킹 라이브러리를 알아보다가 발견한 게 Nock이다.
Nock
Nock은 위에 설명한 대로 HTTP 모킹 라이브러리이다. JavaScript에는 대표적인 모킹 라이브러리로 sinon이 존재한다.(사실 sinon은 모킹만 제공하는 건 아니다.) sinon도 매우 좋고 강력한 라이브러리이지만 내가 사용하려는 용도에 비해서 좀 과하게 느껴졌고 브라우저 자바스크립트에서 쓸 때는 별로 못 느꼈는데 Node.js에서 쓰려니 사용하기가 좀 불편하게 느껴졌다.(전에는 ajax 요청을 stub해서 사용하는 것만 주로 사용해서 그럴 수도 있다.)
내가 원한 건 기존에 내가 작성한 코드에서 외부 HTTP 요청을 보내는 코드가 있다. 테스트마다 상황이 약간씩 다르니 일부 테스트에서 이 외부 HTTP 요청을 모킹해서 HTTP 요청이 실제 해당 서버로 날아가는 것이 아니라 모킹한 서버에서 미리 정해놓은 응답을 받아서 테스트하는 것이다. 보통 이러면 외부 HTTP 요청에 응답 값을 테스트하는 게 아니라 테스트한 결과에 대한 로직을 테스트하려는 의도인데 이럴 경우 원하는 응답 값이 오도록 상황을 만드는 게 어렵거나 내가 제어를 할 수 없기 때문이다.
Nock은 딱 이 역할을 해주는 모킹 라이브러리다. 간단히 사용법을 보기 위해 다음 코드를 보자.
위와 같은 코드가 있다고 해보자. 단순히 https://api.github.com/users/outsideris/events
에 GET 요청을 보낸 결과를 반환하는 함수이다. 이를 mocha로 다음과 같이 테스트를 작성할 수 있다.
이 테스트는 정상적으로 통과한다. 위 요청은 내 타임라인의 최근 30개를 배열로 반환하고 내 계정이므로 계정명도 같이 반환된 것을 볼 수 있다. 여기서 실제 Github에서 데이터를 받지 않고 결과 값을 다른 값을 받아야 한다고 해보자. 물론 이 정도 코드는 HTTP를 모킹하지 않더라도 다른 방법으로 테스트할 수는 있지만 보통 코드에서는 HTTP 요청 부분이 외부에 노출되어 있지 않아서 이를 처리하기 어려운 경우도 많다.
비즈니스 로직은 수정하지 않고 테스트 코드만 수정했다. nock 모듈을 추가하고 before
에서 nock으로 HTTP를 모킹하고 다른 테스트에는 영향을 주지 않도록 after
에서 모킹한 부분을 롤백했다.
모킹하는 코드만 보면 위와 같이 생겼다. nock객체에 HOST 명을 지정하고 체이닝으로 연결할 수 있다. 그래서 위 코드는 /users/outsideris/events
의 GET 요청을 모킹해서 200 OK와 함께 위에 reply
에서 두 번째 파라미터로 전달한 객체를 응답으로 돌려보낸다. 그래서 위 테스트 코드를 실행하면 아까와는 달리 모킹한 응답이 반환되고 테스트가 성공한다.
체이닝
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
를 제공한다.
filteringPath
는 위처럼 정규식을 사용하고 JavaScript의 replace와 비슷하게 생각하면 된다. 경로에서 정규식으로 검사해서 일치하는 패턴을 두 번째 파라미터의 문자열로 대치해버린다. 경로나 쿼리스트링에서 무시하고 싶은 부분이 있다면 해당 부분을 정규식으로 잡아서 치환하면 들어오는 요청의 해당 부분을 치환한 다음에 모킹하는 URL과 비교를 하게 된다. filteringPath
외에도 호스트를 필터링할 수 있는 filteringScope
나 요청바디를 치환할 수 있는 filteringRequestBody
도 존재한다.
그 밖에도 응답시간에 지연을 주거나 모킹할 회수를 지정하는 등의 기능을 지원하고 있다. 간단히 HTTP 요청을 모킹해서 사용하기에 무척 편해서 앞으로 Node.js 테스트를 작성할 때 많이 사용할 것 같다.
깔끔하네요 ㅋㅋ
typo : 장성한 -> 작성한
맞춤법검사기를 돌려도 너의 인간 맞춤법검사기는 매번 못피해가는구나.. 땡큐.. ㅋㅋ