【问题标题】:How to mock module which depends implicitly per test in Jest?如何在 Jest 中模拟隐含依赖于每个测试的模块?
【发布时间】:2020-10-26 16:33:06
【问题描述】:

我有一个集成测试,我在其中对 MongoDB 数据库进行实际的 DB 调用。但为了测试事务是否过期,我需要为特定测试模拟数据库。我进行实际数据库调用的原因有很多,我只是为了这个例子而提到状态。

Jest 有 jest.doMock 函数,但这仅在我想在测试中导入函数时才有用,但在我的情况下,它是我想在 express 中间件内部调用时为特定测试模拟的 DB 函数.

还有另一个选项可以模拟整个 ../db 模块,但这会使我的实际项目中的测试变得非常复杂。如果我可以模拟特定测试的数据库调用,其余的所有测试都应该进行真正的数据库调用,这对我来说会很容易。

有没有办法在 Jest 中做到这一点?

// a.ts
import express from "express"
import db from "../db";

const app = express()

app.get("/api/deduct-balance/:txn_id", (req, res) => {
  const txn = await db.findById(txn_id)
  
  // return error message if txn expired
  if (txn.exipre_at <= new Date()) {
    return res.status(401).json({ error: "txn expired" });
  }

  // otherwise update the txn state
  txn.state = "DEDUCTED";
  await txn.save()

  return res.status(200).json();
});
// a.test.ts
import db from "../db";

describe("mixed tests", () => {
  test("should make REAL db calls", async () => {
    await axios.get("/api/deduct-balance/123")
    const txn = await db.findById("123");
    expect(txn.state).toBe("DEDUCTED");
  });

  test("should use MOCKED value", async () => {
    // need a way to mock the DB call so that I can return an expired transaction
    // when I hit the API

    const { data } = await axios.get("/api/deduct-balance/123")
    
    expect(data).toBe({
      error: {
        message: "txn expired"
      }
    });
  });
})

【问题讨论】:

  • 这类测试最好不要作为集成测试来完成。如果您想测试请求处理程序的行为,您应该模拟所有依赖项以创建可预测和可重复的测试。模块db 应该是一个完整的模拟,findById 应该返回一个模拟事务等等......
  • @Bart 那么我应该做哪种类型的测试作为集成测试?有兴趣知道您对此有何看法。
  • 我留下了关于一般要点的答案。不幸的是,我不得不去,但我可以在稍后阶段扩展我的答案以使其更清楚。

标签: javascript typescript testing jestjs integration-testing


【解决方案1】:

在这种情况下,集成测试是多余的。简单的单元测试就足够了。它们执行速度很快,只测试一件事,你应该有很多。

因为您将处理程序定义为匿名函数,所以默认情况下很难进行单元测试。所以首先要做的是通过提取它来简化测试。

// deduct-balance-handlers.ts
export const deductBalanceByTransaction = async (req, res) => {
   const txn = await db.findById(txn_id)

   // return error message if txn expired
   if (txn.exipre_at <= new Date()) {
        return res.status(401).json({ error: "txn expired" });
   }

   // otherwise update the txn state
   txn.state = "DEDUCTED";
   await txn.save()

   return res.status(200).json();
}

这也将使应用程序配置更干净。

// a.ts
import express from "express"
import db from "../db";
import { deductBalanceByTransaction } from './deduct-balance-handlers';
const app = express()

app.get("/api/deduct-balance/:txn_id", deductBalanceByTransaction);

现在可以轻松地在测试中重用处理程序,而无需依赖 Web 框架或数据库。

// a.test.ts
import db from "../db";
import { deductBalanceByTransaction } from './deduct-balance-handlers';

jest.mock('../db');

describe("deduct-balance", () => {
  test("Expired transaction should respond with 401 status", async () => {
    const response = mockResponse();
    deductBalanceByTransaction(request, response);
    expect(response.status).toBe(401);
  });
})

为简单起见,我将创建模拟响应和模拟模块的部分从代码中删除。可以在此处了解有关 mock 的更多信息:https://jestjs.io/docs/en/manual-mocks

【讨论】:

  • 我非常感谢@Bart 的回答,但我故意将示例代码 sn-ps 保持小以使解释简单。在实际项目中,它比这要复杂得多,这就是我需要集成测试的原因。您上面提到的方法我在我的许多测试中都使用了它,但我在这里的主要挑战是我是否可以模拟仅在单个测试中隐含依赖的模块。如果您能告诉我这在 Jest 中是否可行,那将非常有帮助。 :)
  • 我首先要考虑的一件事是考虑代码的架构。它会妨碍测试吗?如果确实如此,则表明代码的设计有问题。在您的示例中,处理程序有很多职责,例如与数据库交谈并决定边缘情况。通常请求处理程序只做一件事来处理服务层对象的输入(请求)和输出(响应)。它不应该做任何其他事情,因为它是一个 HTTP 传输层。如果设计简单,您将拥有更少的依赖关系并更少处理模拟。
  • 您想要的测试解决方案不是一个好的解决方案。如果您在不同的套件/文件中运行测试,则模拟一个模块可以工作,但我不推荐它,因为它令人困惑。您很可能会使测试过于复杂,因为在当前设计中无法轻松测试代码。
  • 好吧,假设我的代码设计不好,我想改变它。您是否有某些开源库作为示例,最好是后端项目?或者其他人的,我可以寻找某种方法。
  • 这不是你可以从其他项目中学到的东西。你会错过为什么它被设计成这样的意图。它更多地是关于软件工程,并将事物分解成合理的模块。将责任和依赖降至最低,并一遍又一遍地重新考虑您的设计。
猜你喜欢
  • 2020-01-11
  • 2019-04-04
  • 2017-09-25
  • 2018-06-28
  • 2013-07-07
  • 2020-02-12
  • 2022-01-05
  • 1970-01-01
  • 2021-08-27
相关资源
最近更新 更多