UNPKG

unexpected-knex

Version:

Support for testing Knex.js models and migrations using Unexpected

264 lines (225 loc) 8.09 kB
# unexpected-knex [![NPM version](https://badge.fury.io/js/unexpected-knex.svg)](http://badge.fury.io/js/unexpected-knex) ![Build Status](https://github.com/unexpectedjs/unexpected-knex/actions/workflows/ci.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/unexpectedjs/unexpected-knex/badge.svg?branch=master)](https://coveralls.io/github/unexpectedjs/unexpected-knex?branch=master) Provides support for testing [Knex.js](http://knexjs.org/) database models and migrations using [Unexpected](http://unexpected.js.org/). ## Usage Say you have a migration that creates a "user" table: ```js // ./migrations/001-create-user.js exports.up = function (knex) { return knex.schema.createTable('user', function (table) { table.increments(); table.string('first_name'); table.string('last_name'); }); }; exports.down = function (knex) { return knex.schema.dropTable('user'); }; ``` You can test that this migration will apply without error and that it actually works against your database: ```js // ./test/migrations.spec.js var knexFactory = require('knex'); var expect = require('unexpected').clone().use(require('unexpected-knex')); describe('migrations', function () { function createKnex() { // set up an in-memory SQLite database to run tests against // requires you to install the 'sqlite3' module return knexFactory({ client: 'sqlite3', connection: { filename: ':memory:', }, migrations: { directory: './migrations', }, useNullAsDefault: true, // recommended setting for sqlite3 }); } var knex = createKnex(); afterEach(function () { // drop the in-memory database and recreate it after each test to keep // the unit tests independent of each other return knex.destroy().then(function () { knex = createKnex(); }); }); describe('001-create-user.js', function () { it('creates a "user" table', function () { return expect(knex, 'to apply migration', '001-create-user.js').then( function () { return expect(knex, 'to have table', 'user').and('to have columns', { user: ['id', 'first_name', 'last_name'], }); }, ); }); }); }); ``` > Ideally you want to test migrations using a sample database that is similar to > your production database. This therefore assumes that you're running an SQLite > database in production. Alternatively, you can add the test in the migration file if you'd like to keep things in context: ```js // ./migrations/001-create-user.js // exports.up and exports.down omitted for brevity exports.test = function () { this.testUp(function (knex, expect) { return expect(knex, 'to have table', 'user').and('to have columns', { user: ['id', 'first_name', 'last_name'], }); }); }; ``` In this case, the unit test can be written as follows. The `to apply migration` assertion will run the test defined in the migration file. ```js // ./test/migrations.spec.js describe('001-create-user.js', function () { it('creates a "user" table', function () { return expect(knex, 'to apply migration', '001-create-user.js'); }); }); ``` `exports.test` is consumed by this plugin and not by Knex. You should only add it if you would like to test your migrations using this plugin. In this example, `testUp` is a hook that is run by the `to apply migration` assertion after running the `up` migration. You can also add other hooks to set up the database before running the migration or to tear it down: ```js exports.test = function () { this.beforeUp(function (knex, expect) { // called before running the up migration }); this.testUp(function (knex, expect) { // called after running the up migration }); this.beforeDown(function (knex, expect) { // called before running the down migration }); this.testDown(function (knex, expect) { // called after running the down migration }); this.after(function (knex, expect) { // called after everything else }); }; ``` All these hooks are optional. The actual order of calls is as follows: 1. `beforeUp` hook (if provided) 2. `up` migration 3. `testUp` hook (if provided) 4. `beforeDown` hook (if provided) 5. `down` migration 6. `testDown` hook (if provided) 7. `up` migration again 8. `after` hook (if provided) The up migration is ran twice in order to check that your down migration works _and_ that the up migration can still be applied after your down migration. To demonstrate these hooks, we'll assume you now have to merge the "first_name" and "last_name" columns into a "name" column: ```js // ./migrations/002-merge-user-names.js exports.up = function (knex) { return knex.schema .table('user', function (table) { table.string('name'); }) .then(function () { return knex('user') .select() .then(function (users) { return Promise.all( users.map(function (user) { return knex('user') .where('id', '=', user.id) .update('name', user.first_name + ' ' + user.last_name); }), ); }); }) .then(function () { return knex.schema.table('user', function (table) { table.dropColumn('first_name'); table.dropColumn('last_name'); }); }); }; exports.down = function (knex) { return knex.schema .table('user', function (table) { table.string('first_name'); table.string('last_name'); }) .then(function () { return knex('user') .select() .then(function (users) { return Promise.all( users.map(function (user) { var names = user.name.split(' '); return knex('user').where('id', '=', user.id).update({ first_name: names[0], last_name: names[1], }); }), ); }); }) .then(function () { return knex.schema.table('user', function (table) { table.dropColumn('name'); }); }); }; exports.test = function () { this.beforeUp(function (knex) { return knex('user').insert([ { first_name: 'John', last_name: 'Doe' }, { first_name: 'Jane', last_name: 'Doe' }, { first_name: 'John', last_name: 'Smith' }, ]); }); this.testUp(function (knex, expect) { return expect(knex, 'with table', 'user', 'to have rows satisfying', [ { name: 'John Doe', first_name: undefined, last_name: undefined }, { name: 'Jane Doe', first_name: undefined, last_name: undefined }, { name: 'John Smith', first_name: undefined, last_name: undefined }, ]); }); this.testDown(function (knex, expect) { return expect(knex, 'with table', 'user', 'to have rows satisfying', [ { first_name: 'John', last_name: 'Doe', name: undefined }, { first_name: 'Jane', last_name: 'Doe', name: undefined }, { first_name: 'John', last_name: 'Smith', name: undefined }, ]); }); this.after(function (knex) { return knex('user').delete(); }); }; ``` Then test this migration: ```js describe('002-merge-user-names.js', function () { it('merges the first_name and last_name columns into a name column', function () { return expect(knex, 'to apply migration', '002-merge-user-names.js'); }); }); ``` > There is an implicit implication here: that `to apply migration` _also_ > applies all migrations before the provided migration (when the filenames are > sorted). So in this example, even if there was a `003-create-foo.js` migration > in the migrations directory, the batch of filenames that will be handed to the > Knex migrator are `['001-create-user.js', '002-merge-user-names.js']`. This is > because new migrations are usually dependent on older migrations (TODO: it would > be nice to have a graph of which migrations are related). This is of course a minimal example, your migrations will typically be dealing with a lot more complex and sometimes unexpected user data which is a good reason to test them.