Unit Testing and Functional Testing: Testing REST backend services

It’s now time to turn our attention to the user authentication service. We’ve mentioned testing this service, saying that we’ll get to them later. We developed a command-line tool for both administration and ad hoc testing. While that has been useful all along, it’s time to get cracking with some real tests.

There’s a question of which tool to use for testing the authentication service. Mocha does a good job of organizing a series of test cases, and we should reuse it here. But the thing we have to test is a REST service. The customer of this service, the Notes application, uses it through the REST API, giving us a perfect rationalization to test the REST interface rather than calling the functions directly. Our ad hoc scripts used the SuperAgent library to simplify making REST API calls. There happens to be a companion library, SuperTest, that is meant for REST API testing. It’s easy to use that library within a Mocha test suite, so let’s take that route.

Create a directory named compose-stack-test-local/userauth. This directory will contain a test suite for the user authentication REST service. In that directory, create a file named test.mjs that contains the following code:

import Chai from ‘chai’;

const assert = Chai.assert;

import supertest from ‘supertest’;

const request = supertest(process.env.URL_USERS_TEST);

const authUser = ‘them’;

const authKey = ‘D4ED43C0-8BD6-4FE2-B358-7C0E230D11EF’;

describe(‘Users Test’, function() {

});

This sets up Mocha and the SuperTest client. The URL_USERS_TEST environment variable specifies the base URL of the server to run the test against. You’ll almost certainly be using http://localhost:5858, given the configuration we’ve used earlier, but it can be any URL pointing to any host. SuperTest initializes itself a little differently to SuperAgent.

The SuperTest module supplies a function, and we call that function with the URL_USERS_TEST variable. That gives us an object, which we call request, that is used for interacting with the service under test.

We’ve also set up a pair of variables to store the authentication user ID and key. These are the same values that are in the user authentication server. We simply need to supply them when making API calls.

Finally, there’s the outer shell of the Mocha test suite. So, let’s start filling in the before and after test cases:

before(async function() {

await request

.post(‘/create-user’)

.send({

username: “me”, password: “w0rd”, provider: “local”,

familyName: “Einarrsdottir”, givenName: “Ashildr”,

middleName: “”, emails: [], photos: []

})

.set(‘Content-Type’, ‘application/json’)

.set(‘Acccept’, ‘application/json’)

.auth(authUser, authKey);

});

after(async function() {

await request

.delete(‘/destroy/me’)

.set(‘Content-Type’, ‘application/json’)

.set(‘Acccept’, ‘application/json’)

.auth(authUser, authKey);

});

These are our before and after tests. We’ll use them to establish a user and then clean them up by removing the user at the end.

This gives us a taste of how the SuperTest API works. If you refer back to cli.mjs, you’ll see the similarities to SuperAgent.

The post and delete methods we can see here declare the HTTP verb to use. The send method provides an object for the POST operation. The set method sets header values, while the auth method sets up authentication:

describe(‘List user’, function() {

it(“list created users”, async function() {

const res = await request.get(‘/list’)

.set(‘Content-Type’, ‘application/json’)

.set(‘Acccept’, ‘application/json’)

.auth(authUser, authKey);

assert.exists(res.body);

assert.isArray(res.body);

assert.lengthOf(res.body, 1);

assert.deepEqual(res.body[0], {

username: “me”, id: “me”, provider: “local”,

familyName: “Einarrsdottir”, givenName: “Ashildr”,

middleName: “”,

emails: [], photos: []

});

});

});

Now, we can test some API methods, such as the /list operation.

We have already guaranteed that there is an account in the before method, so /list should give us an array with one entry.

This follows the general pattern for using Mocha to test a REST API method. First, we use SuperTest’s request object to call the API method and await its result. Once we have the result, we use assert methods to validate it is what’s expected.

Add the following test cases:

describe(‘find user’, function() {

it(“find created users”, async function() {

const res = await request.get(‘/find/me’)

.set(‘Content-Type’, ‘application/json’)

.set(‘Acccept’, ‘application/json’)

.auth(authUser, authKey);

assert.exists(res.body);

});

});

assert.isObject(res.body); assert.deepEqual(res.body, {

username: “me”, id: “me”, provider: “local”,

familyName: “Einarrsdottir”, givenName: “Ashildr”, middleName: “”,

emails: [], photos: []

it(‘fail to find non-existent users’, async function() {

var res;

try {

res = await request.get(‘/find/nonExistentUser’)

.set(‘Content-Type’, ‘application/json’)

.set(‘Acccept’, ‘application/json’)

.auth(authUser, authKey);

} catch(e) {

return; // Test is okay in this case

}

assert.exists(res.body);

assert.isObject(res.body);

assert.deepEqual(res.body, {});

});

});

We are checking the /find operation in two ways:

  • Positive test: Looking for the account we know exists – failure is indicated if the user account is not found
  • Negative test: Looking for the one we know does not exist – failure is indicated if we receive something other than an error or an empty object

Add the following test case:

describe(‘delete user’, function() {

it(‘delete nonexistent users’, async function() {

let res;

try {

res = await request.delete(‘/destroy/nonExistentUser’)

.set(‘Content-Type’, ‘application/json’)

.set(‘Acccept’, ‘application/json’)

.auth(authUser, authKey);

} catch(e) {

return; // Test is okay in this case

}

assert.exists(res);

assert.exists(res.error);

assert.notEqual(res.status, 200);

});

});

Finally, we should check the /destroy operation. This operation is already checked the after method, where we destroy a known user account. We also need to perform the negative test and verify its behavior against an account we know does not exist.

The desired behavior is that either an error is thrown or the result shows an HTTP status indicating an error. In fact, the current authentication server code gives a 500 status code, along with some other information.

This gives us enough tests to move forward and automate the test run.

In compose-stack-test-local/docker-compose.yml, we need to inject the test.js script into the svc-userauth-test container. We’ll add that here:

svc-userauth-test:

volumes:

– type: bind

source: ./userauth

target: /userauth/test 

This injects the userauth directory into the container as the /userauth/test directory. As we did previously, we then must get into the container and run the test script.

The next step is creating a package.json file to hold any dependencies and a script to run the test:

{

“name”: “userauth-test”,

“version”: “1.0.0”,

“description”: “Test suite for user authentication server”, “scripts”: {

“test”: “cross-env URL_USERS_TEST=http://localhost:5858 mocha test.mjs”

},

“dependencies”: {

“chai”: “^4.2.0”,

“mocha”: “^7.1.1”,

“supertest”: “^4.0.2”,

“cross-env”: “^7.0.2”

}

}

In the dependencies, we list Mocha, Chai, SuperTest, and cross-env. Then, in the test script, we run Mocha along with the required environment variable. This should run the tests.

We could use this test suite from our laptop. Because the test directory is injected into the container the tests, we can also run them inside the container. To do so, add the following code to run.sh:

SVC_USERAUTH=$2

docker exec -it -e DEBUG= –workdir /userauth/test ${SVC_USERAUTH} \

rm -rf node_modules

docker exec -it -e DEBUG= –workdir /userauth/test ${SVC_USERAUTH} \

npm install

docker exec -it -e DEBUG= –workdir /userauth/test ${SVC_USERAUTH} \

npm run test 

This adds a second argument – in this case, the container name for svc-userauth. We can then run the test suite, using this script to run them inside the container. The first two commands ensure the installed packages were installed for the operating system in this container, while the last runs the test suite.

Now, if you run the run.sh test script, you’ll see the required packages get installed. Then, the test suite will be executed.

The result will look like this:

$ sh run.sh notes_svc-notes.1.c8ojirrbrv2sfbva9l505s3nv notes_svc-

userauth.1.puos4jqocjji47vpcp9nrakmy

> userauth-test@1.0.0 test /userauth/test

> cross-env URL_USERS_TEST=http://localhost:5858 mocha mjs 

Users Test

List user

list created users

find user

find created users

fail to find non-existent users

delete user

delete nonexistent users 

4 passing (312ms)

Because URL_USERS_TEST can take any URL, we could run the test suite against any instance of the user authentication service. For example, we could test an instance deployed on AWS EC2 from our laptop using a suitable value for URL_USERS_TEST.

We’re making good progress. We now have test suites for both the Notes and User Authentication services. We have learned how to test a REST service using the REST API. This is different than directly calling internal functions because it is an end-to- end test of the complete system, in the role of a consumer of the service.

Our next task is to automate test results reporting.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *