Introduction
We will be using the code from the 2nd part of this series.
You can find the code in this Github repository, note that you are on the testing branch.
hmu @tekbog if you find any bugs or want to annoy me.
About testing and QA
People still fight over definition and how it should be done, however this article from IBM might give you some insight if you are unfamiliar with software testing.
To largely simplify testing we can define some concepts:
- Unit tests: as the definition implies we will test simple units of code, one function at a time.
- Integration tests or I&T: is when you test several functions or different pieces of code that work together
- End to End testing: as the name implies you fully test your whole application, often as an emulated user
- Test Driven Development: write the test for the function you want, then write the function
A lot of different tools exist for almost any language, in JavaScript the most famous ones are Jest, Mocha and Cypress for E2E. However the overall most famous E2E tool might be Selenium.
Git
I'm not going to go into detail here, if you unfamiliar with Git and want to learn more about check out this tutorial from Atlassian. Start at the beginning if you are confused. A few key points for newbies:
- Git is a version control tool
- Github is the hub that hosts Git projects, it's owned by others and there are other companies such as Bitbucket and Gitlab that do the same.
I'm using my old project so I'm going to make a new branch with
git checkout -b testing main
and stay here as this will be the branch for this article.
Jest (or Mocha)
We will be using Jest since we want to focus on React, however if you are working with Node.js and Mongoose it's discouraged even in the official Mongoose docs so feel free to use an alternative like Mocha. We will use Jest as our application isn't complex enough to run into problems and we will focus on React in the future.
Let's install Jest
npm install --save-dev jest
Afterwards inside package.json
modify the test script
"test": "jest --verbose"
You should have this:
Start testing
Now that we have Jest installed let's make some dummy tests to see how it works.
NB: I will be using normal import/export as ES6 module exports might give you some issues.
Let's make a helper file inside utils folder that will contain our function we want to test test_helper.js
:
//return the sum of two numbers
const sumFunction = (a,b) => {
return a+b
}
module.exports = {
sumFunction
}
Next we will make a folder called tests and inside a file called dummy.test.js where we will test the function above:
const { sumFunction } = require("../utils/test_helper")
//tests take a description and a function
test('test sumFunction, expected to pass', () => {
const a = 5
const b = 6
const result = sumFunction(a, b)
//through expect we indicate the expected result of the test
//the .toBe method expects a specific value you pass
//https://jestjs.io/docs/expect#tobevalue
expect(result).toBe(11)
})
test('test sumFunction, expected to fail', () => {
const a = 5
const b = 6
const result = sumFunction(a, b)
expect(result).toBe(12)
})
Note that .test is needed so Jest knows to read your tests from this file. Now let's do
npm run test
This should be the result:
Check out many of the Expect methods Jest has here
Testing a RESTful API
We will be using ES6 modules so we need to make some changes in our package.json as follows:
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --verbose --runInBand --forceExit"
Jest gives you more information here.
We will be using Supertest, so let's install it
npm install supertest
Now let's make a file inside tests called api.test.js
where we will be importing everything we need to make our tests:
import supertest from 'supertest'
import { app } from '../app.js'
//use the supertest object as our API
const api = supertest(app)
import { Item } from '../models/item'
Then remember to run our server with npm start dev
so it restarts every time we make changes and open a new window terminal so we can type our Jest commands.
GET test
Since we are getting our data from a server we will need to use async functions, keep that in mind.
This is a test for our GET call expecting a 200 status:
test('GET call', async () => {
await api
.get('/api/items')
.expect(200)
})
You can run this test through this command
run npm test -- -t "GET call"
That's how you run individual tests.
In order to make the test a bit more complete we will also add a method to expect JSON data:
test('GET call', async () => {
await api
.get('/api/items')
.expect(200)
.expect('Content-Type', /application\/json/)
})
Your api.test.js
file should look like this by now
import supertest from 'supertest'
import { app } from '../app.js'
//use the supertest object as our API
const api = supertest(app)
//run npm test -- -t "GET call"
//test GET or READ call on localhost:3001/api/items endpoint
test('GET call', async () => {
await api
.get('/api/items')
.expect(200)
.expect('Content-Type', /application\/json/)
})
POST test
In order to interact with the database properly we will use our Mongoose model
import { Item } from '../models/item'
Afterwards we will proceed to make a POST call
test('POST call', async () => {
//build a new item
const newItem = {
description:"sent from Jest!",
likes: 10
}
//we send the item object to the DB through the API
//we expect a successful result
await api
.post('/api/items')
.send(newItem)
.expect(201)
//get all the items in our DB
const items = await Item.find({})
//let's check that the last item added was indeed newItem object
//it should contain the description "sent from Jest!"
expect(items[items.length-1].description).toBe("sent from Jest!")
})
GET item by id
In this test we are going to check if the GET route by specific id works as expected.
test('GET one', async () => {
//get all the items
const items = await Item.find({})
//get the the first item parsed to JSON
const firstItem = items[0].toJSON()
//get the result expecting success and JSON data
const resItem = await
api.get(`/api/items/${firstItem.id}`)
.expect(200)
.expect('Content-Type', /application\/json/)
//check if the item has the same id and the route works as expected
expect(resItem.body.id).toEqual(firstItem.id)
})
Check all items have an id
Let's add one more simple test
test('check all items have ids', async () => {
//get all the items
const items = await api.get('/api/items')
//check that every item in our DB has the id property
for(const item of items.body){
expect(item.id).toBeDefined()
}
})
Refactoring GET calls
Let's use describe to put our three GET calls together as follows:
describe('GET calls', () => {
//run npm test -- -t "GET call"
//test GET or READ call on localhost:3001/api/items endpoint
test('GET call', async () => {
await api
.get('/api/items')
.expect(200)
.expect('Content-Type', /application\/json/)
})
//npm test -- -t "GET one"
//GET item by id
test('GET one', async () => {
//get all the items
const items = await Item.find({})
//get the the first item parsed to JSON
const firstItem = items[0].toJSON()
//get the result expecting success and JSON data
const resItem = await
api.get(`/api/items/${firstItem.id}`)
.expect(200)
.expect('Content-Type', /application\/json/)
//check if the item has the same id and the route works as expected
expect(resItem.body.id).toEqual(firstItem.id)
})
test('check all items have ids', async () => {
//get all the items
const items = await api.get('/api/items')
//check that every item in our DB has the id property
for(const item of items.body){
expect(item.id).toBeDefined()
}
})
})
Afterwards we can use this cmd to call this block of tests
npm test -- -t "GET calls"
If everything is successful we will get this in the console:
DELETE
Let's check that we can delete an item and all its data is gone
test('DELETE item', async () => {
//get items and parse the one you want to delete to JSON
const itemsAtStart = await Item.find({})
const itemToDelete = itemsAtStart[0].toJSON()
//delete the item by id
await api
.delete(`/api/items/${itemToDelete.id}`)
.expect(204)
//get all items from the database again
const itemsNow = await Item.find({})
//check if the number of current items is one less than before
expect(itemsNow).toHaveLength(itemsAtStart.length-1)
//get an array of all the descriptions inside the DB
//could get any other info like the id
const itemsDescriptions = itemsNow.map(i => i.toJSON().description)
//expect the description from the deleted item to not be there
expect(itemsDescriptions).not.toContain(itemToDelete.description)
})
beforeEach
If you want to run any code before tests start you need to use beforEach. In our case we want our database to reset every time we start testing so let's make some data to insert inside a file called test_helper.js
const initialItems = [
{
description: "first item",
likes: 5
},
{
description: "second item",
likes: 7
},
{
description: "third item",
likes: 10
}
]
export {
initialItems
}
then import that data and use it to reset our database every time we start testing. We delete everything and then insert our initial data again:
beforeEach(async () => {
await Item.deleteMany({})
await Item.insertMany(initialItems)
})
afterAll
If you want to run something after all our tests are done then use afterAll
afterAll(() => mongoose.connection.close())
In our case we just close the connection to the database.