【问题标题】:Having trouble stubbing an asynchronous function存根异步函数时遇到问题
【发布时间】:2022-01-05 10:12:04
【问题描述】:

我目前正在使用 MERN 堆栈开发一个全堆栈应用程序。我一直在为每个路由的请求处理程序编写单元测试,并且在测试错误案例时发现了一些困难,特别是在尝试存根函数以拒绝承诺时

我有如下所示的相关代码:

我的一个端点。请求处理委托给 userController

const express = require("express");
const { body } = require("express-validator");

const router = express.Router();

const userController = require("../../controllers/user");

router.post(
  "/",
  body("username")
    .isLength({
      min: 3,
      max: 30,
    })
    .withMessage(
      "Your username must be at least 3 characters and no more than 30!"
    ),
  body("password")
    .isLength({ min: 3, max: 50 })
    .withMessage(
      "Your password must be at least 3 characters and no more than 50!"
    ),
  userController.createNewUser
);

上述端点的请求处理程序。我正在尝试测试 createNewUser。我想存根 createNewUser 以便引发错误,因此我可以测试是否发送了 500 状态代码响应。

const bcrypt = require("bcryptjs");
const { validationResult } = require("express-validator");

const User = require("../models/User");

exports.createNewUser = async (req, res, next) => {
  const { username, password } = req.body;
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    return res.status(400).json({
      success: false,
      errors: errors.array(),
    });
  }

  try {
    // Create a bcrypt salt
    const salt = await bcrypt.genSalt(12);

    // Hash the password
    const hashedPassword = await bcrypt.hash(password, salt);

    // Create a new user
    const user = new User({
      username,
      password: hashedPassword,
    });

    const response = await user.save();

    res.status(200).json(response);
  } catch (err) {
    res.status(500).json({ msg: err.message });
  }
};

用户端点的单元测试我不确定如何测试返回 500 状态码的错误情况...

const request = require("supertest");

// const todosController = require("../controllers/todos");
const server = require("../server");
const User = require("../models/TodoItem");
const db = require("./db");

const agent = request.agent(server);

// Setup connection to the database
beforeAll(async () => await db.connect());
afterEach(async () => await db.clear());
afterAll(async () => await db.close());

describe("User endpoints test suite", () => {
  describe("POST api/user", () => {
    test("It should create a user successfully and return a 200 response code", async () => {
      const response = await agent
        .post("/api/user")
        .set("content-type", "application/json")
        .send({ username: "Bob", password: "12345" });
      expect(response.body.username).toEqual("Bob");
      expect(response.status).toBe(200);
    });
});
});

【问题讨论】:

  • 首先我根本不会对控制器进行单元测试——你的测试不应该使用 Express 的 API。你展示的最后一个测试是没有意义的,它只是练习测试的两倍。
  • @jonrsharpe 嗯,这是有道理的,谢谢...我已经更改了显示端点测试的帖子。您将如何测试发送 500 状态代码响应的错误案例?
  • 那么,究竟什么会导致错误被捕获并导致响应?
  • @jonrsharpe 嗯......不要认为任何预期会......测试过程肯定有点混乱!

标签: node.js unit-testing jestjs mocking sinon


【解决方案1】:

当你创建单元测试时,先创建一些小的东西,你可以在以后增加复杂性和重构。

以下是基于您的代码的简单单元和集成测试示例。

您可以从用户控制器开始。

// File: user.controller.js
const bcrypt = require('bcryptjs');

exports.createNewUser = async (req, res) => {
  try {
    // Create a bcrypt salt.
    const salt = await bcrypt.genSalt(12);
    // Just make it simple, show the salt.
    res.status(200).json(salt);
  } catch (err) {
    // Other wise, return the error message.
    res.status(500).json({ msg: err.message });
  }
};

基于该 try and catch,您可以创建单元测试。

// File: user.controller.spec.js
const bcrypt = require('bcryptjs');
const user = require('./user.controller');

describe('User Controller', () => {
  describe('create New User', () => {
    const fakeJson = jest.fn();
    const fakeStatus = jest.fn().mockReturnThis();
    const fakeRes = {
      status: fakeStatus,
      json: fakeJson,
    };
    const spy = jest.spyOn(bcrypt, 'genSalt');

    afterEach(() => {
      jest.clearAllMocks();
    });

    it('should return salt', async () => {
      const testSalt = 'salt';
      // Mock the bcrypt.genSalt, always resolved with value testSalt.
      spy.mockResolvedValue(testSalt);
      // Call the function under test.
      await user.createNewUser(undefined, fakeRes);
      // Set the expectations.
      expect(fakeStatus).toHaveBeenCalledWith(200);
      expect(fakeJson).toHaveBeenCalledWith(testSalt);
      expect(spy.mock.calls[0][0]).toBe(12);
    });

    it('should return error message when error', async () => {
      const error = new Error('XXX');
      // Mock the bcrypt.genSalt, always resolved with value testSalt.
      spy.mockRejectedValue(error);
      // Call the function under test.
      await user.createNewUser(undefined, fakeRes);
      // Set the expectations.
      expect(fakeStatus).toHaveBeenCalledWith(500);
      expect(fakeJson).toHaveBeenCalledWith({ msg: error.message });
      expect(spy.mock.calls[0][0]).toBe(12);
    });
  });
});

当你在终端上运行它时:

$ npx jest user.controller.spec.js 
 PASS  ./user.controller.spec.js
  User Controller
    create New User
      ✓ should return salt (5 ms)
      ✓ should return error message when error (1 ms)

--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------|---------|----------|---------|---------|-------------------
All files           |     100 |      100 |     100 |     100 |                   
 user.controller.js |     100 |      100 |     100 |     100 |                   
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.511 s, estimated 1 s
Ran all test suites matching /user.controller.spec.js/i.

接下来,如果你确定你的控制器,你可以用 express 创建集成测试。

例如,您像这样创建应用索引。

// File: index.js
const express = require('express');

const userController = require('./user.controller');

const router = express.Router();

router.post('/user', (req, res, next) => userController.createNewUser(req, res, next));

const app = express();

app.use('/api', router);

module.exports = app;

您可以使用 jest 来测试它,以解决像这样的正常和错误情况。

// File: index.spec.js
const request = require('supertest');
const bcrypt = require('bcryptjs');
const server = require('./index');
const userController = require('./user.controller');

const agent = request.agent(server);

describe('App', () => {
  describe('POST /', () => {
    // Create spy on bcrypt.
    const spy = jest.spyOn(bcrypt, 'genSalt');
    const error = new Error('XXX');

    afterEach(() => {
      jest.clearAllMocks();
    });

    it('should create a salt successfully and return a 200 response code', async () => {
      // This test is slow because directly call bcrypt.genSalt.
      // To make it faster, mock bcrypt completely, or use spy.mockResolvedValue('SALT');
      // Send post request.
      const response = await agent.post('/api/user');
      // Make sure the response.
      expect(response.status).toBe(200);
      expect(response.type).toBe('application/json');
      expect(spy.mock.results[0].value).toBeDefined();
      const spyResult = await spy.mock.results[0].value;
      expect(response.body).toBe(spyResult)
    });

    it('should return 500 and error message when catch error', async () => {
      // Makesure spy reject.
      spy.mockRejectedValue(error);
      // Send post request.
      const response = await agent.post('/api/user');
      // Make sure the response.
      expect(response.status).toBe(500);
      expect(response.type).toBe('application/json');
      expect(response.body).toBeDefined();
      expect(response.body.msg).toBeDefined();
      expect(response.body.msg).toBe(error.message);
    });

    // Or play around with another spy to error alternatives.
    it('should return 404 when pass to next', async () => {
      // Makesure createNewUser error.
      jest.spyOn(userController, 'createNewUser').mockImplementation((req, res, next) => {
        // You can setup res here or other implementation to check.
        // For example, do next.
        next();
      });

      // Send post request.
      const response = await agent.post('/api/user');
      // Make sure the response.
      expect(response.status).toBe(404);
      // Method bcrypt.genSalt should not get called.
      expect(spy).not.toHaveBeenCalled();
    });
  });
});

当你从终端运行它时:

$ npx jest index.spec.js 
 PASS  ./index.spec.js
  App
    POST /
      ✓ should create a salt successfully and return a 200 response code (40 ms)
      ✓ should return 500 and error message when catch error (4 ms)
      ✓ should return 404 when pass to next (5 ms)

--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------|---------|----------|---------|---------|-------------------
All files           |     100 |      100 |     100 |     100 |                   
 index.js           |     100 |      100 |     100 |     100 |                   
 user.controller.js |     100 |      100 |     100 |     100 |                   
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.809 s, estimated 1 s
Ran all test suites matching /index.spec.js/i.

注意:不需要使用sinon,jest提供mock functions

【讨论】:

  • 非常感谢!非常感谢详细的回复......我在学习如何在测试中使用模拟和间谍时遇到了很多困难。您有任何建议/知道需要改进的资源吗?
  • 我强烈建议不要使用单元测试控制器。您可以在这里看到,您最终会创建复杂的测试替身(Express 的请求和响应对象),而 您并不拥有。这些是脆弱的测试,对重构或 Express 更改没有帮助。让控制器处理传输层,提取您可以更直接地测试处理业务层的服务。
  • @jonrsharpe 是的,这是一个很好的观点。提取服务到底是什么意思?
  • @jonrsharpe,有任何参考或样本吗?所以我也可以学习..谢谢!
  • @andreyunugro 发现这篇关于服务层和 Node 应用程序整体结构的精彩文章:dev.to/santypk4/…
猜你喜欢
  • 1970-01-01
  • 2020-11-05
  • 1970-01-01
  • 2022-08-06
  • 2017-01-10
  • 1970-01-01
  • 1970-01-01
  • 2019-11-12
  • 1970-01-01
相关资源
最近更新 更多