unexpected-knex
Version:
Support for testing Knex.js models and migrations using Unexpected
264 lines (225 loc) • 8.09 kB
Markdown
# unexpected-knex
[](http://badge.fury.io/js/unexpected-knex)

[](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.