TypeError: Cannot read properties of undefined (reading 'listen') when testing with Jest, Supertest, Express, Typescript

huangapple go评论69阅读模式
英文:

TypeError: Cannot read properties of undefined (reading 'listen') when testing with Jest, Supertest, Express, Typescript

问题

在运行jest和supertest时,我在实际描述的测试之前遇到了一个错误。服务器在使用启动脚本时正常运行,应用程序已定义。但是在运行测试脚本时,应用程序未定义。

错误:

 FAIL  src/components/users/user.test.ts
  ● Test suite failed to run

    TypeError: Cannot read properties of undefined (reading 'listen')

       6 |
       7 | dbConnection();
    >  8 | export const server = app.listen(config.server.port, () => {
         |                           ^
       9 |     logger.info(`Server is running on port: ${config.server.port}`);
      10 | });
      11 |

      at Object.<anonymous> (src/index.ts:8:27)
      at Object.<anonymous> (src/library/exitHandler/exitHandler.ts:2:1)
      at Object.<anonymous> (src/library/errorHandler/errorHandler.ts:2:1)
      at Object.<anonymous> (src/middleware/validateSchema.ts:3:1)
      at Object.<anonymous> (src/components/users/routes.ts:2:1)
      at Object.<anonymous> (src/server.ts:2:1)
      at Object.<anonymous> (src/components/users/user.test.ts:2:1)

这个错误表明在导入app并尝试在其上调用listen方法时出现问题。在你的user.test.ts文件中,你有以下导入:

import app from '../../server';

然后,在server.ts文件中,你导出了app:

export default app;

从错误消息中看,问题可能出现在导入app的方式或顺序上。请确保导入和导出的路径正确,并且模块导入顺序没有问题。检查这些方面,看看是否有任何明显的问题。

此外,还可以确保你的测试环境正确设置,确保你使用了相同的环境文件。如果问题仍然存在,你可能需要进一步调查代码中的其他问题,比如导入和依赖项的设置等。希望这些提示对你有所帮助。

英文:

Problem:

when running jest and supertest, i get an error before it reaches the actual tests i've described. Server runs fine using the start script, app is defined. But when running test script, app is undefined.

Background:

  • I'm fairly new to typescript and this is the first time I'm using any kind of testing.
  • I want to seperate the server instance, as seen on several blog posts and tutorials, as i plan on running multiple tests in different files.

If you have any suggestions even if they are something I already tried, ill try again and let you know. I am at my wits end so any help is much apprecitated. Thank you.

Error:

 FAIL  src/components/users/user.test.ts
  ● Test suite failed to run

    TypeError: Cannot read properties of undefined (reading &#39;listen&#39;)

       6 |
       7 | dbConnection();
    &gt;  8 | export const server = app.listen(config.server.port, () =&gt; {
         |                           ^
       9 |     logger.info(`Server is running on port: ${config.server.port}`);
      10 | });
      11 |

      at Object.&lt;anonymous&gt; (src/index.ts:8:27)
      at Object.&lt;anonymous&gt; (src/library/exitHandler/exitHandler.ts:2:1)
      at Object.&lt;anonymous&gt; (src/library/errorHandler/errorHandler.ts:2:1)
      at Object.&lt;anonymous&gt; (src/middleware/validateSchema.ts:3:1)
      at Object.&lt;anonymous&gt; (src/components/users/routes.ts:2:1)
      at Object.&lt;anonymous&gt; (src/server.ts:2:1)
      at Object.&lt;anonymous&gt; (src/components/users/user.test.ts:2:1)

user.test.ts

import request from &#39;supertest&#39;;
import app from &#39;../../server&#39;;

describe(&#39;User registration&#39;, () =&gt; {
    it(&#39;POST /register --&gt; return new user instance&#39;, async () =&gt; {
        await request(app)           // error occurs when reaching this point
            .post(&#39;/user/register&#39;)
            .send({
                firstName: &#39;Thomas&#39;,
                lastName: &#39;Haek&#39;,
                email: &#39;thomashaek@gmail.com&#39;,
                password: &#39;12345678aA&#39;,
                confirmPassword: &#39;12345678aA&#39;
            })
            .expect(201)
            .then((response) =&gt; {
                expect(response.body).toEqual(
                    expect.objectContaining({
                        _id: expect.any(String),
                        firstName: expect.any(String),
                        lastName: expect.any(String),
                        email: expect.any(String),
                        token: expect.any(String)
                    })
                );
            });
    });
});

server.ts

import express, { Application } from &#39;express&#39;;
import userRouter from &#39;./components/users/routes&#39;;
import { routeErrorHandler } from &#39;./middleware/errorHandler&#39;;
import httpLogger from &#39;./middleware/httpLogger&#39;;
import &#39;./process&#39;;

const app: Application = express();

app.use(httpLogger);
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(&#39;/user&#39;, userRouter);
app.use(routeErrorHandler);

export default app

index.ts

import { createHttpTerminator } from &#39;http-terminator&#39;;
import config from &#39;./config/config&#39;;
import dbConnection from &#39;./config/dbConnection&#39;;
import logger from &#39;./library/logger&#39;;
import app from &#39;./server&#39;

dbConnection();
export const server = app.listen(config.server.port, () =&gt; {
    logger.info(`Server is running on port: ${config.server.port}`);
});

export const httpTerminator = createHttpTerminator({ server });

package.json scripts

&quot;scripts&quot;: {
    &quot;test&quot;: &quot;env-cmd -f ./src/config/test.env jest --watchAll&quot;,
    &quot;start&quot;: &quot;env-cmd -f ./src/config/dev.env node build/index.js&quot;,

  },

tsconfig.json

{
    &quot;compilerOptions&quot;: {
        &quot;outDir&quot;: &quot;./build&quot;,
        &quot;forceConsistentCasingInFileNames&quot;: true,
        &quot;strict&quot;: true,
        &quot;skipLibCheck&quot;: true,
        &quot;module&quot;: &quot;commonjs&quot;,
        &quot;esModuleInterop&quot;: true,
        &quot;allowSyntheticDefaultImports&quot;: true,
        &quot;target&quot;: &quot;es6&quot;,
        &quot;noImplicitAny&quot;: true,
        &quot;moduleResolution&quot;: &quot;node&quot;,
        &quot;sourceMap&quot;: true,
    },
    &quot;include&quot;: [&quot;src/**/*&quot;]
}

jest.config.ts

import { Config } from &#39;jest&#39;;

/** @type {import(&#39;ts-jest&#39;).JestConfigWithTsJest} */
const config: Config = {
    preset: &#39;ts-jest&#39;,
    testEnvironment: &#39;node&#39;,
    roots: [&#39;./src&#39;],
    moduleFileExtensions: [&#39;js&#39;, &#39;ts&#39;],
    clearMocks: true,
    collectCoverage: true,
    coverageDirectory: &#39;coverage&#39;,
    coveragePathIgnorePatterns: [&#39;/node_modules/&#39;, &#39;/src/config/&#39;],
    coverageProvider: &#39;v8&#39;,
    coverageReporters: [&#39;json&#39;, &#39;text&#39;, &#39;lcov&#39;, &#39;clover&#39;],
    verbose: true
};

export default config;

exitHandler.ts

import mongoose from &#39;mongoose&#39;;
import { httpTerminator, server } from &#39;../..&#39;;


import logger from &#39;../logger&#39;;

class ExitHandler {
    public async handleExit(code: number, timeout = 5000): Promise&lt;void&gt; {
        try {
            logger.info(`Attempting graceful shutdown with code: ${code}`);
            setTimeout(() =&gt; {
                logger.info(`Forcing a shutdown with code: ${code}`);
                process.exit(code);
            }, timeout).unref();

            if (server.listening) {
                logger.info(&#39;Terminating HTTP connections&#39;);
                await httpTerminator.terminate();
                await mongoose.connection.close();
            }
            logger.info(`Exiting gracefully with code ${code}`);
            process.exit(code);
        } catch (error) {
            logger.error(error);
            logger.error(
                `Unable to shutdown gracefully... Forcing exit with code: ${code}`
            );
            process.exit(code);
        }
    }
}

export const exitHandler = new ExitHandler();

Things I've tried:

  • using the same env files for both test and server script (same error)
  • messing around with the tsconfig and jest config files (same error)
  • using module.exports = app instead of export default app or export const server = app (same error)
  • commenting out all the middleware and routes and just exporting the app (same error)

答案1

得分: 3

我认为这是由循环依赖引起的。从错误堆栈中可以看到:

在 src/index.ts:8:27 处:

在 src/library/exitHandler/exitHandler.ts:2:1 处:

...

在 src/server.ts:2:1 处:

在 src/components/users/user.test.ts:2:1 处:

我注意到 server.ts 依赖于 exitHandler.ts,而 exitHandler.ts 又依赖于 index.ts。但在 index.ts 中,你使用 import app from './server' 形成了一个循环依赖。

更具体地说,为了创建 server.ts 中的 app,它需要 exitHandler,但这个东西需要 index,而 index 需要 server。这就像没有基本情况返回的递归。与无限函数递归不同,依赖解析只会将 app 视为 undefined

因此,你需要打破这个循环。使用一些依赖注入的技巧来解除 exitHandlerindex 之间的关联将有所帮助。

如果你不知道如何做到这一点,请发布 exitHandler.ts 的代码,我会跟进。


尝试这个方法代替 import { httpTerminator, server } from '../..';

let server, httpTerminator;

export function injectDependency(s, h) {
  server = s;
  httpTerminator = h;
}

现在在 index.ts 中:

import { injectDependency } from "../exitHandler";

injectDependency(server, httpTerminator);
英文:

I believe this is caused by circular dependency. From the error stack

  at Object.&lt;anonymous&gt; (src/index.ts:8:27)
  at Object.&lt;anonymous&gt; (src/library/exitHandler/exitHandler.ts:2:1
  …
  at Object.&lt;anonymous&gt; (src/server.ts:2:1)
  at Object.&lt;anonymous&gt; (src/components/users/user.test.ts:2:1)

I see that server.ts deps on exitHandler.ts which in turn deps on index.ts. But in index.ts you import app from &#39;./server&#39; forming a circle.

More specifically, in order for app from server.ts to be created, it needs exitHandler, but that thing needs index, and index needs server. It’s like recursion without a base case return. Unlike indefinite function recursion, dependency resolution will just give you app as undefined.

Thus you need to break the circle. Use some dependency injection trick to break the tie between exitHandler and index will do.

If you don’t know how to do that, post the exitHandler.ts code and I’ll follow up.


Instead of import { httpTerminator, server } from &#39;../..&#39;; try this:

let server, httpTerminator;

export function injectDependency(s, h) {
  server = s;
  httpTerminator = h;
}

Now in index.ts

import { injectDependency } from &quot;/exitHandler&quot;

injectDependency(server, httpTerminator);

huangapple
  • 本文由 发表于 2023年1月8日 01:37:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/75042451.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定