Supertest.Ja
| プラットフォーム | コマンド |
|---|---|
| npm (All Platforms) | npm install supertest --save-dev |
| Yarn (All Platforms) | yarn add supertest --dev |
| With Jest | npm install jest supertest --save-dev |
| With Mocha | npm install mocha supertest --save-dev |
| With TypeScript | npm install supertest @types/supertest --save-dev |
| Specific Version | npm install supertest@6.3.3 --save-dev |
| コマンド | 説明 |
|---|---|
request(app).get('/api/users') | エンドポイントにGETリクエストを送信 |
request(app).post('/api/users') | エンドポイントにPOSTリクエストを送信 |
request(app).put('/api/users/123') | リソースを更新するPUT要求を送信 |
request(app).patch('/api/users/123') | 部分更新のためにPATCHリクエストを送信 |
request(app).delete('/api/users/123') | リソースを削除するためにDELETE リクエストを送信 |
request(app).head('/api/users') | HEADリクエストを送信(ヘッダーのみ) |
request(app).options('/api/users') | CORSプリフライトのためのOPTIONSリクエストを送信 |
.send({ name: 'John' }) | リクエストボディにJSONデータを送信 |
.query({ page: 1, limit: 10 }) | URLにクエリパラメータを追加する |
.set('Authorization', 'Bearer token') | リクエストヘッダーを設定する |
.expect(200) | HTTPステータスコードを確認する |
.expect('Content-Type', /json/) | レスポンスヘッダーの値をアサートする |
.end((err, res) => {}) | コールバック付きでリクエストを実行 |
await request(app).get('/api/users') | Promise/async-awaitでリクエストを実行する |
| コマンド | 説明 |
|---|---|
.expect(200) | 正確なステータスコードを期待する |
.expect(200, { id: 1 }) | ステータスと完全な本文の一致を期待する |
.expect('Content-Type', 'application/json') | 正確なヘッダー値を期待する |
.expect('Content-Type', /json/) | Expect ヘッダーマッチング regex |
.expect((res) => { if (!res.body) throw new Error() }) | カスタムアサーション関数 |
.expect(200, /success/) | ステータスとボディが正規表現にマッチすることを期待する |
.expect('Location', '/new-url') | リダイレクトロケーションヘッダーを予期する |
.expect({ error: 'Not found' }) | 正確な JSON 本文の一致を期待する |
| コマンド | 説明 |
|---|---|
.set('Authorization', 'Bearer token') | 単一のリクエストヘッダーを設定する |
.set({ 'X-API-Key': 'key', 'Accept': 'json' }) | 複数のヘッダーを設定 |
.auth('username', 'password') | HTTP Basic 認証を設定する |
.field('name', 'value') | フォームフィールドを追加(マルチパート) |
.attach('file', 'path/to/file.pdf') | アップロード用のファイルを添付 |
.attach('file', buffer, 'filename.pdf') | バッファからファイルを添付 |
.timeout(5000) | リクエストのタイムアウトをミリ秒で設定 |
.redirects(5) | 最大5つのリダイレクトに従う |
.type('json') | Content-Typeヘッダーを設定する |
.accept('json') | Accept ヘッダーを設定 |
.send('raw string data') | 生の文字列本文を送信 |
.send(Buffer.from('data')) | バイナリデータを送信 |
| コマンド | 説明 |
|---|---|
request.agent(app) | 永続的なCookieのためのエージェントを作成する |
agent.get('/api/profile') | 認証されたリクエストにエージェントを使用する |
request('https://api.example.com') | 外部APIエンドポイントをテストする |
.parse((res, callback) => {}) | カスタムレスポンスパーサー |
.buffer(true) | バッファレスポンスボディ |
.responseType('blob') | 予想される応答タイプを設定 |
.retry(3) | 失敗したリクエストを3回再試行する |
.ca(cert) | カスタム CA 証明書を設定 |
.cert(cert) | クライアント証明書を設定 |
.key(key) | クライアントの秘密鍵を設定 |
.pfx(pfxData) | PFX/PKCS12 証明書を設定する |
.disableTLSCerts() | TLS 証明書の検証を無効にする |
| パターン | 説明 |
|---|---|
const res = await request(app).get('/api/users') | 複数のアサーションのレスポンスを保存 |
expect(res.body).toHaveLength(10) | テストフレームワークでレスポンスボディをアサートする |
expect(res.headers['x-custom']).toBe('value') | レスポンスヘッダーでアサートする |
expect(res.status).toBe(200) | 代替ステータスアサーション |
Promise.all([request(app).get('/a'), request(app).get('/b')]) | 並列リクエストテスト |
for (const user of users) { await request(app).post('/api/users').send(user) } | 連続テストリクエスト |
// test/setup.js
const request = require('supertest');
const app = require('../app');
module.exports = { request, app };
```## テストパターン
```javascript
// jest.config.js
module.exports = {
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.js'],
collectCoverageFrom: ['src/**/*.js'],
coveragePathIgnorePatterns: ['/node_modules/']
};
```## 設定
```javascript
// test/mocha.opts
--require test/setup.js
--recursive
--timeout 5000
--exit
```### 基本的なテストセットアップ
```json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:api": "jest --testPathPattern=api"
}
}
// config/test.js
module.exports = {
port: process.env.TEST_PORT || 3001,
database: process.env.TEST_DB || 'test_db',
apiTimeout: process.env.API_TIMEOUT || 5000
};
```### Jestの設定
```javascript
const request = require('supertest');
const app = require('../app');
describe('User API', () => {
let userId;
// Create
test('POST /api/users - create user', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: 'John Doe', email: 'john@example.com' })
.expect(201)
.expect('Content-Type', /json/);
userId = res.body.id;
expect(res.body.name).toBe('John Doe');
});
// Read
test('GET /api/users/:id - get user', async () => {
await request(app)
.get(`/api/users/${userId}`)
.expect(200)
.expect((res) => {
expect(res.body.name).toBe('John Doe');
});
});
// Update
test('PUT /api/users/:id - update user', async () => {
await request(app)
.put(`/api/users/${userId}`)
.send({ name: 'Jane Doe' })
.expect(200);
});
// Delete
test('DELETE /api/users/:id - delete user', async () => {
await request(app)
.delete(`/api/users/${userId}`)
.expect(204);
});
});
const request = require('supertest');
const app = require('../app');
describe('Authentication', () => {
let authToken;
test('POST /auth/register - register new user', async () => {
await request(app)
.post('/auth/register')
.send({
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
})
.expect(201);
});
test('POST /auth/login - login user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'SecurePass123!'
})
.expect(200)
.expect('Content-Type', /json/);
authToken = res.body.token;
expect(authToken).toBeDefined();
});
test('GET /api/profile - access protected route', async () => {
await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect((res) => {
expect(res.body.email).toBe('test@example.com');
});
});
test('GET /api/profile - reject without token', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
});
```### Mochaの設定
```javascript
const request = require('supertest');
const app = require('../app');
const path = require('path');
describe('File Upload', () => {
test('POST /api/upload - upload single file', async () => {
const filePath = path.join(__dirname, 'fixtures', 'test.pdf');
const res = await request(app)
.post('/api/upload')
.attach('document', filePath)
.field('title', 'Test Document')
.field('description', 'A test file upload')
.expect(200)
.expect('Content-Type', /json/);
expect(res.body.filename).toMatch(/\.pdf$/);
expect(res.body.size).toBeGreaterThan(0);
});
test('POST /api/upload - upload multiple files', async () => {
await request(app)
.post('/api/upload/multiple')
.attach('files', 'test/fixtures/file1.pdf')
.attach('files', 'test/fixtures/file2.pdf')
.expect(200)
.expect((res) => {
expect(res.body.files).toHaveLength(2);
});
});
});
const request = require('supertest');
const app = require('../app');
describe('Session Management', () => {
const agent = request.agent(app);
test('Login and maintain session', async () => {
// Login
await agent
.post('/auth/login')
.send({ username: 'user', password: 'pass' })
.expect(200);
// Session persists - can access protected route
await agent
.get('/api/dashboard')
.expect(200);
// Session still valid for subsequent requests
await agent
.get('/api/profile')
.expect(200);
// Logout
await agent
.post('/auth/logout')
.expect(200);
// Session expired - access denied
await agent
.get('/api/dashboard')
.expect(401);
});
});
```### package.jsonスクリプト
```javascript
const request = require('supertest');
const app = require('../app');
describe('Error Handling', () => {
test('404 - Resource not found', async () => {
await request(app)
.get('/api/users/99999')
.expect(404)
.expect((res) => {
expect(res.body.error).toBe('User not found');
expect(res.body.code).toBe('USER_NOT_FOUND');
});
});
test('400 - Validation error', async () => {
await request(app)
.post('/api/users')
.send({ name: '' }) // Invalid data
.expect(400)
.expect((res) => {
expect(res.body.errors).toBeDefined();
expect(res.body.errors.name).toContain('required');
});
});
test('429 - Rate limit exceeded', async () => {
// Make multiple requests to trigger rate limit
const requests = Array(101).fill().map(() =>
request(app).get('/api/users')
);
const responses = await Promise.all(requests);
const rateLimited = responses.find(r => r.status === 429);
expect(rateLimited).toBeDefined();
expect(rateLimited.body.error).toMatch(/rate limit/i);
});
test('500 - Internal server error', async () => {
await request(app)
.get('/api/trigger-error')
.expect(500)
.expect((res) => {
expect(res.body.error).toBe('Internal server error');
});
});
});
const res = await request(app).get('/api/users').expect(200);
```### 環境変数
`devDependencies`
```bash
npm install supertest --save-dev
-
Don’t start the server manually: SuperTest handles server binding automatically. Pass the Express app directly without calling
app.listen().// Good: Export app without listening module.exports = app; // Bad: Don't do this in test files app.listen(3000); -
Use request.agent() for session testing: When testing authenticated routes or cookie-based sessions, create an agent to persist cookies across requests.
const agent = request.agent(app); await agent.post('/login').send(credentials); await agent.get('/protected'); // Cookies persist -
Chain expectations for cleaner tests: Multiple
.expect()calls can be chained for comprehensive assertions in a single test.await request(app) .get('/api/users') .expect(200) .expect('Content-Type', /json/) .expect((res) => expect(res.body).toHaveLength(10)); -
Store responses for complex assertions: Capture the response object when you need to perform multiple assertions on different parts.
const res = await request(app).get('/api/users').expect(200); expect(res.body).toHaveLength(5); expect(res.headers['x-total-count']).toBe('100'); -
Test both success and failure cases: Always test error scenarios, validation failures, and edge cases alongside happy paths.
test('returns 404 for non-existent user', async () => { await request(app).get('/api/users/99999').expect(404); }); -
Use descriptive test names: Write test descriptions that clearly explain what behavior is being verified.
test('POST /api/users returns 400 when email is missing', async () => { // Test implementation }); -
Clean up test data: Use
beforeEach/afterEachhooks to reset database state and ensure test isolation.afterEach(async () => { await User.deleteMany({}); }); -
Set appropriate timeouts: For slow endpoints or external API calls, configure timeout values to prevent false failures.
await request(app).get('/api/slow').timeout(10000).expect(200);
Troubleshooting
| 問題 | ソリューション |
|---|---|
EADDRINUSE: address already in use | Don’t call app.listen() in your app file when testing. Export the app without starting the server, or use different ports for testing. |
| Tests hang and never complete | Ensure database connections are closed after tests. Use afterAll() to close connections: afterAll(() => mongoose.connection.close()) |
Cannot read property 'body' of undefined | Missing await keyword or .end() callback. Always use await with async/await or provide .end() callback. |
| Headers already sent error | You’re sending multiple responses in your route handler. Ensure only one res.send(), res.json(), or similar call per request. |
| Timeout errors on slow endpoints | Increase timeout with .timeout(ms) or adjust global timeout in test framework config: jest.setTimeout(10000) |
| Authentication tests fail randomly | Use request.agent() to persist cookies/sessions across requests instead of separate request() calls. |
| File upload tests fail | Verify file path is correct using path.join(__dirname, 'fixtures', 'file.pdf'). Ensure multipart middleware is configured in app. |
| CORS errors in tests | CORS is typically a browser concern. SuperTest bypasses CORS. If testing CORS headers, use .options() request with Origin header. |
| SSL/TLS certificate errors | Use .disableTLSCerts() for testing environments or provide valid certificates with .ca(), .cert(), .key() methods. |
| Response body is empty Buffer | Response might be binary. Use .buffer(true) or check Content-Type. For JSON, ensure server sends correct Content-Type header. |
| Tests pass individually but fail together | Tests aren’t isolated. Clear database/state between tests using beforeEach/afterEach hooks. Check for shared mutable state. |