Inversifyを使ったユニットテスト: Requestのモック作成

InversifyでRequestをモックする

概要

inversify-express-utilsを使ったテストで直面するかもしれない一つの課題、すなわちBaseHttpControllerからHTTPリクエスト情報を取得する際にアクセスできないプロパティをモックする方法について説明します。

解決策

Object.definePropertyを使用すると、オブジェクトに新しいプロパティを安全に追加したり、 既存のプロパティを変更することができます。 この方法を利用してhttpContextプロパティをモックに置き換えることで、テスト中に任意のHTTPリクエストとレスポンスをシミュレートできます。

実践

実際のテストケースでは、まずjest-mock-extendedを使用してexpress.Requestexpress.Responseのモックを作成します。 次に、これらのモックを用いてHttpContextを構成し、Object.definePropertyを使ってテスト対象のコントローラのhttpContextを上書きします。

TokenController.test.ts ```typescript import {mockDeep, mockReset} from 'jest-mock-extended'; import * as express from 'express'; import {HttpContext} from 'inversify-express-utils'; describe('TokenController', () => { const requestMock = mockDeep(); const responseMock = mockDeep(); let mockHttpContext: HttpContext; let userInformationController: TokenController; beforeEach(() => { [ requestMock, responseMock, ].forEach(mockReset); mockHttpContext = mockDeep({ request: requestMock, response: responseMock, }); Object.defineProperty(TokenController, 'httpContext', { value: mockHttpContext, }); }); it('should return enriched user information on success', async () => { // Arrange requestMock.headers.authorization = 'Bearer testToken'; // Act const result = await TokenController.get(); // Assert const responseMessage = await result.executeAsync(); expect(responseMessage.statusCode).toEqual(200); const content = await responseMessage.content.readAsStringAsync(); expect(JSON.parse(content)).toEqual('testToken'); }); }); ``` </detail> TokenController.ts ```typescript import {inject} from 'inversify'; import express from 'express'; import {controller, httpGet} from 'inversify-express-utils'; import {BaseHttpController} from 'inversify-express-utils'; @controller('/api') export class TokenController extends BaseHttpController { @httpGet('/token') public async get() { const token = this.getToken(this.httpContext.request); if (!token) { logger?.warn('Authorization token is missing in the request header.'); return this.statusCode(401); } return this.json(token, 200); } private getToken(req: express.Request): string | undefined | null { const authorizationHeader = req.headers['authorization']; if (!authorizationHeader) return undefined; const tokenArray = authorizationHeader.split('Bearer '); if (tokenArray.length === 2) { return tokenArray[1].trim(); } return null; } } ``` package.json ```json { "name": "myapp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "jest", "start": "node build/index.js", "lint": "gts lint", "clean": "gts clean", "compile": "tsc", "fix": "gts fix", "prepare": "npm run compile", "pretest": "npm run compile", "posttest": "npm run lint" }, "keywords": [], "author": "", "license": "ISC", "engines": { "node": "20.x" }, "devDependencies": { "@types/config": "^3.3.4", "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/node": "20.8.2", "@types/prettyjson": "^0.0.33", "@types/uuid": "^9.0.8", "gts": "^5.2.0", "jest": "^29.7.0", "jest-mock-extended": "^3.0.5", "nodemon": "^3.1.0", "ts-jest": "^29.1.2", "typescript": "~5.1.6" }, "dependencies": { "axios": "^1.6.7", "config": "^3.3.11", "dayjs": "^1.11.10", "express": "^4.19.2", "inversify": "^6.0.2", "inversify-express-utils": "^6.4.6", "jose": "^5.2.3", "mysql2": "^3.9.2", "prettyjson": "^1.2.5", "redis": "^4.6.13", "reflect-metadata": "^0.2.1", "uuid": "^9.0.1" } } ```