用Jest进行Node.js Express测试驱动开发的方法

749 阅读5分钟

测试驱动开发,或称TDD,是工作场所中一种相当普遍的编程风格,而且在许多情况下,是某些项目的强制要求。幸运的是,当你了解这个过程背后的基本原理时,TDD是相当简单的。

也有许多库和框架用于JavaScript的后端测试。其中最流行的两个是JestMocha,还有JasmineAvaTapeQUnit等替代品。

Node.js现在有自己的内置测试运行器,自v18以来一直很稳定。然而,它仍然处于实验模式,所以它很可能会随着时间的推移而改变。

在本文中,我们将讨论如何使用新的Node.js测试运行器进行一些基本测试,以及使用Jest测试不同的端点。

在后端进行测试

首先,让我们跳到使用Node.js在后端进行测试。

使用Node.js测试运行器来测试后端代码

如果你想使用内置的Node.js测试运行器,你需要导入下面的库,并按照Node.js文档来运行测试

// JavaScript ESM Modules syntax
import test from 'node:test';

// JavaScript CommonJS syntax
const test = require('node:test');

当然,你也需要确保你使用的是v18或更高版本。你可以在命令行中使用node -v ,检查你目前运行的是哪个版本。

首先,让我们看看使用内置的Node.js测试运行器进行测试是什么样的。

项目设置

打开你的BASH应用程序,cd 到你选择的目录。运行下面的命令来构建你的项目。

mkdir node-backend-testing
cd node-backend-testing
npm init -y
touch index.js index.test.js

接下来,在你的代码编辑器中打开新创建的项目,将代码添加到它们相关的文件中。

下面是文件index.js 的代码。

const calcAge = (dob) => {
    const digits = {
        year: 'numeric',
    };

    const year = new Date().toLocaleDateString('en-US', digits);

    console.log(year);

    return year - dob;
};

const createBox = (x, y) => {
    return x * y;
};

const canDrive = () => {
    const age = 18;

    if (age >= 18) {
        return 'Full Driving Licence';
    } else {
        return 'Provisional License';
    }
};

const powerLevel = () => {
    const power = 9001;

    if (power > 9000) {
        return true;
    } else {
        return false;
    }
};

const workSchedule = (employeeOne, employeeTwo) => {
    return employeeOne + employeeTwo;
};

module.exports = {
    calcAge,

    createBox,

    canDrive,

    powerLevel,

    workSchedule,
};

接下来,我们有index.test.js 的代码。

const test = require('node:test');

const assert = require('assert/strict');

const {
  calcAge,
  createBox,
  canDrive,
  powerLevel,
  workSchedule,
} = require('./index');

// Calculates how old someone is and depending on the year this test could pass or fail

test('calculates age', () => {
  return assert.equal(calcAge(2000), 22);
});

// Creates a box with an equal height and width

test('creates a box', async (t) => {
  await t.test('creates a small box', () => {
    assert.equal(createBox(10, 10), 100);
  });

  await t.test('creates a large box', () => {
    assert.equal(createBox(50, 50), 2500);
  });
});

// Checks to see whether or not the person has a full driving licence

test('checks license', () => {
  return assert.match(`${canDrive()}`, /Full Driving Licence/);
});

// Confirms that the person has a power level that is over 9000!

test('confirms power level', () => {
  return assert.ok(powerLevel());
});

// Checks to see if the employees have the same amount of shift work days in a week

test('employees have an equal number of work days', () => {
  const employeeOne = ['Monday', 'Tuesday', 'Wednesday,', 'Thursday'];

  const employeeTwo = ['Friday', 'Saturday', 'Sunday,', 'Monday'];

  return assert.equal(workSchedule(employeeOne.length, employeeTwo.length), 8);
});

最后,为文件package.json ,添加这个运行脚本。

"scripts": {

"test": "node index.test.js"

},

该测试脚本运行Node测试运行器,然后将运行index.test.js 中的测试。

运行Node测试

你只需要在根文件夹内运行一个命令。再次强调,你需要使用Node v18或更高版本,这样才能工作。

运行下面的命令,你应该在控制台看到五个测试通过。

npm run test

你可以在index.jsindex.test.js 文件中玩玩代码,看看测试的通过和失败。如果你看一下控制台中的测试错误,你就会知道什么是失败的原因。

下面让我给你举几个例子。

计算年龄测试

要计算一个用户的年龄,使用当前年份减去他们的出生年份。见下面index.test.js 文件中的例子。

test('calculates age', () => {
  return assert.equal(calcAge(2000), 21);
});

要看到测试失败,请输入一个不正确的年龄,如21 。在这个例子中,该函数期望的返回值是22 ,所以数字21 使测试失败。

创建一个方框测试

这个测试希望方程10 x 1050 x 50 的答案分别为1002500 。如果输入的值与正确的输出值相差甚远,则测试失败。

test('creates a box', async (t) => {
  await t.test('creates a small box', () => {
    assert.equal(createBox(10, 30), 100);
  });

  await t.test('creates a large box', () => {
    assert.equal(createBox(50, 20), 2500);
  });
});

检查许可证测试

这个测试检查一个人是否有有效的驾驶执照。将index.js 文件的canDrive 函数中的年龄改为小于18 。 测试将失败。

const canDrive = () => {
  const age = 17;

  if (age >= 18) {
    return 'Full Driving Licence';
  } else {
    return 'Provisional License';
  }
};

确认功率水平测试

这个测试检查一个人的力量水平是否超过9000(你明白龙珠Z的含义吗?😁 )。

index.js 文件的powerLevel 函数内的功率级别改为小于9000 。测试将失败。

const powerLevel = () => {
  const power = 5000;

  if (power > 9000) {
    return true;
  } else {
    return false;
  }
};

相同工作天数测试

这个测试检查员工的工作天数是否相同。

目前有两个雇员各工作四天,我们总共有八天的时间。要看到测试失败,只需从数组中输入或删除一些天数,使它们不再是相同的长度。

test('employees have an equal number of work days', () => {
  const employeeOne = ['Monday', 'Tuesday', 'Wednesday,', 'Thursday'];

  const employeeTwo = ['Friday', 'Saturday'];

  return assert.equal(workSchedule(employeeOne.length, employeeTwo.length), 8);
});

使用Jest来测试后端代码

现在,让我们使用Jest测试库做一些后端测试。

项目设置

打开你的BASH应用程序,cd 到你喜欢的目录。运行下面的命令来构建你的项目。

mkdir jest-backend-testing
cd jest-backend-testing
npm init -y
npm i express http-errors jest nodemon supertest
mkdir routes
touch routes/products.js
touch app.js app.test.js server.js

现在,在你的代码编辑器中打开项目,将下面的代码添加到它们相应的文件中。

下面是文件routes/product.js 的代码。

const express = require('express');

const router = express.Router();

const createError = require('http-errors');

// Products Array

const products = [{ id: '1', name: 'Playstation 5', inStock: false }];

// GET / => array of items

router.get('/', (req, res) => {
  res.json(products);
});

// GET / => items by ID

router.get('/:id', (req, res, next) => {
  const product = products.find(
    (product) => product.id === String(req.params.id)
  );

  // GET /id => 404 if item not found

  if (!product) {
    return next(createError(404, 'Not Found'));
  }

  res.json(product);
});

router.post('/', (req, res, next) => {
  const { body } = req;

  if (typeof body.name !== 'string') {
    return next(createError(400, 'Validation Error'));
  }

  const newProduct = {
    id: '1',

    name: body.name,

    inStock: false,
  };

  products.push(newProduct);

  res.status(201).json(newProduct);
});

module.exports = router;

接下来,是文件app.js 的代码。

const express = require('express');

const productsRoute = require('./routes/products');

const app = express();

app.use(express.urlencoded({ extended: false }));

app.use(express.json());

app.use('/', productsRoute);

module.exports = app;

下面是文件app.test.js 的代码。

const request = require('supertest');

const app = require('./app');

describe('GET /', () => {
  it('GET / => array of items', () => {
    return request(app)
      .get('/')

      .expect('Content-Type', /json/)

      .expect(200)

      .then((response) => {
        expect(response.body).toEqual(
          expect.arrayContaining([
            expect.objectContaining({
              id: expect.any(String),

              name: expect.any(String),

              inStock: expect.any(Boolean),
            }),
          ])
        );
      });
  });

  it('GET / => items by ID', () => {
    return request(app)
      .get('/1')

      .expect('Content-Type', /json/)

      .expect(200)

      .then((response) => {
        expect(response.body).toEqual(
          expect.objectContaining({
            id: expect.any(String),

            name: expect.any(String),

            inStock: expect.any(Boolean),
          })
        );
      });
  });

  it('GET /id => 404 if item not found', () => {
    return request(app).get('/10000000000').expect(404);
  });

  it('POST / => create NEW item', () => {
    return (
      request(app)
        .post('/')

        // Item send code

        .send({
          name: 'Xbox Series X',
        })

        .expect('Content-Type', /json/)

        .expect(201)

        .then((response) => {
          expect(response.body).toEqual(
            expect.objectContaining({
              name: 'Xbox Series X',

              inStock: false,
            })
          );
        })
    );
  });

  it('POST / => item name correct data type check', () => {
    return request(app).post('/').send({ name: 123456789 }).expect(400);
  });
});

差不多了!下面是server.js 文件的代码。

const app = require('./app');

const port = process.env.PORT || 3000;

app.listen(port, () =>
  console.log(`Server running on port ${port}, http://localhost:${port}`)
);

最后,将这些运行脚本添加到你的package.json 文件中。

"scripts": {

"start": "node server.js",

"dev": "nodemon server.js",

"test": "jest --watchAll"

},

start 脚本使用node运行服务器文件。

dev 脚本使用nodemon 运行服务器文件,实现自动重载。

test 脚本运行测试运行器Jest,自动观察文件变化。

运行Jest测试

现在是启动应用程序和测试运行器的时候了。在不同的标签或窗口中运行下面的命令,这样你就有一个脚本运行开发服务器,另一个运行Jest测试运行器。

服务器运行在http://localhost:3000/。

npm run dev

npm run test

你现在应该有dev 服务器在运行。Jest测试运行器也应该在运行,有五个通过的测试。让我们浏览一下每个测试,你可以看到它们的通过或失败。

这些测试的名称与标题相同,所以它们应该很容易在文件中找到。

数组项目测试

这个测试检查是否有一个对象数组被返回。要看到它失败,打开products.js 文件,用下面的代码替换顶部的路线。

router.get('/', (req, res) => {
  res.send('products');
});

你应该得到错误expected Content-Type" matching /json/, got "text/html; charset=utf-8 ,测试应该失败。

现在试着把它改成下面的代码。

router.get('/', (req, res) => {
  res.json('products');
});

你应该得到这个错误Expected: ArrayContaining [ObjectContaining {"id": Any<String>, "inStock": Any<Boolean>, "name": Any<String>}]

项目由id 测试

这个测试检查一个项目是否以正确的方式返回id 。将products.js 文件顶部的products 数组改为id ,它是一个数字(而不是一个字符串),可以看到它失败。

const products = [{ id: 1, name: 'Playstation 5', inStock: false }];

你应该得到这个错误:expected "Content-Type" matching /json/, got "text/html; charset=utf-8"

未找到项目测试

这个测试检查是否找不到一个项目。要看到它失败,请在products.js 文件中把404 error 的代码改为其他内容,如下面的例子。

if (!product) {
  return next(createError(400, 'Not Found'));
}

你应该得到这样的错误:expected 404 "Not Found", got 400 "Bad Request"

正确的数据测试

这个测试检查对象是否有正确的数据和数据类型。要看到它失败,打开app.test.js 文件,用下面的代码替换发送代码。

.send({

name: 'Nintendo Switch',

})

你应该看到这个错误。

 - Expected  - 2
    + Received  + 3

    - ObjectContaining {
    + Object {
    +   "id": "1",
        "inStock": false,
    -   "name": "Xbox Series X",
    +   "name": "Nintendo Switch",

正确的数据类型测试

这个测试检查名称变量是否是正确的数据类型。只有在检测到字符串的情况下才会失败。要看到测试失败,打开app.test.js 文件,滚动到底部,像这样把数字改为字符串。

return request(app).post('/').send({ name: '123456789' }).expect(400);

你应该得到这个错误:expected 400 "Bad Request", got 201 "Created"

Jest 28:新功能

Jest 28于2022年4月发布,带来了很多新功能。其中一个被要求最多的功能叫做分片。本质上,分片让你把你的测试套件分成不同的分片,以运行套件测试的一部分。

例如,要运行三分之一的测试,你可以使用下面的命令。

jest --shard=1/3
jest --shard=2/3
jest --shard=3/3

结语

这就是全部!我们刚刚介绍了在后端使用Node和Jest进行测试。我们所涉及的只是测试的基础知识--还有很多东西需要学习!