Writing tests

Every time we generate a hook or service, the generator will also set up a basic Mocha test that we can use to implement unit tests for it. In this chapter, we will implement unit tests for our hooks and integration tests for the users and messages services.

We can run the code Linter and Mocha tests with

  1. npm test

This will fail initially, since we implemented functionality in our hooks that is not covered by the standard tests. So let’s get those to pass first.

Unit testing hooks

The best way to test individual hooks is to set up a dummy Feathers application with some services that return the data we expect and can test against, then register the hooks and make actual service calls to verify that they return what we’d expect.

The first hook we created was for processing new messages. For this hook, we can create a messages dummy custom service that just returns the same data from the create service method. To pretend we are an authenticated user, we have to pass params.user. For this test, this can be a simple JavaScript object with an _id.

Update test/hooks/process-messages.test.js to the following:

  1. const assert = require('assert');
  2. const feathers = require('@feathersjs/feathers');
  3. const processMessage = require('../../src/hooks/process-message');
  4. describe('\'process-message\' hook', () => {
  5. let app;
  6. beforeEach(() => {
  7. // Create a new plain Feathers application
  8. app = feathers();
  9. // Register a dummy custom service that just return the
  10. // message data back
  11. app.use('/messages', {
  12. async create(data) {
  13. return data;
  14. }
  15. });
  16. // Register the `processMessage` hook on that service
  17. app.service('messages').hooks({
  18. before: {
  19. create: processMessage()
  20. }
  21. });
  22. });
  23. it('processes the message as expected', async () => {
  24. // A user stub with just an `_id`
  25. const user = { _id: 'test' };
  26. // The service method call `params`
  27. const params = { user };
  28. // Create a new message with params that contains our user
  29. const message = await app.service('messages').create({
  30. text: 'Hi there',
  31. additional: 'should be removed'
  32. }, params);
  33. assert.equal(message.text, 'Hi there');
  34. // `userId` was set
  35. assert.equal(message.userId, 'test');
  36. // `additional` property has been removed
  37. assert.ok(!message.additional);
  38. });
  39. });

We can take a similar approach to test the gravatar hook in test/hooks/gravatar.test.js:

  1. const assert = require('assert');
  2. const feathers = require('@feathersjs/feathers');
  3. const gravatar = require('../../src/hooks/gravatar');
  4. describe('\'gravatar\' hook', () => {
  5. let app;
  6. beforeEach(() => {
  7. app = feathers();
  8. // A dummy users service for testing
  9. app.use('/users', {
  10. async create(data) {
  11. return data;
  12. }
  13. });
  14. // Add the hook to the dummy service
  15. app.service('users').hooks({
  16. before: {
  17. create: gravatar()
  18. }
  19. });
  20. });
  21. it('creates a gravatar link from the users email', async () => {
  22. const user = await app.service('users').create({
  23. email: 'test@example.com'
  24. });
  25. assert.deepEqual(user, {
  26. email: 'test@example.com',
  27. avatar: 'https://s.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=60'
  28. });
  29. });
  30. });

In the tests above, we created a dummy service. But sometimes, we need the full Feathers service functionality. feathers-memory is a useful database adapter that supports the Feathers query syntax (and pagination) but does not require a database server. We can install it as a development dependency:

  1. npm install feathers-memory --save-dev

Let’s use it to test the populateUser hook, by updating test/hooks/populate-user.test.js to the following:

  1. const assert = require('assert');
  2. const feathers = require('@feathersjs/feathers');
  3. const memory = require('feathers-memory');
  4. const populateUser = require('../../src/hooks/populate-user');
  5. describe('\'populate-user\' hook', () => {
  6. let app, user;
  7. beforeEach(async () => {
  8. // Database adapter pagination options
  9. const options = {
  10. paginate: {
  11. default: 10,
  12. max: 25
  13. }
  14. };
  15. app = feathers();
  16. // Register `users` and `messages` service in-memory
  17. app.use('/users', memory(options));
  18. app.use('/messages', memory(options));
  19. // Add the hook to the dummy service
  20. app.service('messages').hooks({
  21. after: populateUser()
  22. });
  23. // Create a new user we can use to test with
  24. user = await app.service('users').create({
  25. email: 'test@user.com'
  26. });
  27. });
  28. it('populates a new message with the user', async () => {
  29. const message = await app.service('messages').create({
  30. text: 'A test message',
  31. // Set `userId` manually (usually done by `process-message` hook)
  32. userId: user.id
  33. });
  34. // Make sure that user got added to the returned message
  35. assert.deepEqual(message.user, user);
  36. });
  37. });

If we now run:

  1. npm test

All our tests should pass. Yay!

Note: There are some error stacks printed when running the tests. This is normal, they are log entries when running the tests for 404 (Not Found) errors.

Test database setup

When testing database functionality, we want to make sure that the tests use a different database. We can achieve this by creating a new environment configuration in config/test.json with the following content:

  1. {
  2. "nedb": "../test/data"
  3. }

This will set up the NeDB database to use test/data as the base directory instead of data/ when NODE_ENV is set to test. The same thing can be done with connection strings for other databases.

We also want to make sure that before every test run, the database is cleaned up. To make that possible across platforms, first run:

  1. npm install shx --save-dev

Now we can update the script section of package.json to the following:

  1. "scripts": {
  2. "test": "npm run eslint && npm run mocha",
  3. "eslint": "eslint src/. test/. --config .eslintrc.json",
  4. "start": "node src/",
  5. "clean": "shx rm -rf test/data/",
  6. "mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
  7. }

On Windows the mocha should look like:

  1. npm run clean & SET NODE_ENV=test& mocha test/ --recursive --exit

This will make sure that the test/data folder is removed before every test run and NODE_ENV is set properly.

Testing services

To test the actual messages and users services (with all hooks wired up), we can use any REST API testing tool to make requests and verify that they return correct responses.

But there is a much faster, easier and complete approach. Since everything on top of our own hooks and services is already provided (and tested) by Feathers, we can require the application object using the service methods directly, and “fake” authentication by setting params.user as demonstrated in the hook tests above.

By default, the generator creates a service test file, e.g. test/services/users.test.js, that only tests that the service exists, like this:

  1. const assert = require('assert');
  2. const app = require('../../src/app');
  3. describe('\'users\' service', () => {
  4. it('registered the service', () => {
  5. const service = app.service('users');
  6. assert.ok(service, 'Registered the service');
  7. });
  8. });

We can then add similar tests that use the service. Following is an updated test/services/users.test.js that adds two tests. The first verifies that users can be created, the gravatar gets set and the password gets encrypted. The second verifies that the password does not get sent to external requests:

  1. const assert = require('assert');
  2. const app = require('../../src/app');
  3. describe('\'users\' service', () => {
  4. it('registered the service', () => {
  5. const service = app.service('users');
  6. assert.ok(service, 'Registered the service');
  7. });
  8. it('creates a user, encrypts password and adds gravatar', async () => {
  9. const user = await app.service('users').create({
  10. email: 'test@example.com',
  11. password: 'secret'
  12. });
  13. // Verify Gravatar has been set as we'd expect
  14. assert.equal(user.avatar, 'https://s.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=60');
  15. // Makes sure the password got encrypted
  16. assert.ok(user.password !== 'secret');
  17. });
  18. it('removes password for external requests', async () => {
  19. // Setting `provider` indicates an external request
  20. const params = { provider: 'rest' };
  21. const user = await app.service('users').create({
  22. email: 'test2@example.com',
  23. password: 'secret'
  24. }, params);
  25. // Make sure password has been removed
  26. assert.ok(!user.password);
  27. });
  28. });

We take a similar approach for test/services/messages.test.js. We create a test-specific user from the users service. We then pass it as params.user when creating a new message, and validates that message’s content:

  1. const assert = require('assert');
  2. const app = require('../../src/app');
  3. describe('\'messages\' service', () => {
  4. it('registered the service', () => {
  5. const service = app.service('messages');
  6. assert.ok(service, 'Registered the service');
  7. });
  8. it('creates and processes message, adds user information', async () => {
  9. // Create a new user we can use for testing
  10. const user = await app.service('users').create({
  11. email: 'messagetest@example.com',
  12. password: 'supersecret'
  13. });
  14. // The messages service call params (with the user we just created)
  15. const params = { user };
  16. const message = await app.service('messages').create({
  17. text: 'a test',
  18. additional: 'should be removed'
  19. }, params);
  20. assert.equal(message.text, 'a test');
  21. // `userId` should be set to passed users it
  22. assert.equal(message.userId, user._id);
  23. // Additional property has been removed
  24. assert.ok(!message.additional);
  25. // `user` has been populated
  26. assert.deepEqual(message.user, user);
  27. });
  28. });

Run npm test one more time, to verify that the tests for all our hooks, and the new service tests pass.

Code coverage

Code coverage is a great way to get some insights into how much of our code is actually executed during the tests. Using Istanbul we can add it easily:

  1. npm install istanbul@1.1.0-alpha.1 --save-dev

Now we have to update the script section of our package.json to:

  1. "scripts": {
  2. "test": "npm run eslint && npm run coverage",
  3. "coverage": "npm run clean && NODE_ENV=test istanbul cover node_modules/mocha/bin/_mocha -- test/ --recursive --exit",
  4. "eslint": "eslint src/. test/. --config .eslintrc.json --fix",
  5. "start": "node src/",
  6. "clean": "shx rm -rf test/data/",
  7. "mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
  8. },

On Windows, the coverage command looks like this:

  1. npm run clean & SET NODE_ENV=test& istanbul cover node_modules/mocha/bin/_mocha -- test/ --recursive --exit

To get more coverage information, add a .istanbul.yml in the main folder:

  1. verbose: false
  2. instrumentation:
  3. root: ./src/
  4. reporting:
  5. print: summary
  6. reports:
  7. - html
  8. - text
  9. - lcov
  10. watermarks:
  11. statements: [70, 90]
  12. lines: [70, 90]
  13. functions: [70, 90]
  14. branches: [70, 90]

Now run:

  1. npm test

This will print out some additional coverage information and put a complete HTML report into the coverage folder.

Changing the default test directory

To change the default test directory, specify the directory you want in your project’s package.json file:

  1. {
  2. "directories": {
  3. "test": "server/test/"
  4. }
  5. }

Also, don’t forget to update your mocha script in your package.json file:

  1. "scripts": {
  2. "mocha": "mocha server/test/ --recursive --exit"
  3. }

What’s next?

That’s it - our chat guide is completed! We now have a fully-tested REST and real-time API, with a plain JavaScript frontend including login and signup. Follow up in the Feathers API documentation for complete details about using Feathers, or start building your own first Feathers application!