本教程包括:
- 构建一个FeathersJS API
- 为FeathersJS应用程序编写代码和测试
- 创建一个CI管道来自动测试
在软件开发生命周期中,测试提供的好处远远超过了代码本身。测试向所有各方(开发人员、客户、项目经理等)保证,虽然应用程序可能不是完全没有错误的,但它会按照预期的那样做。通过强调任何引入的回归,测试也提供了对代码进行调整和改进的信心。
大多数开发团队从中央仓库管理代码,使用版本控制系统(VCS)来推送更新和部署到生产服务器。理想情况下,在推送到中央存储库之前和部署到生产服务器之后,都要运行测试。这使得任何问题都能在用户遇到之前被发现和处理。在过去,这种手动过程带来了一个瓶颈,因为更新不能尽快部署。
如果在测试部署时发现问题,还存在着用户体验不佳的风险,导致团队在解决问题时暂时停用应用程序。这只是在将解决方案部署到生产环境之前,通过测试过程的自动化来补救的两个问题。
在这篇文章中,我将告诉你如何使用CircleCI对FeathersJS应用程序进行自动化测试。为了帮助编写测试,我们将使用Mocha。
前提条件
在你开始之前,确保这些项目已经安装在你的系统上。
你可以通过运行这个命令来安装FeathersJS CLI。
npm install -g @feathersjs/cli
对于仓库管理和持续集成/持续部署,你需要。
- 一个GitHub账户
- 一个CircleCI账户
开始使用
为该项目创建一个新的文件夹。
mkdir auth-api-feathersjs
cd auth-api-feathersjs
接下来,使用Feathers CLIgenerate 命令生成一个新的应用程序。
feathers generate app
对于这个项目,我们将使用JavaScript来创建一个REST API。回答CLI的问题,如图所示。
? Do you want to use JavaScript or TypeScript? JavaScript
? Project name auth-api-feathersjs
? Description
? What folder should the source files live in? src
? Which package manager are you using (has to be installed globally)? npm
? What type of API are you making? REST
? Which testing framework do you prefer? Mocha + assert
? This app uses authentication Yes
? Which coding style do you want to use? ESLint
? What authentication strategies do you want to use? (See API docs for all 180+
supported oAuth providers) Username + Password (Local)
? What is the name of the user (entity) service? users
? What kind of service is it? NeDB
? What is the database connection string? nedb://../data
一旦CLI完成了应用程序的脚手架,你可以在你喜欢的任何代码编辑器中打开该项目。
FeathersJS提供了一些基本的测试来确保项目的运行。你可以在项目根部的test 文件夹中找到这些文件。使用这个命令运行测试。
npm test
期待这个输出。
Feathers application tests
✓ starts and shows the index page
404
info: Page not found {"className":"not-found","code":404,"data":{"url":"/path/to/nowhere"},"errors":{},"name":"NotFound","type":"FeathersError"}
✓ shows a 404 HTML page
info: Page not found {"className":"not-found","code":404,"data":{"url":"/path/to/nowhere"},"errors":{},"name":"NotFound","type":"FeathersError"}
✓ shows a 404 JSON error without stack trace
authentication
✓ registered the authentication service
local strategy
✓ authenticates user and creates accessToken (79ms)
'users' service
✓ registered the service
6 passing (282ms)
配置CircleCI
接下来,添加CircleCI的管道配置。对于这个项目,管道将由一个步骤组成。
- 构建和测试 - 在这里,我们构建项目,安装项目依赖,运行项目测试。
在你项目的根部,创建一个名为.circleci 的文件夹,并在其中创建一个名为config.yml 的文件。在新创建的文件中,添加这个配置。
# Use the latest 2.1 version of CircleCI pipeline process engine.
version: 2.1
orbs:
node: circleci/node@5.0.2
jobs:
build-and-test:
executor: node/default
steps:
- checkout
- node/install-packages:
cache-path: ~/project/node_modules
override-ci-command: npm install
- run: npm test
workflows:
test-my-app:
jobs:
- build-and-test
这个配置使用Node.js orbcircleci/node 来安装默认启用了缓存的软件包。它还使npm可用来运行你的测试。
该管道只有一个工作,build-and-test ,供node orbte执行。这个作业的第一步是从GitHub仓库中提取代码。接下来,它安装在package.json 文件中指定的软件包。这个过程通过使用指定目录下的缓存来加速。该配置通过使用override-ci-command ,覆盖了安装软件包的默认命令。这确保了这个项目的正确安装命令被传递。
这项工作的最后一步是运行npm test 命令。
在GitHub上设置该项目
现在你需要把这个项目转换成一个Git仓库,然后在GitHub上进行设置。
登录你的CircleCI账户。如果你用GitHub账户注册,你的所有仓库都会显示在你的仪表板上。
点击auth-api-feathersjs 项目的Set Up Project。
输入你的代码在GitHub上存放的分支名称。然后点击Set Up Project。

你的第一个构建过程将开始运行并成功完成!

点击build-and-test来查看工作步骤和每个工作的状态。

将测试添加到您的FeathersJS应用程序
FeathersJS的主要卖点之一是,它可以在几分钟内轻松建立原型,并在几天内建立生产就绪的应用程序。不用写一行代码,你已经有一个API端点来处理注册和认证。你也有端点来获取所有的用户,更新一个用户,以及删除一个用户。该API包括这些端点。
GET /users逐页列出所有用户。POST /users创建一个新的用户。这个端点将被用于注册。POST /authentication使用所提供的策略对用户进行认证。在本教程中,我们使用 "本地 "认证。这个策略使用保存在本地数据库中的电子邮件地址和密码组合。GET /users/123返回ID为123的用户的详细信息。你也可以在这个请求中包括查询,如 。/users/123?email=yemiwebby@circleci.comPATCH /users/123和 更新ID为123的用户的详细信息。PUT /users/123DELETE /users/123删除ID为123的用户。
你可以在Postman中测试这些端点。使用这个命令运行服务器。
npm run dev

注意,新创建的用户的密码没有在JSON响应中返回。这是在用户服务钩子(位于src/services/users/users.hooks.js )中开箱即定的。

用户服务钩子还有助于确保在用户向任何端点(除注册端点外)提出请求之前,在请求的头中指定一个JWT令牌。
你已经完成了本教程的一个重要步骤,但仍有一些步骤。假设你的应用程序有一个安全要求,即用户只能删除自己的账户。试图删除另一个用户的账户应该返回一个403 错误响应。试图更新或修补另一个用户的帐户也应该是这样的。
你的下一步是为这个安全要求写一个测试套件。为了开始,你将需要设置一个数据库,只用于测试。要做到这一点,在config/test.json 中更新测试环境配置。
{
"nedb": "../test/data"
}
你还需要确保数据库在每次测试运行前都被清理掉。为了实现跨平台,首先,运行。
npm install shx --save-dev
接下来,将package.json文件中的scripts 部分更新为这样。
"scripts": {
"test": "npm run lint && npm run mocha",
"lint": "eslint src/. test/. --config .eslintrc.json --fix",
"dev": "nodemon src/",
"start": "node src/",
"clean": "shx rm -rf test/data/",
"mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
},
这将确保test/data 文件夹在每次测试运行前被删除。
最后,更新test/services/users.test.js 中的代码,使之与此相符。
const axios = require('axios');
const assert = require('assert');
const url = require('url');
const app = require('../../src/app');
const port = app.get('port') || 8998;
const getUrl = (pathname) =>
url.format({
hostname: app.get('host') || 'localhost',
protocol: 'http',
port,
pathname,
});
describe('\'users\' service', () => {
it('registered the service', () => {
const service = app.service('users');
assert.ok(service, 'Registered the service');
});
});
describe('Additional security checks on user endpoints', () => {
let alice = {
email: 'alice@feathersjs.com',
password: 'supersecret12',
};
let bob = {
email: 'bob@feathersjs.com',
password: 'supersecret1',
};
const getTokenForUser = async (user) => {
const { accessToken } = await app.service('authentication').create({
strategy: 'local',
...user,
});
return accessToken;
};
const setupUser = async (user) => {
const { _id } = await app.service('users').create(user);
user._id = _id;
user.accessToken = await getTokenForUser(user);
};
let server;
before(async () => {
await setupUser(alice);
await setupUser(bob);
server = app.listen(port);
});
after(async () => {
server.close();
});
it('should return 403 when user tries to delete another user', async () => {
const { accessToken } = alice;
const { _id: targetId } = bob;
const config = { headers: { Authorization: `Bearer ${accessToken}` } };
try {
await axios.delete(getUrl(`/users/${targetId}`), config);
} catch (error) {
const { response } = error;
assert.equal(response.status, 403);
assert.equal(
response.data.message,
'You are not authorized to perform this operation on another user'
);
}
});
it('should return 403 when user tries to put another user', async () => {
try {
const { accessToken } = bob;
const { _id: targetId } = alice;
const config = { headers: { Authorization: `Bearer ${accessToken}` } };
const testData = { password: bob.password };
await axios.put(getUrl(`/users/${targetId}`), testData, config);
} catch (error) {
const { response } = error;
assert.equal(response.status, 403);
assert.equal(
response.data.message,
'You are not authorized to perform this operation on another user'
);
}
});
it('should return 403 when user tries to patch another user', async () => {
try {
const { accessToken } = alice;
const { _id: targetId } = bob;
const config = { headers: { Authorization: `Bearer ${accessToken}` } };
const testData = { password: alice.password };
await axios.patch(getUrl(`/users/${targetId}`), testData, config);
} catch (error) {
const { response } = error;
assert.equal(response.status, 403);
assert.equal(
response.data.message,
'You are not authorized to perform this operation on another user'
);
}
});
});
在这个测试套件中,我们有两个演员(Alice和Bob)在我们的应用程序上注册。
在一个测试场景中,Alice试图删除Bob的账户。因为这是不允许的,你可以预期API会返回一个403 响应。
在第二个场景中,Bob试图提出一个PUT 的请求来重置Alice的密码。如果这个请求被成功处理,Bob将能够以Alice的身份登录,并以他喜欢的方式使用她的账户。你要防止这种情况,并期望返回一个403 响应。
第三种情况与第二种情况类似,只是这次Alice试图通过一个UPDATE 请求来重置Bob的密码。同样,你可以期待一个403 响应被返回。
好东西!你有了代码和预期场景的测试。剩下的就是推送你的代码,让新功能生效。
git commit -am 'Additional security checks on user endpoints'
git push origin main
回到CircleCI,看到你的新版本运行得很好。一切都是一帆风顺的,直到我们出现一个构建失败的消息。会是什么问题呢?

看起来,应用程序在新的测试套件中没有通过测试。测试报告显示,在第一种情况下,Alice成功删除了Bob的账户。更有趣的是,由于我们删除了Bob的账户,API在第二和第三种情况下返回了404 响应。
想象一下,事实证明你仍然需要为这个功能编写代码,而且你在推送到中央仓库之前忘记了在本地运行测试。这个场景可能看起来很矫情,但事实是这样的错误会发生。有一个自动化的测试过程可以防止这类人为错误的发生。因为在部署更新之前必须通过一层测试,所以这个噩梦永远不会通过测试环境。
那么,你应该在别人发现之前修复这个错误。通过给用户服务钩子添加一个验证检查,你可以防止用户对其他账户进行某些操作。
打开src/services/users/users.hooks.js 。就在以module.exports = { 开始的那行上面,添加这个。
...
const {Forbidden} = require('@feathersjs/errors');
const verifyCanPerformOperation = async context => {
const {_id: authenticatedUserId} = context.params.user;
const {id: targetUserId} = context;
if (authenticatedUserId !== targetUserId) {
throw new Forbidden('You are not authorized to perform this operation on another user');
}
};
...
这个函数把钩子的上下文作为参数,并从中得到两个值:被验证用户的id 和目标用户的id 。如果这些值不一样,就会抛出一个forbidden 错误。这个错误由钩子的错误处理程序来处理,它返回一个403 响应。
接下来,更新src/services/users/users.hooks.js 中的before 条目,使之与此相匹配。
...
before: {
all: [],
find: [authenticate('jwt')],
get: [authenticate('jwt')],
create: [hashPassword('password')],
update: [hashPassword('password'), authenticate('jwt'), verifyCanPerformOperation],
patch: [hashPassword('password'), authenticate('jwt'), verifyCanPerformOperation],
remove: [authenticate('jwt'), verifyCanPerformOperation]
},
...
有了这个改动,验证检查就在update 、patch 、remove 服务方法的验证检查之后进行了。这些方法分别被映射到PUT,PATCH,和DELETE 端点。这确保了对登录用户的访问,以检索他们的ID。
剩下的就是在本地运行测试以确保一切正常,提交最新的修改,然后将这些修改推送到你的GitHub仓库。这就触发了CircleCI的build-and-test 管道。

干得好!
总结
在这篇文章中,你使用FeathersJS构建了一个认证和用户管理API。你还建立了一个CircleCI管道,在将变化部署到生产服务器之前自动测试对repo的变化。
这种方法的好处是,人类的错误不会影响到你的应用程序的安全性。通过自动化的测试过程,你消除了人为错误在生产环境中造成意外破坏的风险。它还为正在维护的软件增加了一个额外的质量控制和保证水平。试试持续集成吧,让代码库瓶颈成为你的团队的过去。