Skip to content

Instantly share code, notes, and snippets.

@cklanac
Last active November 3, 2018 08:54
Show Gist options
  • Save cklanac/3edddcdd8fffa4dc231a58f257763c8d to your computer and use it in GitHub Desktop.
Save cklanac/3edddcdd8fffa4dc231a58f257763c8d to your computer and use it in GitHub Desktop.
Challenge 10: Knex JS Testing

Noteful Challenge - Integration Testing

In this challenge you will re-implement testing on the Noteful app, this time we a real database. And you'll update the tests to cross-check the API results against the database.

Requirements

  • NPM install devDependencies for testing and update package.json file
  • Update server.js to prevent app.listen from running during tests
  • Add a test property to knexfile.js
  • Create a test database
  • Create Seed Data

Install NPM Dev-Dependencies for testing

Install the main dependencies you need for testing

  • mocha the testing framework
  • chai the assertion library
  • chai-http a chai plug-in which allows you to make http requests to the API
  • cross-env which provides a convenient way to set environment variables across platforms
npm install mocha chai chai-http cross-env --save-dev

Configure NPM script to run tests

Add "test": "mocha" to your scripts property in package.json. Notice, the cross-env command will set the NODE_ENV to test when run your test suite using the npm test command.

  "scripts": {
    "start": "node server.js",
    "test": "cross-env NODE_ENV=test mocha"
  },

Update server.js

Prevent app.listen execution and export app

Wrap the app.listen call in a require.main === module conditional to prevent it from running during tests. And add module.exports = app; to export your app to make it available to your test suite.

// Listen for incoming connections
if (require.main === module) {
  app.listen(PORT, function () {
    console.info(`Server listening on ${this.address().port}`);
  }).on('error', err => {
    console.error(err);
  });
}

module.exports = app; // Export for testing

Create a test database

Tests need to run against a separate test database. Create a noteful-test database.

createdb -U dev noteful-test

Or, if you have difficulties with the createdb then you can run CREATE DATABASE in the psql shell.

psql -U dev noteful-app
CREATE DATABASE "noteful-test";

Add a test property to knexfile.js

Update the knexfile.js to include a test property with a TEST_DATABASE_URL which defaults to your local test database.

The complete knexfile.js will look like this.

module.exports = {
  development: {
    client: 'pg',
    connection: process.env.DATABASE_URL || 'postgres://localhost/noteful-app',
    debug: true, // http://knexjs.org/#Installation-debug
    pool: { min: 1, max: 2 }
  },
  test: {
    client: 'pg',
    connection: process.env.TEST_DATABASE_URL || 'postgres://localhost/noteful-test',
    pool: { min: 1, max: 2 }
  },
  production: {
    client: 'pg',
    connection: process.env.DATABASE_URL
  }
};

Add a seed database utility

In the ./db directory, create a seedData.js file and copy in the following code.

const knex = require('../knex');
const util = require('util');
const exec = util.promisify(require('child_process').exec);

module.exports = function(file, user = 'dev') {
  return exec(`psql -U ${user} -f ${file} -d ${knex.client.connectionSettings.database}`);
};

Create /test/server.test.js file

Next, create the /test/server.test.js file. And copy this gist into the file.

The file contains a suite of tests which are similar to the solution form the previous challenge. The main difference are the Mocah life-cycle hooks.

  • before runs before all the tests in the describe block
  • beforeEach runs before each test in the describe block
  • afterEach runs after each test in the describe block
  • after runs after all the tests in the describe block
  beforeEach(function () {
    return seedData('./db/noteful.sql', 'dev');
  });

  after(function () {
    return knex.destroy(); // destroy the connection
  });

Run your tests using npm test to confirm the setup is correct.

npm test

You should see a set of successful tests. If any of the test fail, please fix them before proceeding.

Update your tests to cross-check the DB

Now you are ready to update your test suite to to cross check the API results against the database. Below are 4 examples which show common approaches. Use these are guides to implement your own integrations tests

In server.test.js file, find the test with the description should return the default of 10 Notes and update it to the following. The test will first query the database to get the count of notes, then call the API and verify the response length is the same as the database count

  it('should return the default of 10 Notes ', function () {
    let count;
    return knex.count()
      .from('notes')
      .then(([result]) => {
        count = Number(result.count);
        return chai.request(app).get('/api/notes');
      })
      .then(function (res) {
        expect(res).to.have.status(200);
        expect(res).to.be.json;
        expect(res.body).to.be.a('array');
        expect(res.body).to.have.length(count);
      });
  });

Find the test with the description should return correct search results for a valid query and update it to the following. In this test you will call to the API with a searchTerm and then perform the same query against the database. The test And compare the results. You'll chain these requests using .then().

  it('should return correct search results for a valid query', function () {
    let res;
    return chai.request(app).get('/api/notes?searchTerm=gaga')
      .then(function (_res) {
        res = _res;
        expect(res).to.have.status(200);
        expect(res).to.be.json;
        expect(res.body).to.be.a('array');
        expect(res.body).to.have.length(1);
        expect(res.body[0]).to.be.an('object');
        return knex.select().from('notes').where('title', 'like', '%gaga%');
      })
      .then(data => {
        expect(res.body[0].id).to.equal(data[0].id);
      });
  });

In the example you'll query the database and call the api then compare the results. But this time you'll use Promise.all() so the async requests can be made simultaneously, and you can compare the results with create a variable at a higher scope. Find the test with the description should return correct notes and update to with the following.

    it('should return correct notes', function () {

      const dataPromise = knex.first()
        .from('notes')
        .where('id', 1000);

      const apiPromise = chai.request(app)
        .get('/api/notes/1000');

      return Promise.all([dataPromise, apiPromise])
        .then(function ([data, res]) {
          expect(res).to.have.status(200);
          expect(res).to.be.json;
          expect(res.body).to.be.an('object');
          expect(res.body).to.include.keys('id', 'title', 'content');
          expect(res.body.id).to.equal(1000);
          expect(res.body.title).to.equal(data.title);
        });
    });

And when creating, you will .post() the new document to the endpoint, then using the id returned, query the database and verify it was saved correctly. Find the test with the correct description and update it with the following

  it('should create and return a new item when provided valid data', function () {
    const newItem = {
      'title': 'The best article about cats ever!',
      'content': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...',
      'tags': []
    };
    let body;
    return chai.request(app)
      .post('/api/notes')
      .send(newItem)
      .then(function (res) {
        body = res.body;
        expect(res).to.have.status(201);
        expect(res).to.have.header('location');
        expect(res).to.be.json;
        expect(body).to.be.a('object');
        expect(body).to.include.keys('id', 'title', 'content');
        return knex.select().from('notes').where('id', body.id);
      })
      .then(([data]) => {
        expect(body.title).to.equal(data.title);
        expect(body.content).to.equal(data.content);
      });
  });

Your Turn!

Using the patterns described above, update the rest of the tests for Notes to verify the changes have been persisted to the DB. Then create similar tests for Folders and Tags.

Configure CICD

Open or create a .travis.yml file and add the following configuration. This configuration informs Travis that you intend to use a postgresql database. And it provides a script to create the test database.

language: node_js
node_js: node
services: postgresql
before_script:
  - psql -U postgres -c 'CREATE DATABASE "noteful-test";'

Complete the CDCD configuration process. You can follow the[ CICD guides

Configure Heroku

You have 2 options to configure Heroku with Postgres: ElephantSQL and Heroku's Postgres Add-on.

Using ElephantSQL

On ElephantSQL, create a new database for your production site and copy the postgres connection URL.

In your terminal, run the following command to create your tables and insert sample data. You'll need to replace the sample connection with the URL you copied above

psql -f ./db/noteful.sql postgres://<UN>:<PW>@baasu.db.elephantsql.com:5432/<DB>

On Heroku open your application and go to the setting. Then click "Reveal Config Vars" button. In the KEY field type "DATABASE_URL" and in the VALUE field paste in the ElephantSQL connection URL. You may need to restart the dynos for the change to take effect. Look under the "More" dropdown for the restart dynos option.

Using Heroku's Postgres Add-On

On Heroku, open your app and go to the Resources tabs. Then the Add-ons section, enter "Postgres" in the search box and select "Heroku Postgres". And then provision a "Hobby Dev - Free" instance of postgres.

Once it is provisions click on "Heroku Postgres :: Database". This should open a new window/tab. Go to settings and click "View Credentials..." and copy the postgres URI.

In your terminal, run the following command to create your tables and insert sample data. You'll need to replace the sample connection with the Postgres URI you copies above

psql -f ./db/noteful.sql postgres://<UN>:<PW>@baasu.db.elephantsql.com:5432/<DB>

On Heroku open your application and go to the setting. Then click "Reveal Config Vars" button. In the KEY field type "DATABASE_URL" and in the VALUE field paste in the ElephantSQL connection URL. You may need to restart the dynos for the change to take effect. Look under the "More" dropdown for the restart dynos option.

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