在存根异步函数时遇到问题

Having trouble stubbing an asynchronous function

我目前正在使用 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);
    });
});
});

当您创建单元测试时,先创建一些小的,您可以稍后添加复杂性和重构。

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

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

// 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.

接下来,如果你对你的controller有把握,你可以用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.