使用 redis sinon 存根的摩卡测试在套件外抛出未捕获的错误

mocha test with redis sinon stub throwing uncaught error outside suite

我无法让 Sinon 正确模拟 redis 以进行单元测试。测试用例通过了,但 Mocha 每次仍然因 redis 连接错误而崩溃,我无法深究。经过几天的努力并通过 Whosebug 进行梳理后,我仍然无法解决它,所以是时候请教比我更好的人了。

我从一个 server.ts 文件开始,该文件将导出应用程序,因此可以在外部加载最终 运行time 配置或加载到测试套件中:

import express, { Application } from "express";
import bodyParser from "body-parser";
import auth from "./routes/authRoutes";

const createServer = () => {
  const app: Application = express();

  app.use(bodyParser.json());

  app.get("/", (_req, res: Response, _next) => {
    res.json({
      message: "Hello World",
    });
  });

  app.use("/auth", auth);

  return app;
};

export default createServer;

authRoutes.ts 文件相当简单,为简洁起见,我合并了验证中间件和端点逻辑:

import { Router, Request, Response, NextFunction } from "express";
import { check, validationResult } from "express-validator";
import redisCache from "../data/redis-cache";

const router = Router();

const postAuth = async (req: Request, res: Response, _next: NextFunction) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json(errors.array());
  }
  const sessionId = Math.random().toString();
  await redisCache.setAsync(sessionId, "session1");
  return res.status(200).json({ sessionId: sessionId });
};

router.post(
  "/login",
  [
    check("body").exists().withMessage("No Post Body"),
  ],
  postAuth
);

export default router;

我还设置了一个 redis-cache.ts 文件来承诺和导出我需要的 redis 函数:

import redis from "redis";

import { promisify } from "util";

const client = redis.createClient({
  host: process.env.REDIS_HOST || "localhost",
  port: process.env.REDIS_PORT ? +process.env.REDIS_PORT : 6379,
});

const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.set).bind(client);

export default {
  getAsync,
  setAsync,
};

测试套件 index.spec.ts 很简单,设置了 3 个测试:

import chai, { expect } from "chai";
import chaiHttp from "chai-http";
import * as sinon from "sinon";
import createServer from "../src/server";
import redis from "redis";

chai.use(chaiHttp);
chai.should();

describe("auth routes", function () {
  const mock = sinon.createSandbox();

  before(function () {
    mock.stub(redis, "createClient").returns({
      set: (key: string, value: string, cb: (e: Error | null) => void) => {
        console.log(`mocked set, request: ${key} -> ${value}`);
        return cb(null);
      },
      get: (key: string, cb: (e: Error | null) => void) => {
        console.log(`mocked get, request: ${key} `);
        return cb(null);
      },
      quit: (_cb: () => void) => {
        console.log(`mocked quit method`);
      },
    } as any);
  });

  after(function () {
    mock.restore();
  });

  describe("#GET/login", function () {
    it("responds with 404", function (done) {
      const app = createServer();
      chai
        .request(app)
        .get("/auth/login")
        .end((err: any, res: any) => {
          expect(err).to.be.null;
          expect(res.status).to.equal(404);
          done();
        });
    });
  });

  describe("#POST/login", function () {
    const app = createServer();
    function requestSend() {
      return chai.request(app).post("/auth/login");
    }

    describe("without body", function () {
      it("responds with 422", function (done) {
        requestSend().end(function (err: any, res: any) {
          if (err) return done(err);
          expect(res.status).to.equal(422);
          done();
        });
      });

      it("returns error when no post body sent", function (done) {
        requestSend().end(function (err: any, res: any) {
          if (err) return done(err);
          expect(res.body).to.be.a("array");
          const body = res.body as { msg: string }[];
          const messageIndex = body.findIndex(
            (el) => el.msg === "No Post Body"
          );
          expect(messageIndex).to.not.equal(-1);
          done();
        });
      });
    });
  });
});

现在我 运行 使用 Mocha 进行测试并得到以下结果:

auth routes
    Uncaught error outside test suite
    #GET/login
      ✔ responds with 404
    #POST/login
      without body
        ✔ responds with 422 (45ms)
        ✔ returns error when no post body sent


  3 passing (217ms)
  1 failing

  1) auth routes
       Uncaught error outside test suite:
     Uncaught Redis connection to localhost:6379 failed - connect ECONNREFUSED 127.0.0.1:6379
  Error: connect ECONNREFUSED 127.0.0.1:6379

如您所见,所有测试都通过了,但 Redis 仍在尝试连接测试套件外部,我不知道如何绕过它。如果我在我的 authRoutes.ts 文件中注释掉对 await redisCache.setAsync 的调用并且 运行 测试,所有绿色且没有错误。

我花了几个小时在谷歌上搜索和尝试,但没有成功,我很确定我遗漏了一些东西,但我找不到。任何帮助将不胜感激。

我按照 this blog post 创建了一个类似的设置并且它起作用了,这让我相信这是可以做到的,错误是我自己造成的。

我能够用这个问题回答我自己的问题(终于!)看来我需要对每个请求的 redis 客户端的创建范围进行限制,并确保它在请求完成时在回调中退出。我从上面的博客 post 借用并重建了 redis-cache.ts 文件,以另一种方式将 redis 调用变成 promises。我的新 redis-cache.ts 看起来像这样:

import * as redis from "redis";
import {
  SuccessfulSetResponse,
  FailResponse,
  SuccessfulGetResponse,
} from "../types";

const url = `${process.env.REDIS_HOST || "localhost"}:${
  process.env.REDIS_PORT ? +process.env.REDIS_PORT : 6379
}`;

const getAsync = async (
  key: string
): Promise<SuccessfulGetResponse | FailResponse> => {
  return new Promise((resolve, _reject) => {
    const client = redis.createClient({ url });

    client.get(key, (error, value) => {
      // note the client quits in the callback
      client.quit();

      if (error) {
        resolve({
          reason: error.message,
          ...error,
        } as FailResponse);
      }
      if (value === null) {
        resolve({
          success: false,
        } as SuccessfulGetResponse);
      }

      resolve({
        success: true,
        value: value,
      } as SuccessfulGetResponse);
    });
  });
};

const setAsync = async (
  key: string,
  value: string
): Promise<SuccessfulSetResponse | FailResponse> => {
  return new Promise((resolve, _reject) => {
    const client = redis.createClient({ url });

    client.set(key, value, (error) => {
      // note the client quits in the callback
      client.quit();

      if (error) {
        resolve({
          reason: error.message,
          ...error,
        } as FailResponse);
      }

      resolve({
        success: true,
      } as SuccessfulSetResponse);
    });
  });
};

export default {
  getAsync,
  setAsync,
};

我还在 src/types.d.ts 文件中添加了一些类型定义:

export interface SuccessfulSetResponse {
  success: boolean;
}

export interface SuccessfulGetResponse {
  success: boolean;
  value?: string;
}

export interface FailResponse {
  reason: string;
  [key: string]: string;
}

通过这些更改,我只需要追踪几个使用它的地方(目前唯一用于缓存会话令牌的 auth 中间件)并且所有测试都是绿色的,并且 运行手动应用程序并使用 postman 也可以按预期工作。

我仍然很想知道到底发生了什么,如果有人知道请发表评论和/或编辑此答案以提供关于为什么会这样工作的见解。