Просмотр исходного кода

refactor: adds Route to RequestContext

e22m4u 3 недель назад
Родитель
Сommit
6e64cb8f3b

+ 12 - 6
README.md

@@ -78,9 +78,6 @@ server.listen(3000, 'localhost');             // прослушивание за
 `RequestContext` с набором свойств, содержащих разобранные
 данные входящего запроса.
 
-- `container: ServiceContainer` экземпляр [сервис-контейнера](https://npmjs.com/package/@e22m4u/js-service);
-- `request: IncomingMessage` нативный поток входящего запроса;
-- `response: ServerResponse` нативный поток ответа сервера;
 - `params: ParsedParams` объект ключ-значение с параметрами пути;
 - `query: ParsedQuery` объект ключ-значение с параметрами строки запроса;
 - `headers: ParsedHeaders` объект ключ-значение с заголовками запроса;
@@ -89,6 +86,13 @@ server.listen(3000, 'localhost');             // прослушивание за
 - `path: string` путь включающий строку запроса, например `/myPath?foo=bar`;
 - `pathname: string` путь запроса, например `/myPath`;
 - `body: unknown` тело запроса;
+
+Дополнительные свойства:
+
+- `container: ServiceContainer` экземпляр [сервис-контейнера](https://npmjs.com/package/@e22m4u/js-service);
+- `request: IncomingMessage` нативный поток входящего запроса;
+- `response: ServerResponse` нативный поток ответа сервера;
+- `route: Route` экземпляр текущего маршрута;
 - `meta: object` мета-данные из определения маршрута;
 
 Пример доступа к контексту из обработчика маршрута.
@@ -101,9 +105,6 @@ router.defineRoute({
   handler(ctx) {
     // GET /users/10?include=city
     // Cookie: foo=bar; baz=qux;
-    console.log(ctx.container); // ServiceContainer
-    console.log(ctx.request);   // IncomingMessage
-    console.log(ctx.response);  // ServerResponse
     console.log(ctx.params);    // {id: 10}
     console.log(ctx.query);     // {include: 'city'}
     console.log(ctx.headers);   // {cookie: 'foo=bar; baz=qux;'}
@@ -111,6 +112,11 @@ router.defineRoute({
     console.log(ctx.method);    // "GET"
     console.log(ctx.path);      // "/users/10?include=city"
     console.log(ctx.pathname);  // "/users/10"
+    // дополнительные свойства
+    console.log(ctx.container); // ServiceContainer
+    console.log(ctx.request);   // IncomingMessage
+    console.log(ctx.response);  // ServerResponse
+    console.log(ctx.route);     // Route
     console.log(ctx.meta);      // {prop: 'value'}
     // ...
   },

+ 74 - 18
dist/cjs/index.cjs

@@ -56,6 +56,7 @@ __export(index_exports, {
   createError: () => createError,
   createRequestMock: () => createRequestMock,
   createResponseMock: () => createResponseMock,
+  createRouteMock: () => createRouteMock,
   fetchRequestBody: () => fetchRequestBody,
   getRequestPathname: () => getRequestPathname,
   isPromise: () => isPromise,
@@ -207,6 +208,16 @@ function isResponseSent(response) {
 }
 __name(isResponseSent, "isResponseSent");
 
+// src/utils/create-route-mock.js
+function createRouteMock(options = {}) {
+  return new Route({
+    method: options.method || HttpMethod.GET,
+    path: options.path || "/",
+    handler: options.handler || (() => "OK")
+  });
+}
+__name(createRouteMock, "createRouteMock");
+
 // src/utils/is-readable-stream.js
 function isReadableStream(value) {
   if (!value || typeof value !== "object") return false;
@@ -1356,26 +1367,63 @@ var RouteRegistry = _RouteRegistry;
 // src/request-context.js
 var import_js_format19 = require("@e22m4u/js-format");
 var import_js_service3 = require("@e22m4u/js-service");
-var import_js_service4 = require("@e22m4u/js-service");
 var _RequestContext = class _RequestContext {
   /**
    * Service container.
    *
-   * @type {import('@e22m4u/js-service').ServiceContainer}
+   * @type {ServiceContainer}
    */
-  container;
+  _container;
+  /**
+   * Getter of service container.
+   *
+   * @type {ServiceContainer}
+   */
+  get container() {
+    return this._container;
+  }
   /**
    * Request.
    *
    * @type {import('http').IncomingMessage}
    */
-  request;
+  _request;
+  /**
+   * Getter of request.
+   *
+   * @type {import('http').IncomingMessage}
+   */
+  get request() {
+    return this._request;
+  }
   /**
    * Response.
    *
    * @type {import('http').ServerResponse}
    */
-  response;
+  _response;
+  /**
+   * Getter of response.
+   *
+   * @type {import('http').ServerResponse}
+   */
+  get response() {
+    return this._response;
+  }
+  /**
+   * Route
+   *
+   * @type {Route}
+   */
+  _route;
+  /**
+   * Getter of route.
+   *
+   * @type {Route}
+   */
+  get route() {
+    return this._route;
+  }
   /**
    * Query.
    *
@@ -1409,9 +1457,11 @@ var _RequestContext = class _RequestContext {
   /**
    * Route meta.
    *
-   * @type {object}
+   * @type {import('./route.js').RouteMeta}
    */
-  meta = {};
+  get meta() {
+    return this.route.meta;
+  }
   /**
    * Method.
    *
@@ -1451,35 +1501,43 @@ var _RequestContext = class _RequestContext {
    * @param {ServiceContainer} container
    * @param {import('http').IncomingMessage} request
    * @param {import('http').ServerResponse} response
+   * @param {Route} route
    */
-  constructor(container, request, response) {
-    if (!(0, import_js_service4.isServiceContainer)(container))
+  constructor(container, request, response, route) {
+    if (!(0, import_js_service3.isServiceContainer)(container))
       throw new import_js_format19.Errorf(
         'The parameter "container" of RequestContext.constructor should be an instance of ServiceContainer, but %v was given.',
         container
       );
-    this.container = container;
+    this._container = container;
     if (!request || typeof request !== "object" || Array.isArray(request) || !isReadableStream(request)) {
       throw new import_js_format19.Errorf(
         'The parameter "request" of RequestContext.constructor should be an instance of IncomingMessage, but %v was given.',
         request
       );
     }
-    this.request = request;
+    this._request = request;
     if (!response || typeof response !== "object" || Array.isArray(response) || !isWritableStream(response)) {
       throw new import_js_format19.Errorf(
         'The parameter "response" of RequestContext.constructor should be an instance of ServerResponse, but %v was given.',
         response
       );
     }
-    this.response = response;
+    this._response = response;
+    if (!(route instanceof Route)) {
+      throw new import_js_format19.Errorf(
+        'The parameter "route" of RequestContext.constructor should be an instance of Route, but %v was given.',
+        route
+      );
+    }
+    this._route = route;
   }
 };
 __name(_RequestContext, "RequestContext");
 var RequestContext = _RequestContext;
 
 // src/trie-router.js
-var import_js_service5 = require("@e22m4u/js-service");
+var import_js_service4 = require("@e22m4u/js-service");
 var import_http4 = require("http");
 
 // src/senders/data-sender.js
@@ -1696,11 +1754,8 @@ var _TrieRouter = class _TrieRouter extends DebuggableService {
       this.getService(ErrorSender).send404(request, response);
     } else {
       const { route, params } = resolved;
-      const container = new import_js_service5.ServiceContainer(this.container);
-      const context = new RequestContext(container, request, response);
-      if (route.meta != null) {
-        context.meta = cloneDeep(route.meta);
-      }
+      const container = new import_js_service4.ServiceContainer(this.container);
+      const context = new RequestContext(container, request, response, route);
       container.set(RequestContext, context);
       container.set(import_http4.IncomingMessage, request);
       container.set(import_http4.ServerResponse, response);
@@ -1827,6 +1882,7 @@ var TrieRouter = _TrieRouter;
   createError,
   createRequestMock,
   createResponseMock,
+  createRouteMock,
   fetchRequestBody,
   getRequestPathname,
   isPromise,

+ 13 - 9
src/request-context.d.ts

@@ -1,10 +1,8 @@
-import {ServerResponse} from 'http';
-import {IncomingMessage} from 'http';
-import {RouteMeta} from './route.js';
-import {ParsedQuery} from './parsers/index.js';
+import {Route, RouteMeta} from './route.js';
 import {ParsedCookies} from './utils/index.js';
-import {ParsedHeaders} from './parsers/index.js';
 import {ServiceContainer} from '@e22m4u/js-service';
+import {IncomingMessage, ServerResponse} from 'http';
+import {ParsedQuery, ParsedHeaders} from './parsers/index.js';
 
 /**
  * Parsed params.
@@ -20,17 +18,22 @@ export declare class RequestContext {
   /**
    * Container.
    */
-  container: ServiceContainer;
+  get container(): ServiceContainer;
 
   /**
    * Request.
    */
-  request: IncomingMessage;
+  get request(): IncomingMessage;
 
   /**
    * Response.
    */
-  response: ServerResponse;
+  get response(): ServerResponse;
+
+  /**
+   * Route.
+   */
+  get route(): Route;
 
   /**
    * Query.
@@ -60,7 +63,7 @@ export declare class RequestContext {
   /**
    * Route meta.
    */
-  meta: RouteMeta;
+  get meta(): RouteMeta;
 
   /**
    * Method.
@@ -88,5 +91,6 @@ export declare class RequestContext {
     container: ServiceContainer,
     request: IncomingMessage,
     response: ServerResponse,
+    route: Route,
   );
 }

+ 72 - 15
src/request-context.js

@@ -1,9 +1,12 @@
+import {Route} from './route.js';
 import {Errorf} from '@e22m4u/js-format';
-import {isReadableStream} from './utils/index.js';
-import {isWritableStream} from './utils/index.js';
-import {ServiceContainer} from '@e22m4u/js-service';
-import {getRequestPathname} from './utils/index.js';
-import {isServiceContainer} from '@e22m4u/js-service';
+import {ServiceContainer, isServiceContainer} from '@e22m4u/js-service';
+
+import {
+  isReadableStream,
+  isWritableStream,
+  getRequestPathname,
+} from './utils/index.js';
 
 /**
  * Request context.
@@ -12,23 +15,66 @@ export class RequestContext {
   /**
    * Service container.
    *
-   * @type {import('@e22m4u/js-service').ServiceContainer}
+   * @type {ServiceContainer}
+   */
+  _container;
+
+  /**
+   * Getter of service container.
+   *
+   * @type {ServiceContainer}
    */
-  container;
+  get container() {
+    return this._container;
+  }
 
   /**
    * Request.
    *
    * @type {import('http').IncomingMessage}
    */
-  request;
+  _request;
+
+  /**
+   * Getter of request.
+   *
+   * @type {import('http').IncomingMessage}
+   */
+  get request() {
+    return this._request;
+  }
 
   /**
    * Response.
    *
    * @type {import('http').ServerResponse}
    */
-  response;
+  _response;
+
+  /**
+   * Getter of response.
+   *
+   * @type {import('http').ServerResponse}
+   */
+  get response() {
+    return this._response;
+  }
+
+  /**
+   * Route
+   *
+   * @type {Route}
+   */
+  _route;
+
+  /**
+   * Getter of route.
+   *
+   * @type {Route}
+   */
+  get route() {
+    return this._route;
+  }
 
   /**
    * Query.
@@ -68,9 +114,11 @@ export class RequestContext {
   /**
    * Route meta.
    *
-   * @type {object}
+   * @type {import('./route.js').RouteMeta}
    */
-  meta = {};
+  get meta() {
+    return this.route.meta;
+  }
 
   /**
    * Method.
@@ -115,15 +163,16 @@ export class RequestContext {
    * @param {ServiceContainer} container
    * @param {import('http').IncomingMessage} request
    * @param {import('http').ServerResponse} response
+   * @param {Route} route
    */
-  constructor(container, request, response) {
+  constructor(container, request, response, route) {
     if (!isServiceContainer(container))
       throw new Errorf(
         'The parameter "container" of RequestContext.constructor ' +
           'should be an instance of ServiceContainer, but %v was given.',
         container,
       );
-    this.container = container;
+    this._container = container;
     if (
       !request ||
       typeof request !== 'object' ||
@@ -136,7 +185,7 @@ export class RequestContext {
         request,
       );
     }
-    this.request = request;
+    this._request = request;
     if (
       !response ||
       typeof response !== 'object' ||
@@ -149,6 +198,14 @@ export class RequestContext {
         response,
       );
     }
-    this.response = response;
+    this._response = response;
+    if (!(route instanceof Route)) {
+      throw new Errorf(
+        'The parameter "route" of RequestContext.constructor ' +
+          'should be an instance of Route, but %v was given.',
+        route,
+      );
+    }
+    this._route = route;
   }
 }

+ 45 - 17
src/request-context.spec.js

@@ -1,16 +1,21 @@
 import {expect} from 'chai';
 import {format} from '@e22m4u/js-format';
-import {createRequestMock} from './utils/index.js';
 import {RequestContext} from './request-context.js';
 import {ServiceContainer} from '@e22m4u/js-service';
-import {createResponseMock} from './utils/index.js';
+
+import {
+  createRouteMock,
+  createRequestMock,
+  createResponseMock,
+} from './utils/index.js';
 
 describe('RequestContext', function () {
   describe('constructor', function () {
     it('requires the parameter "container" to be the ServiceContainer', function () {
       const req = createRequestMock();
       const res = createResponseMock();
-      const throwable = v => () => new RequestContext(v, req, res);
+      const route = createRouteMock();
+      const throwable = v => () => new RequestContext(v, req, res, route);
       const error = v =>
         format(
           'The parameter "container" of RequestContext.constructor ' +
@@ -32,8 +37,9 @@ describe('RequestContext', function () {
 
     it('requires the parameter "request" to be the ServiceContainer', function () {
       const res = createResponseMock();
+      const route = createRouteMock();
       const cont = new ServiceContainer();
-      const throwable = v => () => new RequestContext(cont, v, res);
+      const throwable = v => () => new RequestContext(cont, v, res, route);
       const error = v =>
         format(
           'The parameter "request" of RequestContext.constructor ' +
@@ -55,8 +61,9 @@ describe('RequestContext', function () {
 
     it('requires the parameter "response" to be the ServiceContainer', function () {
       const req = createRequestMock();
+      const route = createRouteMock();
       const cont = new ServiceContainer();
-      const throwable = v => () => new RequestContext(cont, req, v);
+      const throwable = v => () => new RequestContext(cont, req, v, route);
       const error = v =>
         format(
           'The parameter "response" of RequestContext.constructor ' +
@@ -76,22 +83,39 @@ describe('RequestContext', function () {
       throwable(createResponseMock())();
     });
 
-    it('sets properties from given arguments', function () {
+    it('requires the parameter "route" to be the Route', function () {
       const req = createRequestMock();
       const res = createResponseMock();
       const cont = new ServiceContainer();
-      const ctx = new RequestContext(cont, req, res);
-      expect(ctx.container).to.be.eq(cont);
-      expect(ctx.request).to.be.eq(req);
-      expect(ctx.response).to.be.eq(res);
+      const throwable = v => () => new RequestContext(cont, req, res, v);
+      const error = v =>
+        format(
+          'The parameter "route" of RequestContext.constructor ' +
+            'should be an instance of Route, but %s was given.',
+          v,
+        );
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      throwable(createRouteMock())();
     });
 
-    it('sets an empty object to the "meta" property', function () {
+    it('sets properties from given arguments', function () {
       const req = createRequestMock();
       const res = createResponseMock();
+      const route = createRouteMock();
       const cont = new ServiceContainer();
-      const ctx = new RequestContext(cont, req, res);
-      expect(ctx.meta).to.be.eql({});
+      const ctx = new RequestContext(cont, req, res, route);
+      expect(ctx.container).to.be.eq(cont);
+      expect(ctx.request).to.be.eq(req);
+      expect(ctx.response).to.be.eq(res);
     });
   });
 
@@ -99,8 +123,9 @@ describe('RequestContext', function () {
     it('returns the method name in upper case', function () {
       const req = createRequestMock({method: 'post'});
       const res = createResponseMock();
+      const route = createRouteMock();
       const cont = new ServiceContainer();
-      const ctx = new RequestContext(cont, req, res);
+      const ctx = new RequestContext(cont, req, res, route);
       expect(ctx.method).to.be.eq('POST');
     });
   });
@@ -109,8 +134,9 @@ describe('RequestContext', function () {
     it('returns the request pathname with the query string', function () {
       const req = createRequestMock({path: '/pathname?foo=bar'});
       const res = createResponseMock();
+      const route = createRouteMock();
       const cont = new ServiceContainer();
-      const ctx = new RequestContext(cont, req, res);
+      const ctx = new RequestContext(cont, req, res, route);
       expect(req.url).to.be.eq('/pathname?foo=bar');
       expect(ctx.path).to.be.eq('/pathname?foo=bar');
     });
@@ -120,8 +146,9 @@ describe('RequestContext', function () {
     it('returns the request pathname without the query string', function () {
       const req = createRequestMock({path: '/pathname?foo=bar'});
       const res = createResponseMock();
+      const route = createRouteMock();
       const cont = new ServiceContainer();
-      const ctx = new RequestContext(cont, req, res);
+      const ctx = new RequestContext(cont, req, res, route);
       expect(req.url).to.be.eq('/pathname?foo=bar');
       expect(ctx.pathname).to.be.eq('/pathname');
     });
@@ -129,8 +156,9 @@ describe('RequestContext', function () {
     it('sets the cache to the "_pathname" property and uses is for next accesses', function () {
       const req = createRequestMock({path: '/pathname'});
       const res = createResponseMock();
+      const route = createRouteMock();
       const cont = new ServiceContainer();
-      const ctx = new RequestContext(cont, req, res);
+      const ctx = new RequestContext(cont, req, res, route);
       expect(ctx._pathname).to.be.undefined;
       expect(ctx.pathname).to.be.eq('/pathname');
       expect(ctx._pathname).to.be.eq('/pathname');

+ 1 - 1
src/route.spec.js

@@ -381,7 +381,7 @@ describe('Route', function () {
       const req = createRequestMock();
       const res = createResponseMock();
       const cont = new ServiceContainer();
-      const ctx = new RequestContext(cont, req, res);
+      const ctx = new RequestContext(cont, req, res, route);
       const result = route.handle(ctx);
       expect(result).to.be.eq('OK');
     });

+ 2 - 7
src/trie-router.js

@@ -5,7 +5,7 @@ import {ServiceContainer} from '@e22m4u/js-service';
 import {ServerResponse, IncomingMessage} from 'http';
 import {DebuggableService} from './debuggable-service.js';
 import {DataSender, ErrorSender} from './senders/index.js';
-import {cloneDeep, isPromise, isResponseSent} from './utils/index.js';
+import {isPromise, isResponseSent} from './utils/index.js';
 import {HookInvoker, HookRegistry, RouterHookType} from './hooks/index.js';
 
 /**
@@ -91,12 +91,7 @@ export class TrieRouter extends DebuggableService {
       // в контекст запроса, что бы родительский контекст
       // нельзя было модифицировать
       const container = new ServiceContainer(this.container);
-      const context = new RequestContext(container, request, response);
-      // чтобы метаданные маршрута были доступны в хуках,
-      // их копия устанавливается в контекст запроса
-      if (route.meta != null) {
-        context.meta = cloneDeep(route.meta);
-      }
+      const context = new RequestContext(container, request, response, route);
       // регистрация контекста запроса в сервис-контейнере
       // для доступа через container.getRegistered(RequestContext)
       container.set(RequestContext, context);

+ 31 - 19
src/trie-router.spec.js

@@ -32,7 +32,7 @@ describe('TrieRouter', function () {
       expect(typeof router.requestListener).to.be.eq('function');
     });
 
-    it('passes request context to the route handler', function (done) {
+    it('provides the request context to the route handler', function (done) {
       const router = new TrieRouter();
       router.defineRoute({
         method: HttpMethod.GET,
@@ -47,7 +47,7 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
     });
 
-    it('passes path parameters to the request context', function (done) {
+    it('provides path parameters to the request context', function (done) {
       const router = new TrieRouter();
       router.defineRoute({
         method: HttpMethod.GET,
@@ -62,7 +62,7 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
     });
 
-    it('passes query parameters to the request context', function (done) {
+    it('provides query parameters to the request context', function (done) {
       const router = new TrieRouter();
       router.defineRoute({
         method: HttpMethod.GET,
@@ -77,7 +77,7 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
     });
 
-    it('passes parsed cookies to the request context', function (done) {
+    it('provides parsed cookies to the request context', function (done) {
       const router = new TrieRouter();
       router.defineRoute({
         method: HttpMethod.GET,
@@ -92,7 +92,7 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
     });
 
-    it('passes plain text body to the request context', function (done) {
+    it('provides the plain text body to the request context', function (done) {
       const router = new TrieRouter();
       const body = 'Lorem Ipsum is simply dummy text.';
       router.defineRoute({
@@ -108,7 +108,7 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
     });
 
-    it('passes parsed JSON body to the request context', function (done) {
+    it('provides the parsed JSON body to the request context', function (done) {
       const router = new TrieRouter();
       const data = {p1: 'foo', p2: 'bar'};
       router.defineRoute({
@@ -124,7 +124,7 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
     });
 
-    it('passes headers to the request context', function (done) {
+    it('provides request headers to the request context', function (done) {
       const router = new TrieRouter();
       router.defineRoute({
         method: HttpMethod.GET,
@@ -142,20 +142,32 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
     });
 
-    it('passes the route meta to the request context as a deep copy', function (done) {
+    it('provides the route to the request context', function (done) {
       const router = new TrieRouter();
       const metaData = {foo: {bar: {baz: 'qux'}}};
+      const currentRoute = router.defineRoute({
+        method: HttpMethod.GET,
+        path: '/',
+        meta: metaData,
+        handler: ({route}) => {
+          expect(route).to.be.eq(currentRoute);
+          done();
+        },
+      });
+      const req = createRequestMock();
+      const res = createResponseMock();
+      router.requestListener(req, res);
+    });
+
+    it('provides access to route meta via the request context', function (done) {
+      const router = new TrieRouter();
+      const metaData = {role: 'admin'};
       router.defineRoute({
         method: HttpMethod.GET,
         path: '/',
         meta: metaData,
         handler: ({meta}) => {
-          expect(meta).to.be.not.eq(metaData);
-          expect(meta).to.be.eql(metaData);
-          expect(meta.foo).to.be.not.eq(metaData.foo);
-          expect(meta.foo).to.be.eql(metaData.foo);
-          expect(meta.foo.bar).to.be.not.eq(metaData.foo.bar);
-          expect(meta.foo.bar).to.be.eql(metaData.foo.bar);
+          expect(meta).to.eql(metaData);
           done();
         },
       });
@@ -164,7 +176,7 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
     });
 
-    it('uses DataSender to send the response', function (done) {
+    it('uses the DataSender to send the server response', function (done) {
       const router = new TrieRouter();
       const resBody = 'Lorem Ipsum is simply dummy text.';
       router.defineRoute({
@@ -184,7 +196,7 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
     });
 
-    it('uses ErrorSender to send the response', function (done) {
+    it('uses the ErrorSender to send the server response', function (done) {
       const router = new TrieRouter();
       const error = new Error();
       router.defineRoute({
@@ -264,7 +276,7 @@ describe('TrieRouter', function () {
         expect(order).to.be.eql(['handler', 'postHandler1', 'postHandler2']);
       });
 
-      it('passes the request context to the "preHandler" hooks', async function () {
+      it('provides the request context to the "preHandler" hooks', async function () {
         const router = new TrieRouter();
         const order = [];
         const body = 'OK';
@@ -295,7 +307,7 @@ describe('TrieRouter', function () {
         expect(order).to.be.eql(['preHandler1', 'preHandler2', 'handler']);
       });
 
-      it('passes the request context and return value from the route handler to the "postHandler" hooks', async function () {
+      it('provides the request context and return value from the route handler to the "postHandler" hooks', async function () {
         const router = new TrieRouter();
         const order = [];
         const body = 'OK';
@@ -480,7 +492,7 @@ describe('TrieRouter', function () {
   });
 
   describe('_handleRequest', function () {
-    it('should register the RequestContext in the request-scope ServiceContainer', function (done) {
+    it('should register the request context in the request-scope ServiceContainer', function (done) {
       const router = new TrieRouter();
       router.defineRoute({
         method: HttpMethod.GET,

+ 19 - 0
src/utils/create-route-mock.d.ts

@@ -0,0 +1,19 @@
+import {Route, HttpMethod} from '../route.js';
+import type {RouteHandler} from '../route.js';
+
+/**
+ * Route mock options.
+ */
+type RouteMockOptions = {
+  method?: HttpMethod;
+  path?: string;
+  handler?: RouteHandler;
+};
+
+/**
+ * Create route mock.
+ *
+ * @param {Route} options
+ * @returns {Route}
+ */
+export function createRouteMock(options: RouteMockOptions): Route;

+ 22 - 0
src/utils/create-route-mock.js

@@ -0,0 +1,22 @@
+import {Route, HttpMethod} from '../route.js';
+
+/**
+ * @typedef {object} RouteMockOptions
+ * @property {HttpMethod} method
+ * @property {string} path
+ * @property {import('../route.js').RouteHandler} handler
+ */
+
+/**
+ * Create route mock.
+ *
+ * @param {Route} options
+ * @returns {Route}
+ */
+export function createRouteMock(options = {}) {
+  return new Route({
+    method: options.method || HttpMethod.GET,
+    path: options.path || '/',
+    handler: options.handler || (() => 'OK'),
+  });
+}

+ 28 - 0
src/utils/create-route-mock.spec.js

@@ -0,0 +1,28 @@
+import {expect} from 'chai';
+import {HttpMethod, Route} from '../route.js';
+import {createRouteMock} from './create-route-mock.js';
+
+describe('createRouteMock', function () {
+  it('returns an instance of Route with default options', function () {
+    const res = createRouteMock();
+    expect(res).to.be.instanceof(Route);
+    expect(res.method).to.be.eq(HttpMethod.GET);
+    expect(res.path).to.be.eq('/');
+    expect(res.handler()).to.be.eq('OK');
+  });
+
+  it('sets the "method" option', function () {
+    const res = createRouteMock({method: HttpMethod.POST});
+    expect(res.method).to.be.eq(HttpMethod.POST);
+  });
+
+  it('sets the "path" option', function () {
+    const res = createRouteMock({path: 'test'});
+    expect(res.path).to.be.eq('test');
+  });
+
+  it('sets the "handler" option', function () {
+    const res = createRouteMock({handler: () => 'Hey!'});
+    expect(res.handler()).to.be.eq('Hey!');
+  });
+});

+ 1 - 0
src/utils/index.d.ts

@@ -5,6 +5,7 @@ export * from './parse-cookies.js';
 export * from './to-camel-case.js';
 export * from './create-debugger.js';
 export * from './is-response-sent.js';
+export * from './create-route-mock.js';
 export * from './is-readable-stream.js';
 export * from './parse-content-type.js';
 export * from './is-writable-stream.js';

+ 1 - 0
src/utils/index.js

@@ -5,6 +5,7 @@ export * from './parse-cookies.js';
 export * from './to-camel-case.js';
 export * from './create-debugger.js';
 export * from './is-response-sent.js';
+export * from './create-route-mock.js';
 export * from './is-readable-stream.js';
 export * from './parse-content-type.js';
 export * from './is-writable-stream.js';