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:
- Mocha (Testing framework)
- Chai (Assertion Library)
- Istanbul/Nyc (Code coverage)
- Supertest (http mocking)
- Sinon (Mocking, Stubbing, etc)
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.
- You are using NodeJS and Express
- You have some form of models / controllers / routes
- The examples below are unit tests
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:
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.
You want to look at this here to get started.
Basically,
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
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.