Automating testing for FeathersJS applications
Fullstack Developer and Tech Author
This is one of a two-part series. You can also learn how to automate the deployment of FeathersJS apps to Heroku.
In the software development lifecycle, testing offers benefits that reach far beyond the code itself. Testing assures stakeholders (developers, clients, project managers, executives) that, while the application may not be completely bug-free, it does what is expected, as expected. By highlighting any regressions that are introduced, testing also provides the confidence to make adjustments and improvements to the code.
Most development teams manage code from a central repository, using a version control system (VCS) to push updates and deploy to production servers. Ideally, tests are run before pushing to the central repository and after deploying to the production servers. This allows any issues to be spotted and dealt with before a user encounters them. In the past, this manual process introduced a bottleneck because updates could not be deployed as soon as they were available.
There was also the risk of poor user experience if an issue was identified while testing the deployment, causing the team to temporarily disable the application while the issue was being resolved. These are just two of the issues that are remedied by the automation of the testing process before deploying a solution to the production environment.
In this article, I will show you how to automate the testing for a FeathersJS application using continuous integration with CircleCI. To help with writing tests, you will be using Mocha.
Prerequisites
Before you start, make sure these items are installed on your system:
- A minimum NodeJS version of 10.0.0.
- An up-to-date JavaScript package manager such as NPM or Yarn.
- The FeathersJS CLI.
You can install the FeathersJS CLI by running this command:
npm install -g @feathersjs/cli
For repository management and continuous integration/continuous deployment, you need:
Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.
Getting started
Create a new folder for the project:
mkdir ci-api-feathersjs
cd ci-api-feathersjs
Next, generate a new app using the Feathers CLI generate
command:
feathers generate app
For this project, you will be using JavaScript to create a REST API. Respond to the questions from the CLI as shown here:
? Do you want to use JavaScript or TypeScript? JavaScript
? What is the name of your application? ci-api-feathersjs
? Write a short description
? Which HTTP framework do you want to use? KoaJS (recommended)
? What APIs do you want to offer? HTTP (REST), Real-time
? Which package manager are you using? npm
? Generate client? Can be used with React, Angular, Vue, React Native, Node.js etc. No
? What is your preferred schema (model) definition format? Schemas allow to type, validate, secure and populate your data and configuration TypeBox (recommended)
? Which database are you connecting to? Databases can be added at any time SQLite
? Enter your database connection string ci-api-feathersjs.sqlite
Once the CLI completes the scaffolding of the application, you can open the project in your favorite code editor.
Run the application with:
npm start
The application will run on port 3000
by default. Visit http://localhost:3030
to review the page.
Adding authentication
To add more functionality to your application, you will leverage the Feathers CLI to quickly add new features for storing and retrieving user credentials.
Run the following command to initialize authentication setup:
feathers generate authentication
You will be prompted to provide some answers, respond as shown here:
? Which authentication methods do you want to use? Other methods and providers can be added at any time. Email + Password
? What is your authentication service name? user
? What path should the service be registered on? users
? What database is the service using? SQL
? Which schema definition format do you want to use? Schemas allow to type, validate, secure and populate data TypeBox (recommended)
Once you are done, Feathers CLI will create the schema, services and migration files for the user object.
FeathersJS provides some basic tests to ensure the project works. You can find the files in the test
folder at the root of the project. Run the tests using this command:
npm test
Expect this output:
Feathers application tests
✔ starts and shows the index page (57ms)
✔ shows a 404 JSON error
users service
✔ registered the service
3 passing (66ms)
In the next section you will configure a CI/CD pipeline to automatically run the tests once the project is deployed to GitHub.
Configuring CircleCI
Now you can add the pipeline configuration for CircleCI. For this project, the pipeline will consist of one job:
build-and-test
- this job builds the project, installs the project dependencies, and runs project tests.
At the root of your project, create a folder named .circleci
. In it, create a file named config.yml
. In the newly created file, add this configuration:
# Use the latest 2.1 version of CircleCI pipeline process engine.
version: 2.1
orbs:
node: circleci/node@5.1.1
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
This configuration uses the Node.js orb circleci/node
to install packages with caching enabled by default. It also makes npm available for you to run your tests.
The pipeline has just one job, build-and-test
, for the node orb to execute. The first step in this job is pulling the code from the GitHub repository. Next, it installs the packages specified in the package.json
file. The process is accelerated by using the cache in the specified directory and the configuration overrides the default command using override-ci-command
. This ensures that the correct installation command for this project is passed.
The last step in this job is to run the npm test
command.
Setting up the project on GitHub
Now, you need to convert this project into a Git repository and then set it up on GitHub. See this post for help: Pushing your project to GitHub.
Log into your CircleCI account. If you signed up with your GitHub account, all your repositories will be displayed on your dashboard.
Click Set Up Project for the ci-api-feathersjs
project.
Enter the name of the branch your code is on, then click Set Up Project.
Your first build will start running and complete successfully!
Click Build-and-test to review the job steps and the status of each job.
Adding more tests to your FeathersJS application
One of the benefits of FeathersJS is that it makes it easy to build prototypes in minutes and production-ready apps in days. Without writing a line of code, you already have an API endpoint to handle registration and authentication. You also have endpoints to get all users, update a user, and delete a user. The API includes these endpoints:
GET /users
lists all users page by page.POST /users
creates a new user. This endpoint will be used for registration.POST /authentication
authenticates a user using the provided strategy. For this tutorial, you are using ‘local’ authentication. This strategy uses an email address and password combination saved in the local database.GET /users/123
returns the details of the user with id 123. You can also include queries in this request, like/users/123?email=yemiwebby@circleci.com
.PATCH /users/123
update the details of the user with id 123.DELETE /users/123
deletes the user with id 123.
You can test these endpoints in Postman. Run the server using this command:
npm run dev
Notice that the password of the newly created user isn’t returned in the JSON response. This is specified out-of-the-box in the user service hook (located in src/services/users/users.hooks.js
).
The user service hook also helps ensure that before a user can make requests to any endpoint (apart from the registration endpoint) a JWT token is specified in the header of the request.
You have completed an important step in this tutorial, but there are still some remaining. Assume that your application has a security requirement that a user can delete only their own account. Trying to delete another user’s account should return a 403
error response. The same should be true for trying to update or patch another user’s account.
Your next step is to write a test suite for these security requirements. To get started, you will need to set up a database to use only with the tests. Update the test environment configuration in config/test.json
with this:
{
"nedb": "../test/data"
}
You will also need to make sure that the database is cleaned up before every test run. To make that possible across platforms, run:
npm install shx --save-dev
Next, update the scripts
section of the package.json
file to this:
"scripts": {
"start": "node src",
"dev": "nodemon src/",
"prettier": "npx prettier \"**/*.js\" --write",
"clean": "shx rm -rf test/data/",
"mocha": "npm run clean && cross-env NODE_ENV=test mocha test/ --recursive --exit",
"test": "cross-env NODE_ENV=test npm run migrate && npm run mocha",
"bundle:client": "npm pack --pack-destination ./public",
"migrate": "knex migrate:latest",
"migrate:make": "knex migrate:make"
},
This will make sure that the test/data
folder is removed before every test run.
Finally, update the code in test/services/users.test.js
to match this:
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 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"
);
}
});
});
In this test suite, you have two actors (Alice and Bob) registered on your application.
In one test scenario, Alice tries to delete Bob’s account. Because this is not permitted, you can expect the API to return a 403
response.
In the second scenario, Bob tries to make a PATCH
request to reset Alice’s password. If this request were to be handled successfully, Bob would be able to log in as Alice and use her account however he pleases. You want to prevent this and expect a 403
response to be returned.
Great stuff! You have the code and the tests for your expected scenarios. All that is left to do is push your code and let the new features take effect.
git commit -am 'Additional security checks on user endpoints'
git push origin main
Go back to CircleCI. Your new build is running beautifully until there is a build failed message. What could be the problem?
It looks like the application fails the tests in the new test suite. The test reports reveal that in the first scenario, Alice successfully deleted Bob’s account. To make things even more interesting, because you deleted Bob’s account, the API returned 404
responses in the second and third scenarios.
Imagine that it turns out you still need to write the code for this feature and that you forgot to run the tests locally before pushing them to the central repository. This scenario may seem contrived, but the truth is that mistakes like this happen. Having an automated testing process prevents this kind of human error. Because a layer of tests must be passed before the update is deployed, this nightmare will never get past the test environment.
You should probably fix this bug before someone finds out. By adding a verification check to the user service hook, you can prevent users from performing certain operations on other accounts.
Open src/services/users/users.hooks.js
. Just above the line beginning with module.exports = {
, add this:
...
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');
}
};
...
This function takes the hook context as a parameter and gets two values from it: the id
of the authenticated user and the id
of the target user. If these values are not the same, then a forbidden
error is thrown. This error is handled by the hook’s error handler, which returns a 403
response.
Next, update the before
entry in src/services/users/users.js
to match this:
...
around: {
all: [schemaHooks.resolveExternal(userExternalResolver), schemaHooks.resolveResult(userResolver)],
find: [authenticate('jwt')],
get: [authenticate('jwt')],
create: [],
update: [authenticate('jwt'), verifyCanPerformOperation],
patch: [authenticate('jwt'), verifyCanPerformOperation],
remove: [authenticate('jwt'), verifyCanPerformOperation]
},
...
With this change, the verification check is carried out after the authentication check for the update
, patch
, and remove
service methods. These methods are mapped to the PUT
, PATCH
,and DELETE
endpoints respectively. This ensures access to the logged-in user to retrieve their id.
All that is left is to run the tests locally to be sure everything is in order, commit the latest changes, and then push these changes to your GitHub repository. This triggers the CircleCI build-and-test
pipeline.
Nice work!
Conclusion
In this article, you built an authentication and user management API using FeathersJS. You also set up a CircleCI pipeline to automatically test changes to the repo before deploying the changes to the production server.
By automating the testing process, you removed the risk of human error wreaking unexpected havoc in the production environment. This approach also adds an additional level of quality control and assurance to the software being maintained. Give continuous integration a try and make codebase bottlenecks a thing of the past for your team!
The entire codebase for this tutorial is available on GitHub.