Supertest는 Node.js HTTP 서버를 테스트하기 위한 SuperAgent 기반의 라이브러리이다.
supertest 테스트 코드의 예시를 먼저 보자
import * as request from 'supertest';
it('should return a list of users with a valid API key', async () => {
const response = await request(app)
// get Method + url
.get('/api/users')
// header setting
.set('X-API-Key', 'valid-api-key')
// expect
.expect(200);
expect(response.body).toBeInstanceOf(Array);
});
request/index.js 에서 exports하는 함수를 살펴보자
let http2
try {
http2 = require('http2');
} catch (_) {
//
}
const methods = require('methods');
/**
* Test against the given `app`,
* returning a new `Test`.
*
* @param {Function|Server|String} app
* @return {Test}
* @api public
*/
module.exports = function(app, options = {}) {
const obj = {};
if (typeof app === 'function') {
if (options.http2) {
if (!http2) {
throw new Error(
'supertest: this version of Node.js does not support http2'
);
}
// 1. http2가 있으면 http2로 서버를 생성하고 없다면 http 내장 모듈로 서버를 실행한다.
app = http2.createServer(app);
} else {
app = http.createServer(app);
}
}
// 2. 빈 객체 obj에 nodejs에 존재하는 method를 key, Test 객체를 생성하는 함수를 Value로 할당한다.
methods.forEach(function(method) {
obj[method] = function(url) {
var test = new Test(app, method, url);
if (options.http2) {
test.http2();
}
return test;
};
});
// Support previous use of del
obj.del = obj.delete;
return obj;
};
{
get: function(url) { ... },
post: function(url) { ... },
put: function(url) { ... },
delete: function(url) { ... },
...
}
var test = new Test(app, 'get', '/api/users');
// ...
return test;
Test 클래스를 보면 superagent의 Request Class를 상속 받고 있는 것을 확인 되고 생성자 함수를 보면 테스트 된 _asserts 배열을 멤버 변수로 만들고 테스트를 실행할 URL을 등록한다.
const { Request } = require('superagent');
class Test extends Request {
/**
* Initialize a new `Test` with the given `app`,
* request `method` and `path`.
*
* @param {Server} app
* @param {String} method
* @param {String} path
* @api public
*/
constructor (app, method, path) {
super(method.toUpperCase(), path);
this.redirects(0);
this.buffer();
this.app = app;
// 1. 테스트 된 배열
this._asserts = [];
// 2. URL 등록
// string 주소가 들어오면 그대로 사용하고 <- 아마 도메인일지도
// 아니라면 url 주소를 생성한다.
this.url = typeof app === 'string'
? app + path
: this.serverAddress(app, path);
}
...
/**
* Returns a URL, extracted from a server.
*
* @param {Server} app
* @param {String} path
* @returns {String} URL address
* @api private
*/
serverAddress(app, path) {
const addr = app.address();
// 1. 주소가 없으면 서버를 실행한다.
if (!addr) this._server = app.listen(0);
// 2. 주소의 포트를 가져온다.
const port = app.address().port;
// 3. protocol을 가져온다.
const protocol = app instanceof Server ? 'https' : 'http';
// 4. localhost(127.0.0.1:port/path) <- 앞서 실행한 로컬 호스트 주소를 완성한다.
return protocol + '://127.0.0.1:' + port + path;
}
expect 함수 위에 주석으로 테스트할 예시 코드가 나와 있다.
전달되는 매개 변수의 개수와 타입에 따라 Header, Body, Status 등 결과의 어떤 부분을 검사하고 싶은지 분기처리 한 후 검사하는 것을 볼 수 있다!
이 후 결과물은 asserts 배열에 담게 된다!
/**
* Expectations:
*
* .expect(200)
* .expect(200, fn)
* .expect(200, body)
* .expect('Some body')
* .expect('Some body', fn)
* .expect(['json array body', { key: 'val' }])
* .expect('Content-Type', 'application/json')
* .expect('Content-Type', 'application/json', fn)
* .expect(fn)
* .expect([200, 404])
*
* @return {Test}
* @api public
*/
expect(a, b, c) {
// callback
if (typeof a === 'function') {
this._asserts.push(wrapAssertFn(a));
return this;
}
if (typeof b === 'function') this.end(b);
if (typeof c === 'function') this.end(c);
// status
if (typeof a === 'number') {
this._asserts.push(wrapAssertFn(this._assertStatus.bind(this, a)));
// body
if (typeof b !== 'function' && arguments.length > 1) {
this._asserts.push(wrapAssertFn(this._assertBody.bind(this, b)));
}
return this;
}
// multiple statuses
if (Array.isArray(a) && a.length > 0 && a.every(val => typeof val === 'number')) {
this._asserts.push(wrapAssertFn(this._assertStatusArray.bind(this, a)));
return this;
}
// header field
if (typeof b === 'string' || typeof b === 'number' || b instanceof RegExp) {
this._asserts.push(wrapAssertFn(this._assertHeader.bind(this, { name: '' + a, value: b })));
return this;
}
// body
this._asserts.push(wrapAssertFn(this._assertBody.bind(this, a)));
return this;
}
Supertest의 package.json을 보면 supertestagent, methods라이브러리에 의존하고 있는 것을 확인할 수있다.
// packages.json
{
"name": "supertest",
"description": "SuperAgent driven library for testing HTTP servers",
"version": "6.3.4",
"author": "TJ Holowaychuk",
"contributors": [],
"dependencies": {
"methods": "^1.1.2", // ✅ methods
"superagent": "^8.1.2" // ✅ superagent
},
"devDependencies": {
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"eslint": "^8.32.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.27.5",
"express": "^4.18.2",
"mocha": "^10.2.0",
"nock": "^13.3.0",
"nyc": "^15.1.0",
"proxyquire": "^2.1.3",
"should": "^13.2.3"
},
methods는 간단한 라이브러리이다.
현재 HTTP 요청에 Methods가 있으면 해당 Method를 반환하고 아니면 Nodejs의 기본 메서드 배열을 반환해주는 라이브러리이다!

SuperAgent는 유연성, 가독성 및 낮은 학습 곡선을 위해 만들어진 경량의 진보적인 ajax API이다! axios, fetch 같은 HTTP 요청 라이브러리 같은 것이다.
request
.get('/search')
.set('API-Key', 'foobar')
.set('Accept', 'application/json')
.then(callback);
request
.get('/search')
.query({ query: 'Manny' })
.query({ range: '1..5' })
.query({ order: 'desc' })
.then(res => {
});
위와 같이 superagent를 활용하여 HTTP API 요청을 할 수 있는데, superagent가 매핑된 supertest는 superagent 코드 뒤에 expect 메서드를 활용하여 통합 테스트를 편하게 할 수 있도록 도와주는 라이브러리 인 것이다!
🤔 axios를 사용하면 axiostest일까?
찾아보니 axios-test-instance가 있긴 했다!!
superagent에 대한 설명은 superagent를 참고하면 좋을 것 같다 :)