【问题标题】:mocha test with redis sinon stub throwing uncaught error outside suite带有 redis sinon 存根的 mocha 测试在套件外抛出未捕获的错误
【发布时间】:2021-09-19 09:07:45
【问题描述】:

我无法让 Sinon 正确模拟 redis 以进行单元测试。测试用例通过了,但 Mocha 仍然每次都因 redis 连接错误而崩溃,我无法深入了解它。经过几天的工作并通过 Stackoverflow 梳理我仍然无法解决它,所以是时候问比我更好的头脑了。

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

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 创建了一个类似的设置并且它可以工作,这让我相信这可以完成并且错误是我的一部分。

【问题讨论】:

    标签: typescript express redis mocha.js sinon


    【解决方案1】:

    我能够用这个回答我自己的问题(终于!)看来我需要确定每个请求的 redis 客户端的创建范围,并确保它在请求完成时退出回调。我从上面的博客文章中借用并重建了redis-cache.ts 文件,以另一种方式将redis调用转换为promise。我的新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;
    }
    

    通过这些更改,我只需要追踪几个使用它的地方(在用于缓存会话令牌的身份验证中间件中是迄今为止唯一的东西)并且所有测试都是绿色的,并且手动运行应用程序并使用邮递员按预期工作。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-16
      • 2017-11-21
      • 2018-03-15
      • 2012-02-19
      • 1970-01-01
      • 1970-01-01
      • 2014-09-21
      • 1970-01-01
      相关资源
      最近更新 更多