Git Product home page Git Product logo

express-testing-mocha-knex's Introduction

Testing Node and Express

This tutorial looks at how to test an Express CRUD app with Mocha and Chai. Although we'll be writing both unit and integration tests, the focus will be on the latter so that tests run against the database in order to test the full functionality of our app. Postgres will be used, but feel free to use your favorite relational database.

Let's get to it!

Objectives

By the end of this tutorial, you will be able to...

Why Test?

Are you currently manually testing your app?

When you push new code do you manually test all features in your app to ensure the new code doesn't break existing functionality? How about when you're fixing a bug? Do you manually test your app? How many times?

Stop wasting time!

If you do any sort of manual testing write an automated test instead. Your future self will thank you.

Getting Started

Project Set Up

To quickly create an app boilerplate install the following generator:

$ npm install -g [email protected]

Make sure you have Mocha, Chai, Gulp, and Yeoman installed globally as well:

Create a new project directory, and then run the generator to scaffold a new app:

$ yo galvanize-express

NOTE: Add your name for the MIT License and do not add Gulp Notify.

Make sure to review the structure.

Install the dependencies:

$ npm install

Finally, let's run the app to make sure all is well:

$ gulp

Navigate to http://localhost:3000/ in your favorite browser. You should see:

Welcome to Express!
The sum is 3

Database Set Up

Make sure the Postgres database server is running, and then create two new databases in psql, for development and testing:

# create database express_tdd;
CREATE DATABASE
# create database express_tdd_testing;
CREATE DATABASE
#

Install Knex and pg:

$ npm install [email protected] [email protected] --save-dev

Run knex init to generate a new knexfile.js file in the project root, which is used to store database config. Update the file like so:

module.exports = {
  development: {
    client: 'postgresql',
    connection: 'postgres://localhost:5432/express_tdd',
    migrations: {
      directory: __dirname + '/src/server/db/migrations'
    },
    seeds: {
      directory: __dirname + '/src/server/db/seeds'
    }
  },
  test: {
    client: 'postgresql',
    connection: 'postgres://localhost:5432/express_tdd_testing',
    migrations: {
      directory: __dirname + '/src/server/db/migrations'
    },
    seeds: {
      directory: __dirname + '/src/server/db/seeds'
    }
  }
};

Here, different database configuration is used based on the app's environment, either development or test. The environment variable NODE_ENV will be used to change the environment. NODE_ENV defaults to development, so when we run test, we'll need to update the variable to test.

Next, let's init the connection. Create a new folder with "server" called "db" and then add a file called knex.js:

const environment = process.env.NODE_ENV;
const config = require('../../../knexfile.js')[environment];
module.exports = require('knex')(config);

The database connection is establish by passing the proper environment to knexfile.js which returns the associated object that gets passed to the knex library in the third line above.

Now is a great time to initilize a new git repo and commit!

Test Structure

With that complete, let's look at the current test structure. In the "src" directory, you'll notice a "test" directory, which as you probably guessed contains the test specs. Two sample tests have been created, plus there is some basic configuration set up for JSHint and JSCS.

Run the tests:

$ npm test

They all should pass:

jscs
  ✓ should pass for working directory (357ms)

routes : index
  GET /
    ✓ should render the index (88ms)
  GET /404
    ✓ should throw an error

jshint
  ✓ should pass for working directory (247ms)

controllers : index
  sum()
    ✓ should return a total
    ✓ should return an error


6 passing (724ms)

Glance at the sample tests. Notice how we are updating the environment variable at the top of each test:

process.env.NODE_ENV = 'test';

Remember what this does? Now, when we run the tests, knex is intilized with the test config.

Schema Migrations

To keep the code simple, let's use one CRUD resource, users:

Endpoint HTTP Result
users GET get all users
users/:id GET get a single user
users POST add a single user
users/:id PUT update a single user
users/:id DELETE delete a single user

Init a new migration:

$ knex migrate:make users

This command created a new migration file within the "src/server/db/migrations" folder. Now we can create the table along with the individual fields:

Field Name Data Type Constraints
id integer not null, unique
username string not null, unique
email string not null, unique
created_at timestamp Not null, default to current date and time

Add the following code to the migration file:

exports.up = (knex, Promise) => {
  return knex.schema.createTable('users', (table) => {
    table.increments();
    table.string('username').unique().notNullable();
    table.string('email').unique().notNullable();
    table.timestamp('created_at').defaultTo(knex.fn.now());
  });
};

exports.down = (knex, Promise) => {
  return knex.schema.dropTable('users');
};

Apply the migration:

$ knex migrate:latest --env development

Make sure the schema was applied within psql:

# \c express_tdd
You are now connected to database "express_tdd".
# \dt
                   List of relations
 Schema |         Name         | Type  |     Owner
--------+----------------------+-------+---------------
 public | knex_migrations      | table | michaelherman
 public | knex_migrations_lock | table | michaelherman
 public | users                | table | michaelherman
(3 rows)

# select * from users;
 id | username | email | created_at
----+----------+-------+------------
(0 rows)

Seed

We need to see the database to have some basic data to work with. Init a new seed, which will add a new seed file to "src/server/db/seeds/":

$ knex seed:make users

Update the file:

exports.seed = (knex, Promise) => {
  // Deletes ALL existing entries
  return knex('users').del()
  .then(() => {
    return Promise.all([
      // Inserts seed entries
      knex('users').insert({
        username: 'michael',
        email: '[email protected]'
      }),
      knex('users').insert({
        username: 'michaeltwo',
        email: '[email protected]'
      })
    ]);
  });
};

Run the seed:

$ knex seed:run --env development

Then make sure the data is in the database:

# select * from users;
 id |  username  |         email          |          created_at
----+------------+------------------------+-------------------------------
  1 | michael    | [email protected]    | 2016-09-08 15:08:00.31772-06
  2 | michaeltwo | [email protected] | 2016-09-08 15:08:00.320299-06
(2 rows)

Set up complete.

Integration Tests

We'll be taking a test first approach to development, roughly following these steps for each endpoint:

  1. Write test
  2. Run the test - it should fail
  3. Write code
  4. Run the test - it should pass

Start by thinking about the expected input (JSON payload) and output (JSON object) for each RESTful endpoint:

Endpoint HTTP Input Output
users GET none array of objects
users/:id GET none single object
users POST user object single object
users/:id PUT user object single object
users/:id DELETE none single object

The input user object will always look something like:

{
  "username": "michael",
  "email": "[email protected]"
}

Likewise, the output will always have the following structure:

{
  "status": "success",
  "data": "either an array of objects or a single object"
}

Create a new file in the "test/integration" directory called "routes.users.test.js" and add the following code:

process.env.NODE_ENV = 'test';

const chai = require('chai');
const should = chai.should();
const chaiHttp = require('chai-http');
chai.use(chaiHttp);

const server = require('../../src/server/app');
const knex = require('../../src/server/db/knex');

describe('routes : users', () => {

  beforeEach((done) => {
    knex.migrate.rollback()
    .then(() => {
      knex.migrate.latest()
      .then(() => {
        knex.seed.run()
        .then(() => {
          done();
        })
      });
    });
  });

  afterEach((done) => {
    knex.migrate.rollback()
    .then(() => {
      done();
    });
  });

});

What's happening here? Think about it on your own. Turn to Google if necessary. Still have questions? Comment below.

With that, let's start writing some code...

GET ALL Users

Add the first test:

describe('GET /api/v1/users', () => {
  it('should respond with all users', (done) => {
    chai.request(server)
    .get('/api/v1/users')
    .end((err, res) => {
      // there should be no errors
      should.not.exist(err);
      // there should be a 200 status code
      res.status.should.equal(200);
      // the response should be JSON
      res.type.should.equal('application/json');
      // the JSON response body should have a
      // key-value pair of {"status": "success"}
      res.body.status.should.eql('success');
      // the JSON response body should have a
      // key-value pair of {"data": [2 user objects]}
      res.body.data.length.should.eql(2);
      // the first object in the data array should
      // have the right keys
      res.body.data[0].should.include.keys(
        'id', 'username', 'email', 'created_at'
      );
      done();
    });
  });
});

Take note of the inline code comments. Run the test to make sure it fails. Now write the code to get the test pass, following these steps:

Update the route config (src/server/config/route-config.js)

(function (routeConfig) {

  'use strict';

  routeConfig.init = function (app) {

    // *** routes *** //
    const routes = require('../routes/index');
    const userRoutes = require('../routes/users');

    // *** register routes *** //
    app.use('/', routes);
    app.use('/api/v1/users', userRoutes);

  };

})(module.exports);

Now we have a new set of routes set up that we can use within src/server/routes/users.js which we need to set up...

Set up new routes

Create the users.js file in "src/server/routes/", and then add in route boilerplate:

const express = require('express');
const router = express.Router();

const knex = require('../db/knex');

module.exports = router;

Now we can add in the route handler:

router.get('/', (req, res, next) => {
  knex('users').select('*')
  .then((users) => {
    res.status(200).json({
      status: 'success',
      data: users
    });
  })
  .catch((err) => {
    res.status(500).json({
      status: 'error',
      data: err
    });
  });
});

Run the tests:

$ npm test

You should see the test passing:

routes : users
  GET /api/v1/users
    ✓ should respond with all users

GET Single User

Moving on, we can just copy and paste the previous test and use that boilerplate to write the next test:

describe('GET /api/v1/users/:id', () => {
  it('should respond with a single user', (done) => {
    chai.request(server)
    .get('/api/v1/users/1')
    .end((err, res) => {
      // there should be no errors
      should.not.exist(err);
      // there should be a 200 status code
      res.status.should.equal(200);
      // the response should be JSON
      res.type.should.equal('application/json');
      // the JSON response body should have a
      // key-value pair of {"status": "success"}
      res.body.status.should.eql('success');
      // the JSON response body should have a
      // key-value pair of {"data": 1 user object}
      res.body.data[0].should.include.keys(
        'id', 'username', 'email', 'created_at'
      );
      done();
    });
  });
});

Run the test. Watch it fail. Write the code to get it to pass:

router.get('/:id', (req, res, next) => {
  const userID = parseInt(req.params.id);
  knex('users')
  .select('*')
  .where({
    id: userID
  })
  .then((users) => {
    res.status(200).json({
      status: 'success',
      data: users
    });
  })
  .catch((err) => {
    res.status(500).json({
      status: 'error',
      data: err
    });
  });
});

POST

Test:

describe('POST /api/v1/users', () => {
  it('should respond with a success message along with a single user that was added', (done) => {
    chai.request(server)
    .post('/api/v1/users')
    .send({
      username: 'ryan',
      email: '[email protected]'
    })
    .end((err, res) => {
      // there should be no errors
      should.not.exist(err);
      // there should be a 201 status code
      // (indicating that something was "created")
      res.status.should.equal(201);
      // the response should be JSON
      res.type.should.equal('application/json');
      // the JSON response body should have a
      // key-value pair of {"status": "success"}
      res.body.status.should.eql('success');
      // the JSON response body should have a
      // key-value pair of {"data": 1 user object}
      res.body.data[0].should.include.keys(
        'id', 'username', 'email', 'created_at'
      );
      done();
    });
  });
});

Code:

// *** add a user *** //
router.post('/', (req, res, next) => {
  const newUsername = req.body.username;
  const newEmail = req.body.email;
  knex('users')
  .insert({
    username: newUsername,
    email: newEmail
  })
  .returning('*')
  .then((user) => {
    res.status(201).json({
      status: 'success',
      data: user
    });
  })
  .catch((err) => {
    res.status(500).json({
      status: 'error',
      data: err
    });
  });
});

PUT

Test:

describe('PUT /api/v1/users', () => {
  it('should respond with a success message along with a single user that was updated', (done) => {
    knex('users')
    .select('*')
    .then((user) => {
      const userObject = user[0];
      chai.request(server)
      .put(`/api/v1/users/${userObject.id}`)
      .send({
        username: 'updatedUser',
        email: '[email protected]'
      })
      .end((err, res) => {
        // there should be no errors
        should.not.exist(err);
        // there should be a 200 status code
        res.status.should.equal(200);
        // the response should be JSON
        res.type.should.equal('application/json');
        // the JSON response body should have a
        // key-value pair of {"status": "success"}
        res.body.status.should.eql('success');
        // the JSON response body should have a
        // key-value pair of {"data": 1 user object}
        res.body.data[0].should.include.keys(
          'id', 'username', 'email', 'created_at'
        );
        // ensure the user was in fact updated
        var newUserObject = res.body.data[0];
        newUserObject.username.should.not.eql(userObject.username);
        newUserObject.email.should.not.eql(userObject.email);
        // redundant
        newUserObject.username.should.eql('updatedUser');
        newUserObject.email.should.eql('[email protected]');
        done();
      });
    });
  });
});

Code:

// *** update a user *** //
router.put('/:id', (req, res, next) => {
  const userID = parseInt(req.params.id);
  const updatedUsername = req.body.username;
  const updatedEmail = req.body.email;
  knex('users')
  .update({
    username: updatedUsername,
    email: updatedEmail
  })
  .where({
    id: userID
  })
  .returning('*')
  .then((user) => {
    res.status(200).json({
      status: 'success',
      data: user
    });
  })
  .catch((err) => {
    res.status(500).json({
      status: 'error',
      data: err
    });
  });
});

DELETE

Test:

describe('DELETE /api/v1/users/:id', () => {
  it('should respond with a success message along with a single user that was deleted', (done) => {
    knex('users')
    .select('*')
    .then((users) => {
      const userObject = users[0];
      const lengthBeforeDelete = users.length;
      chai.request(server)
      .delete(`/api/v1/users/${userObject.id}`)
      .end((err, res) => {
        // there should be no errors
        should.not.exist(err);
        // there should be a 200 status code
        res.status.should.equal(200);
        // the response should be JSON
        res.type.should.equal('application/json');
        // the JSON response body should have a
        // key-value pair of {"status": "success"}
        res.body.status.should.eql('success');
        // the JSON response body should have a
        // key-value pair of {"data": 1 user object}
        res.body.data[0].should.include.keys(
          'id', 'username', 'email', 'created_at'
        );
        // ensure the user was in fact deleted
        knex('users').select('*')
        .then((updatedUsers) => {
          updatedUsers.length.should.eql(lengthBeforeDelete - 1);
          done();
        });
      });
    });
  });
});

Code:

// *** delete a user *** //
router.delete('/:id', (req, res, next) => {
  const userID = parseInt(req.params.id);
  knex('users')
  .del()
  .where({
    id: userID
  })
  .returning('*')
  .then((user) => {
    res.status(200).json({
      status: 'success',
      data: user
    });
  })
  .catch((err) => {
    res.status(500).json({
      status: 'error',
      data: err
    });
  });
});

Run all your tests. All should pass.

Keep in mind that we are not running any tests to handle errors. For examples, what happens if you pass an invalid email into the POST request? Or if you provide an invalid ID to the PUT request. Think about edge cases. Then write tests. Do this on your own.

Unit Tests

New business requirement!

We need a route to return all users created after a certain date. Since we already know how to write routes, let's add a helper function that takes an array of users and a year that then returns an array of users created on or after the specified date. We can then use this function in a future route handler.

Steps:

  1. Write a unit test
  2. Run the tests (the unit test should fail)
  3. Write the code to pass the unit test
  4. Run the tests (all should pass!)

Write a unit test

Create a new file called controllers.users.test.js within the "test/unit/" directory:

process.env.NODE_ENV = 'test';

const chai = require('chai');
const should = chai.should();

const usersController = require('../../src/server/controllers/users');

describe('controllers : users', () => {

  describe('filterByYear()', () => {
    // add code here
  });

});

Now add the body of the test:

const userArray = [
  {
    id: 1,
    username: 'michael',
    email: '[email protected]',
    created_at: '2016-09-10T16:44:28.015Z'
  },
  {
    id: 2,
    username: 'mike',
    email: '[email protected]',
    created_at: '2015-09-10T16:44:28.015Z'
  },
  {
    id: 3,
    username: 'mike',
    email: '[email protected]',
    created_at: '2014-09-10T16:44:28.015Z'
  }
];
it('should return all users created on or after (>=) specified year',
(done) => {
  usersController.filterByYear(userArray, 2015, (err, total) => {
    should.not.exist(err);
    total.length.should.eql(2);
    done();
  });
});

What's happening?

Within the it block we passed in the userArray, a year, and a callback function to a function called filterByYear. This then asserts that a error does not exist and that the length of the response (total) is 2.

Run the tests. Watch them fail.

Now, let's add the code!

Write the code to pass the unit test

Blah

Fixture

  1. install faker
  2. create a helper script
  3. write the code
  4. write a new test

npm install [email protected] --save-dev

Validation

npm install [email protected] --save

const expressValidator = require('express-validator');

app.use(expressValidator([options]));

  1. Refactoring

express-testing-mocha-knex's People

Contributors

mjhea0 avatar

Watchers

James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.