Testing Sails Apps: A Beginner's Guide

Testing Sails Apps: A Beginner's Guide

Learn Basic Testing Techniques for Building Reliable Sails Applications

ยท

11 min read

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:

  • Mocha will be used to test your models.

  • Supertest will be used to test your endpoints.

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:

  1. Install dependencies.

  2. Prepare the project for testing.

  3. Make improvements to the project.

  4. 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
});
๐Ÿ’ก
Notice: It is advisable to set the 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:

  1. Check the database for the title of the article about to be created.

  2. If the database finds a matching article, return a 400 response.

  3. 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:

  1. Check the inputs to see if an author object was sent.

  2. If it was sent, return a 400 response.

  3. 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;
  }
};
๐Ÿ’ก
To update one object using MantaSails, type 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.

  1. Test for creating objects.

  2. Test for fetching all objects.

  3. 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:

  1. Test for creating an article.

  2. Test for fetching all articles.

  3. Test for fetching a single article.

  4. Test for updating an article.

  5. 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:

  1. Queries the database for articles

  2. Pick the first item

  3. Dynamically makes use of the ID to fetch a single article from the endpoint

  4. 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.

ย