使用 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 也可以按预期工作。
我仍然很想知道到底发生了什么,如果有人知道请发表评论和/或编辑此答案以提供关于为什么会这样工作的见解。
我无法让 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 也可以按预期工作。
我仍然很想知道到底发生了什么,如果有人知道请发表评论和/或编辑此答案以提供关于为什么会这样工作的见解。