Skip to content

Instantly share code, notes, and snippets.

@snggeng
Last active July 15, 2022 12:25
Show Gist options
  • Save snggeng/c61cc3a8eebafdf92d2fd23227fac71c to your computer and use it in GitHub Desktop.
Save snggeng/c61cc3a8eebafdf92d2fd23227fac71c to your computer and use it in GitHub Desktop.
Setting up Automated Testing & CI

Automated Testing in JS

We talked about testing / debugging in class today, and I wanted to provide a guide for classmates who are working on a Javascript stack. There are multiple testing frameworks available in the JS ecosystem, but our group uses:

Testing Stack (Tools)

Continuous Integration (CI)

You should take a look at this article and decide on the framework that best suits your needs. Personally I've heard great things about Jest and CircleCI.

Assumptions

  • You are using NodeJS and Express
  • You have some form of models / controllers / routes
  • The examples below are unit tests

Installation

cd into the root directory of your server-side repository where your package.json and node_modules are located.

Install the dependencies using npm / yarn:

yarn add --dev mocha, chai, nyc, sinon, supertest

In your package.json you should see this:

  "devDependencies": {
    "chai": "^4.1.2",
    "concurrently": "^3.5.0",
    "mocha": "^4.1.0",
    "nyc": "^11.4.1",
    "supertest": "^3.0.0"
  }

Create a test directory in the root directory. It is important that this folder is called test because mocha will look for a directory called test when running test commands.

Now add these commands to your package.json:

"test-dev": "NODE_ENV=test nodemon --exec \"nyc mocha --timeout 15000 --recursive --exit\"",
"test": "NODE_ENV=test nyc mocha --timeout 15000 --recursive --exit",

Above are two commands: test-dev and test. We'll use test to run tests and test-dev to automatically restart tests.

If you are not using NODE_ENV or some kind of way to differentiate your different environments (e.g. development, production, test), you can remove NODE_ENV=test from the commands.

Notice we are using nodemon to watch our tests in the test-dev command. What that does is allow you to keep your tests running and nodemon will detect changes and restart your tests automatically.

The actual commands that run our tests are mocha. So if you run mocha it will run the test files in your test directory.

We want to add code coverage to our tests, so we add nyc in front of mocha, which will generate a command-line output and a report after your tests are run. The --timeout option allows us to specify to mocha how long it should allow a test to run (for async tests sometimes the test can timeout because it takes too long) while the --recursive option tells mocha to traverse your test directory recursively in case you have nested sub-directories in it. The --exit option tells mocha to exit after all tests are run.

You can run individual tests by specifying the filename as a command line argument like so:

yarn test ./test/server_test.js

Once you have the above setup, we can now write our first test:

Writing Tests

Create a file inside ./test called server_test.js and paste the following code in:

/* globals describe before after it */
const { expect } = require('chai')
const supertest = require('supertest')
const mongoose = require('mongoose')

// Loading express
//   ✓ should respond with 200 to /
//   ✓ should respond with 404 for unspecified endpoints

describe('Server Tests', () => {
  let api
  beforeEach(async () => {
    try {
      api = supertest(require('../app'))
      await mongoose.connection.dropDatabase()
    } catch(err) {
      console.log(err)
    }
  })

  // GET /
  //     ✓ should return a 200 response
  describe('GET /', () => {
    it('should return a 200 response', (done) => {
      api.get('/')
      .set('Accept', 'application/json')
      .expect(200, (err, res) => {
        if (err) { return done(err) }
        done()
      })
    })
  })

  // Loading Express
  // ✓ should respond with 200 to /
  // ✓ should respond with 404 for unspecified endpoints
  describe('Loading Express', () => {
    it('should respond with 200 to /', (done) => {
      api.get('/')
        .expect(200, done)
    })
    it('should respond with 404 for unspecified endpoints', (done) => {
      api.get('/foo/bar')
        .expect(404, (err, res) => {
          expect(err).to.be.a('null')
          expect(res).to.have.property('error')
          expect(res.body).have.property('error')
          expect(res.body).have.property('message')
          done()
        })
    })
  })

  // Error handling
  // ✓ should handle errors
  // ✓ should disallow unauthenticated API calls
  describe('Error handling', () => {
    it('should handle errors', async () => {
      await api.get('/foo')
      .set('Accept', 'application/json')
      .then((error, response) => {
        expect(error).to.have.property('status')
        expect(error.body).to.have.property('message')
        expect(error.body).to.have.property('error')
        expect(response).to.be.an('undefined')
      })
    })

    it('should disallow unauthenticated API calls', async () => {
      await api.get('/api/ingredients/1')
        .expect((err, res) => {
          expect(err.body).to.have.property('message')
          expect(err.body).to.have.property('error')
          expect(err.status).to.equal(401)
        })
    })
  })
})

You should visit the mocha and chai documentation to figure out how to write tests, but basically what the above does is make sure that our server, express, is working and sends the right status codes for different requests. Notice we used a beforeEach hook that drops our database before each test runs. This is a useful pattern that you can use to isolate testing environments for each test. You'll have to do your own research on how to actually write and use these tests.

Now run the command:

yarn test

This will run all of the tests in our ./test directory but since we only have one test, you'll only see one test being run.

You'll see something like this:

15:38 🐑 💨  yarn test ./test/server_test.js 
yarn run v1.3.2
$ NODE_ENV=test nyc mocha --timeout 15000 --recursive --exit ./test/server_test.js


  Server Tests
    GET /
      ✓ should return a 200 response
    Loading Express
      ✓ should respond with 200 to /
      ✓ should respond with 404 for unspecified endpoints
    Error handling
      ✓ should handle errors
      ✓ should disallow unauthenticated API calls


  5 passing (4s)

-----------------|----------|----------|----------|----------|----------------|
File             |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
-----------------|----------|----------|----------|----------|----------------|
All files        |    27.36 |      0.7 |     1.51 |    31.67 |                |
 config          |    53.06 |     7.14 |    18.18 |    54.17 |                |
  footprint.js   |       60 |      100 |        0 |       60 |    12,16,21,26 |
  index.js       |      100 |       50 |      100 |      100 |             21 |
  passport.js    |       60 |        0 |    33.33 |       60 |... 15,17,18,20 |
  seed.js        |       35 |        0 |        0 |    36.84 |... 27,28,29,31 |
 controllers     |    14.38 |        0 |        0 |     17.7 |                |
  carts.js       |    16.92 |        0 |        0 |    20.75 |... 95,97,98,99 |
  formulas.js    |     13.1 |        0 |        0 |    15.49 |... 131,132,133 |
  ingredients.js |    12.36 |        0 |        0 |    15.28 |... 117,118,120 |
  order_carts.js |    16.92 |        0 |        0 |    20.75 |... 92,94,95,96 |
  orders.js      |    11.32 |        0 |        0 |    15.19 |... 137,138,139 |
  productions.js |     8.94 |        0 |        0 |    12.79 |... 136,137,138 |
  spendings.js   |    19.05 |        0 |        0 |    19.23 |... 169,170,172 |
  stocks.js      |    18.46 |        0 |        0 |    21.43 |... 101,102,103 |
  storages.js    |    14.52 |        0 |        0 |       20 |... 75,81,82,83 |
  users.js       |       16 |        0 |        0 |    19.28 |... 157,165,167 |
  vendors.js     |    14.61 |        0 |        0 |    18.06 |... 131,132,134 |
 middleware      |       50 |     37.5 |       50 |    52.17 |                |
  auth.js        |    57.14 |       75 |      100 |    57.14 |... 22,23,34,35 |
  permission.js  |       40 |        0 |    33.33 |    44.44 |   7,8,10,11,12 |
 models          |       40 |        0 |        0 |    41.88 |                |
  cart.js        |       50 |        0 |        0 |    51.72 |... 68,70,71,72 |
  formula.js     |    84.62 |      100 |        0 |    84.62 |          65,66 |
  ingredient.js  |    54.17 |        0 |        0 |    54.17 |... 65,67,69,70 |
  order_cart.js  |    32.69 |        0 |        0 |    36.17 |... 123,124,129 |
  production.js  |    24.69 |        0 |        0 |    25.97 |... 191,193,194 |
  stock.js       |    84.62 |      100 |        0 |    84.62 |          48,49 |
  storage.js     |    39.02 |        0 |        0 |       40 |... 90,91,93,94 |
  user.js        |    38.24 |        0 |        0 |    39.39 |... 77,78,79,91 |
  vendor.js      |     38.3 |        0 |        0 |    40.91 |... 126,127,131 |
 routes          |    98.86 |      100 |        0 |      100 |                |
  api.js         |      100 |      100 |      100 |      100 |                |
  public.js      |    88.89 |      100 |        0 |      100 |                |
 utils           |    29.03 |        0 |        0 |    33.33 |                |
  dates.js       |    33.33 |        0 |        0 |    33.33 |        2,4,6,7 |
  index.js       |       28 |        0 |        0 |    33.33 |... 33,34,35,36 |
-----------------|----------|----------|----------|----------|----------------|
✨  Done in 5.50s.
✔ ~/dev/ece458/hypothetical-meals [gs/sqft ↑·8|✚ 4]
15:47 🐑 💨  

Awesome, our tests are passing! For more information about how to test REST APIs and how to write tests using supertest, sinon, or whichever framework you chose, just find a tutorial online that uses your frameworks. One example is here.

Now that we have tests, lets hook up CI.

CI

You want to look at this here to get started.

Basically,

Prerequisites

To start using Travis CI, make sure you have all of the following:

  • GitHub login
  • Project hosted as a repository on GitHub
  • Working code in your project
  • Working build or test script

To get started with Travis CI

Using your GitHub account, sign in to either

  • Travis CI .org for public repositories
  • Travis CI .com for private repositories and accept the GitHub access permissions confirmation.

Once you’re signed in to Travis CI, and we’ve synchronized your GitHub repositories, go to your profile page and enable the repository you want to build: enable button Add a .travis.yml file to your repository to tell Travis CI what to do. This is what ours looks like:

language: node_js
node_js:
  - "lts/*"

services:
  - mongodb

"lts/*" stands for Long Term Support which is recommended for any NodeJS project in production. Current LTS is at v8.9.0.

That's it! You're now up on CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment