Pesquisa de site

node.js - Classe TypeScript Express Api não é um construtor ao testar com ts-node


Configurei uma aplicação Express usando classes TypeScript e encontrei um problema estranho. Todos os testes estavam passando, e hoje, quando fui atualizar algumas das rotas, meus testes não rodam mais. Quando executo meu script de teste, recebo esta mensagem de erro de volta no console:

mocha -c --reporter spec --compilers ts:ts-node/register ./test/*.test.ts --
timeout 20000

/Users/christiantodd/Development/projects/bby-react-api/src/index.ts:8
const app: Api = new Api();
             ^
TypeError: Api_1.default is not a constructor
at Object.<anonymous> (/Users/christiantodd/Development/projects/bby-react-api/src/index.ts:8:18)
at Module._compile (module.js:635:30)
at Module.m._compile (/Users/christiantodd/Development/projects/bby-react-api/node_modules/ts-node/src/index.ts:392:23)
at Module._extensions..js (module.js:646:10)
at Object.require.extensions.(anonymous function) [as .ts] (/Users/christiantodd/Development/projects/bby-react-api/node_modules/ts-node/src/index.ts:395:12)

Meu arquivo Api.ts é o seguinte:

import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as expressValidator from 'express-validator';
import * as helmet from 'helmet';
import * as morgan from 'morgan';
import * as passport from 'passport';
import * as compression from 'compression';

/* import all routers */
import BestBuyRouter from './routes/BestBuyRouter';
import UserRouter from './routes/UserRouter';

export default class Api {
  /* reference to the express instance */
  public express: express.Application;

  /* create the express instance and attach app level middleware and routes */
  constructor() {
    this.express = express();
    this.middleware();
    this.routes();
  }

  /* get current environment */
  public currentEnv(): string {
    return this.express.get('env');
  }

  /* apply middleware */
  private middleware(): void {
    this.express.use((req, res, next) => {
      /* Don't allow caching. Needed for IE support :/ */
      res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
      res.header('Pragma', 'no-cache');
      res.header('Access-Control-Allow-Origin', '*');
      res.header(
        'Access-Control-Allow-Methods',
        'PUT, GET, POST, DELETE, OPTIONS'
      );
      res.header(
        'Access-Control-Allow-Headers',
        'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials'
      );
      res.header('Access-Control-Allow-Credentials', 'true');
      next();
    });
    this.express.use(compression());
    this.express.use(helmet());
    this.express.use(morgan('dev'));
    this.express.use(bodyParser.json());
    this.express.use(bodyParser.urlencoded({ extended: false }));
    this.express.use(passport.initialize());
    this.express.use(expressValidator());
    this.express.use((err, req, res, next) => {
      console.error(err);
      res.status(err.status || 500).json({
        message: err.message,
        error: err
      });
    });
  }

  /* connect resource routers */
  private routes(): void {
    /* create an instance of the each of our routers */
    const userRouter = new UserRouter();
    const bestBuyRouter = new BestBuyRouter();

    /* attach all routers to our express app */
    this.express.use(userRouter.path, userRouter.router);
    this.express.use(bestBuyRouter.path, bestBuyRouter.router);
  }
}

e meu index.ts:

import Api from './Api';
require('dotenv').config();
const mongoose = require('mongoose');
/* Set mongoose promise to native ES6 promise */
mongoose.Promise = global.Promise;

/* Instantiate our app instance */
const app: Api = new Api();

const connectOptions = {
  useMongoClient: true,
  keepAlive: true,
  reconnectTries: Number.MAX_VALUE
};

/* Get current environment */
export const ENV = app.currentEnv();

let DATABASE_URL;
let PORT;

/* set environment variables */
if (ENV === 'production') {
  DATABASE_URL = process.env.MONGODB_URI;
  PORT = parseInt(process.env.PORT, 10);
} else {
  DATABASE_URL = process.env.TEST_DATABASE_URL;
  PORT = 3000;
}

let server;

export const runServer = async (
  dbURL: string = DATABASE_URL,
  port: number = PORT
) => {
  try {
    await mongoose.connect(dbURL, connectOptions);
    await new Promise((resolve, reject) => {
      server = app.express
        .listen(port, () => {
          console.info(`The ${ENV} server is listening on port ${port} 🤔`);
          resolve();
        })
        .on('error', err => {
          mongoose.disconnect();
          reject(err);
        });
    });
  } catch (err) {
    console.error(err);
  }
};

export const closeServer = async () => {
  try {
    await mongoose.disconnect();
    await new Promise((resolve, reject) => {
      console.info(`Closing server. Goodbye old friend.`);
      server.close(err => (err ? reject(err) : resolve()));
    });
  } catch (err) {
    console.error(err);
  }
};

require.main === module && runServer().catch(err => console.error(err));

Por último, meu tsconfig.json

{
  "compilerOptions": {
    "lib": ["dom", "es7"],
    "allowJs": true,
    "watch": true,
    "noImplicitAny": false,
    "removeComments": true,
    "sourceMap": false,
    "target": "es6",
    "module": "commonjs",
    "outDir": "./lib",
    "types": [
      "body-parser",
      "mongodb",
      "mongoose",
      "passport",
      "node",
      "nodemailer",
      "mocha",
      "chai",
      "express",
      "express-validator",
      "chai-http"
    ],
    "typeRoots": ["./node_modules/@types"]
  },
  "compileOnSave": true,
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "**/*.test.ts"]
}

Para mim, é realmente estranho ter esse comportamento de repente, quando essa configuração funcionou perfeitamente para mim no passado. Ainda consigo iniciar meu servidor sem problemas, mas por algum motivo o ts-node não quer compilar meus arquivos *test.ts para que o mocha execute meus testes. Alguma ideia do que isso poderia ser?

Resposta #1

Depuração de erros "is not a ..."

Então, isso provavelmente NÃO será a resposta, mas essa pergunta foi o resultado principal para o erro que eu estava depurando e aqui está uma dica de depuração.

Eu estava executando mocha com o seguinte:

test/mocha.opts

--require ts-node/register
--require source-map-support/register
--watch-extensions ts

Mas não importa como eu importei ./app, eu não conseguia fazer as classes ou funções funcionarem, apesar de o tsc compilar corretamente e o node e o mocha funcionarem bem nos arquivos .js compilados.

import * as app from './app'
console.log({app}); // Pretty print object

Como era de se esperar, eu tinha um projeto Heroku com app.json e app.ts.

A combinação de mocha e ts-node estava carregando a extensão de arquivo .json como prioridade mais alta em vez do arquivo .ts, e o Typescript não permite que eu especifique uma extensão de arquivo. Então, esse comportamento é diferente em tsc versus mocha + ts-node.

Pontos de Bônus - Cobertura de Código Typescript

Testes unitários: nyc mocha src/**/*-test.ts

Testes de integração: nyc mocha test/**/*.ts

package.json

{
  "nyc": {
    "extension": [
      ".ts"
    ],
    "include": [
      "src/**/*.ts"
    ],
    "exclude": [
      "src/**/*-test.ts",
      "test/**/*.ts"
    ],
    "require": [
      "ts-node/register"
    ],
    "reporter": [
      "text-summary"
    ],
    "sourceMap": true,
    "instrument": true,
    "all": true
  }
}

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "target":"es2017",

    "esModuleInterop":true,
    // "strict": true,
    "sourceMap": true,
    "outDir": "dist",
    "baseUrl": ".",
    "types":["node"],
    "rootDirs":[
      "src"    
    ]
  },
  "include": [
    "src/**/*",
    "test/**/*"
  ],
  "exclude":[
    "**/node_modules/**"
  ]

}

Artigos relacionados