Testing Sails Apps: A Beginner's Guide
Learn Basic Testing Techniques for Building Reliable Sails Applications
Testing does not have to be complex. Some developers see testing as dull, repetitive, and too complicated. However, testing is an essential process in software development, and the benefits are :
You gain more confidence as you build.
You don't have to go through the hassle of manually testing your application.
Recently, I have been having so much fun building with Sails. I decided to become more confident in what I was building with Sails, and that's when I began my journey of testing.
This article aims to take you on an adventure as you write tests for your Sails application.
Requirements
To get the most value out of this article, you need to make sure you have gone through the previous article: CRUD in Sails.
You also need to have these modules installed:
You will find the installation steps below, do not fret if you don't have them installed.
What is Testing?
Testing is a way to ensure that what you are working on performs exactly the way you said it would. When your code is properly tested, you gain more confidence as you unleash it into the real world.
Your code ensures more trust when it is tested.
There is no official way to test an application or a chosen test framework to use; it is all based on preference. There are a lot more test frameworks out there for you to check out.
Testing your Sails app
What you will do in this section is:
Install dependencies.
Prepare the project for testing.
Make improvements to the project.
Testing the models and endpoints.
Let's get right to it!
Install dependencies
As mentioned earlier, the required tools to be installed for you to follow this article are:
Mocha
npm install mocha --save-dev
SuperTest
npm install supertest --save-dev
Preparation
Organizing your test is quite important, it helps you keep track of your test cases. Right now, you will have to:
Properly structure the project
Setup script to run the tests
Prepare the test lifecycle
Project Structure
Structuring your project is a thing of preference, feel free to setup how you want. To follow along swiftly, you should structure your project this way:
./article-api
...
|--api/
|--config/
|--node_modules/
|--test/
|----controllers/
|----models/
|____lifecycle.test.js
...
Script setup
Check the package.json
file and look at the script
object. We need a script to run your test when you want to.
Change the properties of your script object to match the ones below:
"scripts": {
"start": "NODE_ENV=production node app.js",
"test": "npm run custom-tests && echo 'Done.'",
"lint": "eslint . --max-warnings=0 --report-unused-disable-directives && echo 'โ Your .js files look good.'",
"lint:fix": "eslint . --fix",
"custom-tests": "mocha test/lifecycle.test.js test/**/*.js"
},
Notice the lifecycle.test.js
file? Let's talk about that. No, you should not run the script right now; there are no test cases yet.
Test lifecycle
This file is necessary when you want to execute a piece of code before or after your test cases. It also has more features, like executing a command before each test case or executing a command after each test case.
For the sake of this project, we only need to lift our Sails app before our test case and lower the app after our test case.
Let's get to the code. I will add a bunch of comments that you do not necessarily need in your code.
var sails = require('sails');
var supertest = require('supertest');
// `done` let's mocha know that the function is asynchronous
before(function (done) {
// increase the mocha timeout so that Sails have enough time to lift
this.timeout(5000);
sails.lift({ // start the Sails server
hooks: { grunt: false }, // load Sails without grunt hook
log: { level: 'warn' }, // disable all logs except errors and warnings
}, (err) => {
if (err) {return done(err);}
global.sails = sails; // store Sails in a global object
global.supertest = supertest; // store supertest in a global object
return done();
});
});
after((done) => {
sails.lower(done); // stop the Sails server
});
model
object in config/models.js
file from alter
or drop
. Details about this was discussed in the previous article.Make some improvements
Before you get your hands dirty with writing tests, you need to make some improvements to the project to ensure our tests are more effective.
The files that need improvement are:
Models: Article.js
Controllers: Create.js
Controllers: Update.js
Models: Article.js
Navigate to api/models
then open the Article.js
file. You will add one more feature to the title
attribute. It should look like this:
title: {
type: 'string',
required: true,
unique: true,
maxLength: 60
},
By adding unique: true
to the title
attribute, you are communicating with the database that you don't want an article with the same title to be created twice.
Controllers: create.js
You will find create.js
at api/controllers
. Since the title
attribute has been set to unique, You also need to compare an article about to be stored with the available records to ensure it does not exist before storing it. This action will simply prevent any database errors.
The steps to follow are:
Check the database for the title of the article about to be created.
If the database finds a matching article, return a
400
response.Otherwise, store the new article.
Your code should look like this:
fn: async function ({author, title, body}) {
let article = await Articles.findOne({ title: title });
if (article) {
return this.res.status(400).json('Title already exists');
}
let articleRecord = await Articles.create({ author, title, body }).fetch();
return articleRecord;
}
Tip: to find one object using MantaSails, type
fo
and you get a snippet to work with.
Controllers: update.js
Another thing to check off our improvement list is that updating the author of an article should not be possible. Why should that not be possible? Fret not; in the subsequent post, you will see how you can update an author.
How to get this one:
Check the
inputs
to see if an author object was sent.If it was sent, return a
400
response.Otherwise, proceed with updating the article.
Now you should head to your inputs
object and locate the author
attribute set required
to false
. Navigate to your function body, then write an if-statement to check if author
is not undefined
.
Your file should look like this:
module.exports = {
friendlyName: 'Update',
description: 'Update articles.',
inputs: {
author: {
type: 'string',
required: false
},
title: {
type: 'string',
required: true
},
body: {
type: 'string',
required: true
}
},
exits: {},
fn: async function ({author, title, body}) {
if (author) {
return this.res.status(400).json('Cannot update author');
}
let articleRecord = await Articles.updateOne({ id: this.req.params.id })
.set({ title, body });
return articleRecord;
}
};
ur
and you get a snippet to work with.Bravo! ๐ You just improved the CRUD operation of this project. Now let the testing begin! ๐ฅ
Testing your Sails app
In this testing phase, you will write tests for models and endpoints. Ensure that you name the test files according to how you see them in this article, this will enable the test to run just the way you arrange it.
Model testing
I'm glad you made it here; without further hesitation, let's get to it.
Three test cases will be addressed in this section.
Test for creating objects.
Test for fetching all objects.
Test for fetching a single object.
Test case: 1.create.test.js
This test case is to test if the database used to create a user works as it should.
describe('#createArticle()', () => {
it('should create an article', async () => {
var article = await Articles.createEach([{
author: 'Test',
title: 'Test title',
body: 'Test body'
}, {
author: 'Alex Hormozi',
title: '100M offers',
body: 'This is another body'
}, {
author: 'Alex Hormozi',
title: '100M offers book',
body: 'This is another body again'
}]).fetch();
if (!article) {
return new Error('Should create an article but it failed');
}
});
});
The test case above takes multiple objects and simultaneously stores them in the database. After storing, we fetch the objects, and then we check if article
is undefined
.
Just like that, you have completed your first test case.
Test case: 2.fetchAll.test.js
Another thing you can test is to fetch all records for a model in the database.
describe('#fetchAllArticles()', () => {
it('should fetch all articles', (done) => {
Articles.find({})
.then((data) => {
if (data.length < 1) {
return done(new Error('No article created'));
}
return done();
})
.catch(done);
});
});
After querying the database for all records, the length of the data is checked to be sure that it is greater than one, or else an error should be returned.
Test case: 3.fetchSingle.test.js
The final test case is to check if it is possible to fetch a single item from the database without an error.
describe('#fetchSingleArticle()', () => {
it('should fetch single article', (done) => {
Articles.findOne({title: 'Test title'})
.then((data) => {
if (!data) {
return done(new Error('Article not found'));
}
return done();
})
.catch(done);
});
});
It is pretty straightforward, right? Search for an article and check if it was found.
Awesome! To clear the air, whether this works or not, run the npm command below:
npm run test
If all is fair and just, then you should see only greens. ๐
Endpoint testing
If the previous test cases were fun, trust me, you will love this one.
For this set of test cases, we need them to run in a particular order. Hence, we will add a number as part of the name of the files in the order we want them to be executed.
The test cases that will be addressed are:
Test for creating an article.
Test for fetching all articles.
Test for fetching a single article.
Test for updating an article.
Test for deleting an article.
Test case: 1.create.test.js
A POST
request will be sent by the test case containing an object, and then it expects a particular response code. If it does not match, then return an error.
describe('POST /articles', () => {
it('should create an article', (done) => {
global.supertest(sails.hooks.http.app)
.post('/articles')
.send({
'author': 'Alex Hormozi',
'title': '100M offers books',
'body': 'This is another body againnnnn'
})
.expect(200, done);
});
});
global.supertest
takes the place of a request module that sends the requests to the desired endpoints.
Test case: 2.fetchAll.test.js
To keep the test case simple, an external function was used to operate. As the name suggests, the operation is.
A GET
request is sent to the desired endpoint. Once a response is received, the function checks the length of the response to see if it is empty or not.
describe('GET /articles', () => {
it('should fetch all articles', (done) => {
global.supertest(sails.hooks.http.app)
.get('/articles')
.expect(checkBodyLength)
.expect(200, done);
});
});
function checkBodyLength(res) {
if (res.body.length < 1) {
throw new Error('missing body!');
}
}
Test case: 3.fetchSingle.test.js
This test case is quite peculiar. It sequentially executes two instances.
The first instance:
Queries the database for articles
Pick the first item
Dynamically makes use of the ID to fetch a single article from the endpoint
Expect
200
response
The second instance sends a random string to the same endpoint and expects a 404
response.
describe('GET /articles/:id', () => {
it('should fetch single article', (done) => {
Articles.find({})
.then(data => {
global.supertest(sails.hooks.http.app)
.get(`/articles/${data[0].id}`)
.expect(200, done);
})
.catch((err) => {
throw new Error(err);
});
});
it('should not fetch single article', (done) => {
global.supertest(sails.hooks.http.app)
.get('/articles/999')
.expect(404, done);
});
});
Test case: 4.update.test.js
Just as in the previous test case, this executes two instances. Which is more like expecting a positive and preparing for a negative.
The pattern for dynamically inputting an ID is the same for both instances. The difference between them is that one expects a 200
response and the other expects a 400
response.
describe('PATCH /articles/:id', () => {
it('should update single article', (done) => {
Articles.find({})
.then(data => {
global.supertest(sails.hooks.http.app)
.patch(`/articles/${data[0].id}`)
.send({
title: data[0].title,
body: 'This is updated body'
})
.expect(200, done);
})
.catch((err) => {
throw new Error(err);
});
});
it('should not update single article', (done) => {
Articles.find({})
.then(data => {
global.supertest(sails.hooks.http.app)
.patch(`/articles/${data[0].id}`)
.send({
author: data[0].author,
title: data[0].title,
body: 'This is updated body'
})
.expect(400, done);
})
.catch((err) => {
throw new Error(err);
});
});
});
Test case: 5.delete.test.js
This test case also follows the same pattern of dynamically inputting an ID. What it does is send the chosen ID to the delete
endpoint. I think you know what happens from there.
describe('DELETE /articles/:id', () => {
it('should delete single article', (done) => {
Articles.find({})
.then(data => {
global.supertest(sails.hooks.http.app)
.delete(`/articles/${data[0].id}`)
.expect(200, done);
})
.catch((err) => {
throw new Error(err);
});
});
});
To see what the test output of our test cases will be, run the command below:
npm run test
You can find the code for this project here.
Conclusion
Hurray! ๐ You crushed testing a Sails app. This is a great start for you to build more and test more. As I mentioned earlier, testing your code comes with confidence. The more you test, the more confidence you gain.
In the subsequent articles, you will implement authentication in our API using JWT. You will set up the authentication from the ground up, and it will look like you built an actual NPM package, so brace yourself!
Until next time, remember...
"Look for the highest level of whatever you can achieve, then pay the price for it." - Bo Eason.