使用Mocha,Chai和Sinon单元测试Node.js应用程序

2020年12月30日11:21:27 发表评论 34 次浏览

本文概述

测试有助于记录应用程序的核心功能。正确编写的测试可确保新功能不会引入会破坏应用程序的更改。

维护代码库的工程师不一定与编写初始代码的工程师相同。如果对代码进行了正确的测试, 则另一位工程师可以放心地添加新代码或修改现有代码, 并期望新更改不会破坏其他功能, 或者至少不会对其他功能造成副作用。

JavaScript和Node.js具有如此众多的测试和断言库, 例如笑话, 茉莉花, Qunit和摩卡咖啡。但是, 在本文中, 我们将研究如何使用摩卡咖啡供测试用, hai对于断言和诗乃用于模拟, 间谍和存根。

摩卡咖啡

摩卡咖啡是在Node.js和浏览器中运行的功能丰富的JavaScript测试框架。它将测试封装在测试套件(描述块)和测试用例(it-block)中。

摩卡咖啡具有许多有趣的功能:

  • 浏览器支持
  • 简单的异步支持, 包括承诺
  • 测试覆盖率报告
  • 异步测试超时支持
  • 之前, 后, 之前, 每次之后钩子等

hai

为了进行相等性检查或将预期结果与实际结果进行比较, 我们可以使用Node.js内置的断言模块。但是, 当发生错误时, 测试用例仍将通过。因此, Mocha建议使用其他断言库, 在本教程中, 我们将使用hai.

Chai公开了三个断言接口:Expect(), assert()和should()。它们中的任何一个都可以用于断言。

诗乃

通常, 正在测试的方法需要与其他外部方法进行交互或调用其他外部方法。因此, 你需要一个实用程序来监视, 存根或模拟那些外部方法。就是这样诗乃为你做。

存根, 模拟和间谍程序可以使测试更加健壮, 并且在依赖代码不断发展或内部结构被修改的情况下更不容易损坏。

间谍

一种间谍是一个伪函数, 它跟踪参数, 返回值, 这个并为其引发所有异常(如果有)来电.

我们为制作了一个自定义演示.
不完全是。点击这里查看.

使用Mocha,Chai和Sinon单元测试Node.js应用程序1

存根

一种存根是具有预定行为的间谍。

我们可以使用存根来执行以下操作:

  • 采取预定动作, 例如引发异常
  • 提供预定的响应
  • 防止直接调用特定方法(尤其是当它触发诸如HTTP请求之类的不良行为时)

嘲笑

一种嘲笑是具有预编程行为(如存根)和预编程期望的伪函数(如间谍)。

我们可以使用模拟来:

  • 验证被测代码与其调用的外部方法之间的约定
  • 验证外部方法被调用的次数正确
  • 验证是否使用正确的参数调用了外部方法

模拟的经验法则是:如果你不打算为某个特定的调用添加断言, 请不要模拟它。请改用存根。

编写测试

为了演示我们上面解释的内容, 我们将构建一个简单的节点应用程序来创建和检索用户。你可以在以下位置找到本文的完整代码示例CodeSandbox.

项目设置

让我们为用户应用项目创建一个新的项目目录:

mkdir mocha-unit-test && cd mocha-unit-test
mkdir src

创建一个package.json文件放在源文件夹中, 并添加以下代码:

// src/package.json
{
  "name": "mocha-unit-test", "version": "1.0.0", "description": "", "main": "app.js", "scripts": {
    "test": "mocha './src/**/*.test.js'", "start": "node src/app.js"
  }, "keywords": [
    "mocha", "chai"
  ], "author": "Godwin Ekuma", "license": "ISC", "dependencies": {
    "body-parser": "^1.18.3", "dotenv": "^6.2.0", "express": "^4.16.4", "jsonwebtoken": "^8.4.0", "morgan": "^1.9.1", "pg": "^7.12.1", "pg-hstore": "^2.3.3", "sequelize": "^5.19.6"
  }, "devDependencies": {
    "chai": "^4.2.0", "mocha": "^6.2.1", "sinon": "^7.5.0", "faker": "^4.1.0"
  }
}

运行npm安装安装项目依赖项。

注意与测试相关的软件包摩卡咖啡, 柴, 西农和骗子保存在dev-dependencies中。

的测试脚本使用自定义球状(./src/**/*.test.js)以配置测试文件的文件路径。 Mocha会寻找测试文件(文件结尾为.test.js)在目录和子目录中src夹。

储存库, 服务和控制器

我们将使用控制器, 服务和, 资料库模式, 因此我们的应用程序将分为存储库, 服务和控制器。 Repository-Service-Controller模式将应用程序的业务层分为三个不同的层:

  • 信息库类处理将数据移入和移出数据存储区的过程。在服务层和模型层之间使用存储库。例如, 在用户资料库你将创建将用户写入数据库或从数据库读取用户的方法
  • 服务类调用存储库类, 并且可以将其数据合并以形成新的, 更复杂的业务对象。它是控制器和存储库之间的抽象。例如, 用户服务将负责执行所需的逻辑以创建新用户
  • 控制器包含很少的逻辑, 用于调用服务。除非有充分的理由, 否则控制器很少会直接调用存储库。控制器将对从服务返回的数据执行基本检查, 以便将响应发送回客户端

以这种方式分解应用程序使测试变得容易。

UserRepository类

首先创建一个存储库类:

// src/user/user.repository.js
const { UserModel } = require("../database");
class UserRepository {
  constructor() {
    this.user = UserModel;
    this.user.sync({ force: true });
  }
  async create(name, email) {
    return this.user.create({
      name, email
    });
  }
  async getUser(id) {
    return this.user.findOne({ id });
  }
}
module.exports = UserRepository;

的用户资料库类有两种方法, 创造和getUser。的创造方法在将新用户添加到数据库的同时getUser方法从数据库中搜索用户。

让我们测试一下userRepository方法如下:

// src/user/user.repository.test.js
const chai = require("chai");
const sinon = require("sinon");
const expect = chai.expect;
const faker = require("faker");
const { UserModel } = require("../database");
const UserRepository = require("./user.repository");
describe("UserRepository", function() {
  const stubValue = {
    id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past()
  };
  describe("create", function() {
    it("should add a new user to the db", async function() {
      const stub = sinon.stub(UserModel, "create").returns(stubValue);
      const userRepository = new UserRepository();
      const user = await userRepository.create(stubValue.name, stubValue.email);
      expect(stub.calledOnce).to.be.true;
      expect(user.id).to.equal(stubValue.id);
      expect(user.name).to.equal(stubValue.name);
      expect(user.email).to.equal(stubValue.email);
      expect(user.createdAt).to.equal(stubValue.createdAt);
      expect(user.updatedAt).to.equal(stubValue.updatedAt);
    });
  });
});

上面的代码正在测试创造的方法用户资料库。请注意, 我们正在对UserModel.create方法。存根是必需的, 因为我们的目标是测试存储库而不是模型。我们用骗子对于测试夹具:

// src/user/user.repository.test.js

const chai = require("chai");
const sinon = require("sinon");
const expect = chai.expect;
const faker = require("faker");
const { UserModel } = require("../database");
const UserRepository = require("./user.repository");

describe("UserRepository", function() {
  const stubValue = {
    id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past()
  };
   describe("getUser", function() {
    it("should retrieve a user with specific id", async function() {
      const stub = sinon.stub(UserModel, "findOne").returns(stubValue);
      const userRepository = new UserRepository();
      const user = await userRepository.getUser(stubValue.id);
      expect(stub.calledOnce).to.be.true;
      expect(user.id).to.equal(stubValue.id);
      expect(user.name).to.equal(stubValue.name);
      expect(user.email).to.equal(stubValue.email);
      expect(user.createdAt).to.equal(stubValue.createdAt);
      expect(user.updatedAt).to.equal(stubValue.updatedAt);
    });
  });
});

测试getUser方法, 我们还必须存根UserModel.findone。我们用期望(存根称为一次)为真断言存根至少被调用一次。其他断言正在检查由getUser方法。

UserService类

// src/user/user.service.js

const UserRepository = require("./user.repository");
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }
  async create(name, email) {
    return this.userRepository.create(name, email);
  }
  getUser(id) {
    return this.userRepository.getUser(id);
  }
}
module.exports = UserService;

的用户服务类也有两种方法创造和getUser。的创造方法调用创造存储库方法, 将新用户的名称和电子邮件作为参数传递。的getUser调用存储库getUser方法。

让我们测试一下userService方法如下:

// src/user/user.service.test.js

const chai = require("chai");
const sinon = require("sinon");
const UserRepository = require("./user.repository");
const expect = chai.expect;
const faker = require("faker");
const UserService = require("./user.service");
describe("UserService", function() {
  describe("create", function() {
    it("should create a new user", async function() {
      const stubValue = {
        id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past()
      };
      const userRepo = new UserRepository();
      const stub = sinon.stub(userRepo, "create").returns(stubValue);
      const userService = new UserService(userRepo);
      const user = await userService.create(stubValue.name, stubValue.email);
      expect(stub.calledOnce).to.be.true;
      expect(user.id).to.equal(stubValue.id);
      expect(user.name).to.equal(stubValue.name);
      expect(user.email).to.equal(stubValue.email);
      expect(user.createdAt).to.equal(stubValue.createdAt);
      expect(user.updatedAt).to.equal(stubValue.updatedAt);
    });
  });
});

上面的代码正在测试UserService创建方法。我们为存储库创建了一个存根创造方法。以下代码将测试getUser服务方法:

const chai = require("chai");
const sinon = require("sinon");
const UserRepository = require("./user.repository");
const expect = chai.expect;
const faker = require("faker");
const UserService = require("./user.service");
describe("UserService", function() {
  describe("getUser", function() {
    it("should return a user that matches the provided id", async function() {
      const stubValue = {
        id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past()
      };
      const userRepo = new UserRepository();
      const stub = sinon.stub(userRepo, "getUser").returns(stubValue);
      const userService = new UserService(userRepo);
      const user = await userService.getUser(stubValue.id);
      expect(stub.calledOnce).to.be.true;
      expect(user.id).to.equal(stubValue.id);
      expect(user.name).to.equal(stubValue.name);
      expect(user.email).to.equal(stubValue.email);
      expect(user.createdAt).to.equal(stubValue.createdAt);
      expect(user.updatedAt).to.equal(stubValue.updatedAt);
    });
  });
});

再次, 我们存根UserRepository getUser方法。我们还断言该存根至少调用一次, 然后断言该方法的返回值正确。

UserContoller类

/ src/user/user.controller.js

class UserController {
  constructor(userService) {
    this.userService = userService;
  }
  async register(req, res, next) {
    const { name, email } = req.body;
    if (
      !name ||
      typeof name !== "string" ||
      (!email || typeof email !== "string")
    ) {
      return res.status(400).json({
        message: "Invalid Params"
      });
    }
    const user = await this.userService.create(name, email);
    return res.status(201).json({
      data: user
    });
  }
  async getUser(req, res) {
    const { id } = req.params;
    const user = await this.userService.getUser(id);
    return res.json({
      data: user
    });
  }
}
module.exports = UserController;

的用户控制器班有寄存器和getUser方法。这些方法中的每一个都接受两个参数要求和资源对象。

// src/user/user.controller.test.js

describe("UserController", function() {
  describe("register", function() {
    let status json, res, userController, userService;
    beforeEach(() => {
      status = sinon.stub();
      json = sinon.spy();
      res = { json, status };
      status.returns(res);
      const userRepo = sinon.spy();
      userService = new UserService(userRepo);
    });
    it("should not register a user when name param is not provided", async function() {
      const req = { body: { email: faker.internet.email() } };
      await new UserController().register(req, res);
      expect(status.calledOnce).to.be.true;
      expect(status.args\[0\][0]).to.equal(400);
      expect(json.calledOnce).to.be.true;
      expect(json.args\[0\][0].message).to.equal("Invalid Params");
    });
    it("should not register a user when name and email params are not provided", async function() {
      const req = { body: {} };
      await new UserController().register(req, res);
      expect(status.calledOnce).to.be.true;
      expect(status.args\[0\][0]).to.equal(400);
      expect(json.calledOnce).to.be.true;
      expect(json.args\[0\][0].message).to.equal("Invalid Params");
    });
    it("should not register a user when email param is not provided", async function() {
      const req = { body: { name: faker.name.findName() } };
      await new UserController().register(req, res);
      expect(status.calledOnce).to.be.true;
      expect(status.args\[0\][0]).to.equal(400);
      expect(json.calledOnce).to.be.true;
      expect(json.args\[0\][0].message).to.equal("Invalid Params");
    });
    it("should register a user when email and name params are provided", async function() {
      const req = {
        body: { name: faker.name.findName(), email: faker.internet.email() }
      };
      const stubValue = {
        id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past()
      };
      const stub = sinon.stub(userService, "create").returns(stubValue);
      userController = new UserController(userService);
      await userController.register(req, res);
      expect(stub.calledOnce).to.be.true;
      expect(status.calledOnce).to.be.true;
      expect(status.args\[0\][0]).to.equal(201);
      expect(json.calledOnce).to.be.true;
      expect(json.args\[0\][0].data).to.equal(stubValue);
    });
  });
});

在前三个it块, 我们正在测试如果未提供一个或两个必需参数(电子邮件和姓名), 则不会创建用户。请注意, 我们正在对状态并监视res.json:

describe("UserController", function() {
  describe("getUser", function() {
    let req;
    let res;
    let userService;
    beforeEach(() => {
      req = { params: { id: faker.random.uuid() } };
      res = { json: function() {} };
      const userRepo = sinon.spy();
      userService = new UserService(userRepo);
    });
    it("should return a user that matches the id param", async function() {
      const stubValue = {
        id: req.params.id, name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past()
      };
      const mock = sinon.mock(res);
      mock
        .expects("json")
        .once()
        .withExactArgs({ data: stubValue });
      const stub = sinon.stub(userService, "getUser").returns(stubValue);
      userController = new UserController(userService);
      const user = await userController.getUser(req, res);
      expect(stub.calledOnce).to.be.true;
      mock.verify();
    });
  });
});

为了getUser测试我们嘲笑的JSON方法。请注意, 我们还必须使用间谍用户资料库在创建新的实例时用户服务.

总结

使用以下命令运行测试:

npm test

你应该看到测试通过:

通过摩卡柴的单元测试

我们已经看到了如何结合使用摩卡咖啡, hai和诗乃为节点应用程序创建健壮的测试。请务必查看它们各自的文档, 以扩大你对这些工具的了解。有问题或评论吗?请在下面的评论部分中放置它们。

只有200 监视生产中失败和缓慢的网络请求

部署基于节点的Web应用程序或网站很容易。确保你的Node实例继续为你的应用程序提供资源是一件很困难的事情。如果你希望确保成功完成对后端或第三方服务的请求,

尝试notlogy

.

LogRocket网络请求监控

https://notlogy.com/signup/

日志火箭就像Web应用程序的DVR一样, 实际上记录了你网站上发生的一切。无需猜测问题发生的原因, 你可以汇总并报告有问题的网络请求, 以快速了解根本原因。

notlogy用你的应用程序记录基线性能计时, 例如页面加载时间, 到第一个字节的时间, 缓慢的网络请求, 并记录Redux, NgRx和Vuex的操作/状态。

免费开始监控

.

一盏木

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: