- Describe the importance of testing your code programmatically
- Use describe and assertion functions to do basic testing
We've now created a number of applications. All these apps cover a single topic, so most of the time, they are quite small. But when you create a larger application, the codebase will become bigger and more complex every time you add some features. At some point, adding code in file A will break features in file B, and to avoid these "side-effects" or at least recognize immediately when they happen, we need to write tests our app and run them on each change. In a production-level application, providing a high level of test coverage for an application is usually required in order to guarantee that code is bug-free and functions as intended.
See: Types of Software Testing
- How tests are executed:
- Manual - user runs test via the UI
- Automated - test scripts are executed that call into the code and compare results to expected values
- Granularity:
- Unit - focuses on testing individual "units" of code, usually individual components, functions or methods
- Integration - set of components that are collaborating (interacting) to perform a task
- End-to-end (E2E) - complete application running in an environment that mimics a real-world production environment
- Purpose:
- Functional
- Positive testing - does it work when it is supposed to work.
- Negative testing - does it fail when it is supposed to fail.
- Regression - Did we break anything?
- Smoke - Did the build work?
- Performance / Load - How does the software behave under a heavy load?
- Lots of users / traffic
- Large data sets
- Usability - How intuitive (easy to use) is the software?
- Security - How secure is the application?
- Compatibility - How well does the software work with various hardware, O.S., network environments?
- Recovery - How well does the system respond to hardware or software failures? Is it fault-tolerant?
- User Acceptance Testing (UAT) - Does the software do what the customers want it to do?
- Actual software users test the software to make sure it can handle required tasks in real-world scenarios, according to specifications.
- Functional
A development methodology of writing the tests first, then writing the code to make those tests pass. Thus the process is:
- Define a test set for the unit
- Implement the unit
- Verify that the implementation of the unit makes the tests succeed
- Refactor
- Repeat
A development methodology that was derived from TDD
and DDD
(Domain-driven design) where tests are written in an English-like language (i.e. the Gherkin
language) that specifies the external behavior (the specifications) of the unit without reference to how the unit was implemented (thus it is a form of black box testing). The purpose of BDD is to both describe and test the behavior of a unit of code in a single specification file.
See What’s the difference between Unit Testing, TDD and BDD?
To test our code in Node, we will use two primary libraries: one to run the tests and a second one to run the assertions.
Mocha will be our testing framework. From the Mocha Website:
"Mocha is a feature-rich JavaScript test framework running on Node.js and the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases."
For assertions, we will use Chai. From the Chai website:
"Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework."
To be able to make HTTP requests inside tests, we will use Supertest:
"The motivation with this module is to provide a high-level abstraction for testing HTTP"
Take the starter code provided in the starter-code
directory of this lesson plan. Take a moment to familiarize yourself with the Express app and get everything set up. Be sure to npm install
and start the server.
git clone https://github.com/ga-wdi-lessons/mocha-chai-supertest
cd mocha-chai-supertest/starter-code/app
npm install
node app.js
To test this app, we need to install a couple of dependencies.
First, let's install mocha using --save-dev:
npm install mocha --save-dev
Then we will install chai using --save-dev
npm install chai --save-dev
Last dependency we need to install is supertest:
npm install supertest --save-dev
Now that we're configured, let's set up our file and folder structure. All the tests will be written inside a folder test
at the root of the app:
mkdir test
Then we will write the tests inside a file called candies_tests.js
:
touch test/candies_tests.js
Note: Because our tests will request the application through HTTP, students have to make sure they are running the app while running the tests
Open the file candies_tests.js
. We now need to require some dependencies at the top of this file:
var should = require("chai").should(),
expect = require("chai").expect,
supertest = require("supertest"),
api = supertest("http://localhost:3000")
Make sure you set the url correctly, as this will be used to request the app and analyze the response.
All the tests need to be inside a describe
function. We will use one describe block per route:
describe("GET /candies", function(){
//tests will be written inside this function
});
First, we will write a test to make sure that a request to the index path /candies
returns a http status 200:
describe("GET /candies", function(){
it("should return a 200 response", function(done){
api.get("/candies")
.set("Accept", "application/json")
.expect(200, done)
})
})
Note: Make sure that the done is in the it block and not the describe block!
Now go in the command line and type mocha
. When you do, you may get an error saying that the mocha
command cannot be found. This is because any npm package we want to run from the command line needs to be installed globally. While we could simply install mocha globally and run the test, we would not be using the specified version of mocha listed a dev dependency in our package.json
and contained in node_modules
. In order to run mocha from our local node_modules
folder, we can either:
- Run mocha directly from our
node_modules
folder:node_modules/.bin/mocha
- Alias the
mocha
command to an npm script in ourpackage.json
:{ "name": "express-routing-lab", "version": "0.0.0", "description": "", "main": "app.js", "scripts": { "test": "mocha" }, "dependencies": { ... } ... }
One thing to keep in mind when using NPM to run tests (really running anything with NPM scripts for that matter) is that NPM will prefer local node modules over globally installed modules. If something has not been installed properly locally this could lead to differing behavior between running
mocha
andnpm test
.
You will know the test successfully ran when if get an output looking like this:
This test is passing!
Every block of code that starts with it()
represents a test.
The callback
represents a function that Mocha will pass to the code so that the next test will be executed only when the current is finished and the done
function is called - this allows tests to be executed once at a time.
Now, let's verify the content of the response by looking at the data sent back by hitting the /candies
endpoint:
[{
"id": 1,
"name": "Chewing Gum",
"color": "Red"
}, {
"id": 2,
"name": "Pez",
"color": "Green"
}, {
"id": 3,
"name": "Marshmallow",
"color": "Pink"
}, {
"id": 4,
"name": "Candy Stick",
"color": "Blue"
}]
We can write a test that verifies the response is an array:
it("should return an array", function(done){
api
.get("/candies")
.set("Accept", "application/json")
.end(function(error, response){
expect(response.body).to.be.an('array');
done()
})
})
NB: In the first test, we were using the .expect
method of supertest
. Here we are using the expect function provided by chai
.
We can write another test that verifies the presence of a field in the response:
it("should return an array of objects that have a field called 'name' ", function(done){
api
.get("/candies")
.set("Accept", "application/json")
.end(function(error, response){
expect(response.body[0]).to.have.property('name');
done()
})
})
We can also send data to the server and test the behavior - in our case, we want to make sure that when we post some JSON to /candies
, a new object is added to the array candies.
Because we are going to test another route, lets add another describe block:
describe("POST /candies", function(){
})
For this test, we need to:
- Create a new object by sending a
POST
request - Verify that a new object has been "saved" by requesting the index route
For this, we will use before
blocks. A before
block will be executed for every it
function is called inside a describe
block.
Add this inside the new describe
block:
before(function(done){
api.post("/candies")
.set("Accept", "application/json")
.send({
"id": 5,
"name": "Lollipop",
"color": "Red"
}).end(done)
})
This code will be called for every test we will add into the current describe
block.
Now, we can verify that calling "POST" will add an object to candies:
it("should add a candy object to the collection candies and return it", function(done){
api.get("/candies")
.set("Accept", "application/json")
.end(function(error, response){
expect(response.body.length).to.equal(5);
done()
})
})
Run the mocha
command in the terminal, you should now have four passing tests!
How many times can you run this test and have it pass? How can you fix this?
Note: This can be a pair programming activity or done independently.
Add tests to the suite:
- Write a test that makes sure the object returned when you call the
show
route with a specific ID contains the right fields. - Write a test that ensures an object is deleted from the array candies when you call delete.
- Write a test that ensures a property is updated when you call
PUT /candies/:id
Review the answers to the tests specs above
We've covered the principles of testing in JavaScript, but Chai offers a lot of different expectations syntaxes. Check the Chai Documentation
- How does Mocha work with Chai to write tests in your JavaScript application?
- Describe how to configure your app to use Mocha and Chai.