A collection of utilities to help with unit-testing Sequelize models and code that needs those models.
Related Projects
How to use
Prerequisites
This library assumes:
- You are using
chai
— Version 4 or better. - You are using
sinon
— Version 5 or better. - Using
mocha
is also recommended, but as long as you are using chai
and sinon
this should work with any test runner.
Note Jest is not supported unless you are also using sinon
and chai
, which is unlikely.
Installation
Add sequelize-test-helpers
as a devDependency
:
npm i -D sequelize-test-helpers
Examples
Unit testing models created with sequelize.define
Note: See below for how to test models created using Model.init
Let's say you have a Sequelize model User
as follows:
src/models/User.js
const model = (sequelize, DataTypes) => {
const User = sequelize.define(
'User',
{
age: {
type: DataTypes.INTEGER.UNSIGNED
},
firstName: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true
}
},
lastName: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true
}
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
lowercase: true,
validate: {
isEmail: true,
notEmpty: true
}
},
token: {
type: DataTypes.STRING,
validate: {
notEmpty: true
}
}
},
{
indexes: [
{ unique: true, fields: ['email'] },
{ unique: true, fields: ['token'] },
{ unique: false, fields: ['firstName', 'lastName'] }
]
}
)
User.associate = ({ Company }) => {
User.belongsTo(Company)
}
return User
}
module.exports = model
You can use sequelize-test-helpers
to unit-test this with mocha
as follows:
test/unit/models/User.spec.js
const { expect } = require('chai')
const {
sequelize,
dataTypes,
checkModelName,
checkUniqueIndex,
checkPropertyExists
} = require('sequelize-test-helpers')
const UserModel = require('../../src/models/User')
describe('src/models/User', () => {
const User = UserModel(sequelize, dataTypes)
const user = new User()
checkModelName(User)('User')
context('properties', () => {
;['age', 'firstName', 'lastName', 'email', 'token'].forEach(checkPropertyExists(user))
})
context('associations', () => {
const Company = 'some dummy company'
before(() => {
User.associate({ Company })
})
it('defined a belongsTo association with Company', () => {
expect(User.belongsTo).to.have.been.calledWith(Company)
})
})
context('indexes', () => {
context('unique', () => {
;['email', 'token'].forEach(checkUniqueIndex(user))
})
context('non unique (and also composite in this example)', () => {
;[['firstName', 'lastName']].forEach(checkNonUniqueIndex(user))
})
})
})
Built-in checks
Check | What it does |
---|
checkHookDefined | Checks that a particular hook is defined. |
checkModelName | Checks that the model is named correctly. |
checkNonUniqueIndex | Checks that a specific non-unique index is defined. |
checkPropertyExists | Checks that the model has defined the given property. |
checkUniqueIndex | Checks that a specific unique index is defined. |
Deprecation notice
Check | Note |
---|
checkUniqueCompoundIndex | Use either checkUniqueIndex or checkNonUniqueIndex |
Checking associations
The various association functions are stubbed so you can simply invoke the the model's associate
function in a before
block then use sinon
's standard expectation syntax to check they were called with the correct values.
hasOne
it("defined a hasOne association with Image as 'profilePic'", () => {
expect(User.hasOne).to.have.been.calledWith(Image, {
as: 'profilePic'
})
})
belongsTo
it('defined a belongsTo association with Company', () => {
expect(User.belongsTo).to.have.been.calledWith(Company)
})
hasMany
it("defined a hasMany association with User as 'employees'", () => {
expect(Company.hasMany).to.have.been.calledWith(User, {
as: 'employees'
})
})
belongsToMany
it("defined a belongsToMany association with Category through CategoriesCompanies as 'categories'", () => {
expect(Company.belongsToMany).to.have.been.calledWith(Category, {
through: CategoriesCompanies,
as: 'categories'
})
})
Unit testing code that requires models
Let's say you have a utility function that takes some data and uses it to update a user record. If the user does not exist it returns null
. (Yes I know this is a contrived example)
src/utils/save.js
const { User } = require('../models')
const save = async ({ id, ...data }) => {
const user = await User.findOne({ where: { id } })
if (user) return await user.update(data)
return null
}
module.exports = save
You want to unit-test this without invoking a database connection (so you can't require('src/models')
in your test).
This is where makeMockModels
, sinon
, and proxyquire
come in handy.
test/unit/utils/save.test.js
const { expect } = require('chai')
const { match, stub, resetHistory } = require('sinon')
const proxyquire = require('proxyquire')
const { makeMockModels } = require('sequelize-test-helpers')
describe('src/utils/save', () => {
const User = { findOne: stub() }
const mockModels = makeMockModels({ User })
const save = proxyquire('../../../src/utils/save', {
'../models': mockModels
})
const id = 1
const data = {
firstName: 'Testy',
lastName: 'McTestFace',
email: 'testy.mctestface.test.tes',
token: 'some-token'
}
const fakeUser = { id, ...data, update: stub() }
let result
context('user does not exist', () => {
before(async () => {
User.findOne.resolves(undefined)
result = await save({ id, ...data })
})
after(resetHistory)
it('called User.findOne', () => {
expect(User.findOne).to.have.been.calledWith(match({ where: { id } }))
})
it("didn't call user.update", () => {
expect(fakeUser.update).not.to.have.been.called
})
it('returned null', () => {
expect(result).to.be.null
})
})
context('user exists', () => {
before(async () => {
fakeUser.update.resolves(fakeUser)
User.findOne.resolves(fakeUser)
result = await save({ id, ...data })
})
after(resetHistory)
it('called User.findOne', () => {
expect(User.findOne).to.have.been.calledWith(match({ where: { id } }))
})
it('called user.update', () => {
expect(fakeUser.update).to.have.been.calledWith(match(data))
})
it('returned the user', () => {
expect(result).to.deep.equal(fakeUser)
})
})
})
As a convenience, makeMockModels
will automatically populate your mockModels
with mocks of all of the models defined in your src/models
folder (or if you have a .sequelizerc
file it will look for the models-path
in that). Simply override any of the specific models you need to do stuff with.
Testing models created with Model.init
Sequelize also allows you to create models by extending Sequelize.Model
and invoking its static init
function as follows:
Note: creating your models this way makes it harder to test their use.
const { Model, DataTypes } = require('sequelize')
const factory = sequelize => {
class User extends Model {}
User.init(
{
firstName: DataTypes.STRING,
lastName: DataTypes.STRING
},
{ sequelize, modelName: 'User' }
)
return User
}
module.exports = factory
You can test this using sequelize-test-helpers
, sinon
, and proxyquire
.
const { expect } = require('chai')
const { spy } = require('sinon')
const proxyquire = require('proxyquire')
const { sequelize, Sequelize } = require('sequelize-test-helpers')
describe('src/models/User', () => {
const { DataTypes } = Sequelize
const UserFactory = proxyquire('src/models/User', {
sequelize: Sequelize
})
let User
before(() => {
User = UserFactory(sequelize)
})
after(() => {
User.init.resetHistory()
})
it('called User.init with the correct parameters', () => {
expect(User.init).to.have.been.calledWith(
{
firstName: DataTypes.STRING,
lastName: DataTypes.STRING
},
{
sequelize,
modelName: 'User'
}
)
})
})
Listing your models
Assuming your src/models/index.js
(or your equivalent) exports all your models, it's useful to be able to generate a list of their names.
const { listModels } = require('sequelize-test-helpers')
console.log(listModels())
Similarly to makeMockModels
above, listModels
will find all of the models defined in your src/models
folder (or if you have a .sequelizerc
file it will look for the models-path
in that).
Custom models
paths and custom file suffixes
By default makeMockModels
and listModels
will both look for your models in files ending with .js
in either the models path defined in .sequelizerc
, or in src/models
. If however your models are not .js
files and the models
folder is somewhere else you can pass in a custom models folder path and a custom suffix.
-
listModels(customModelsFolder, customSuffix)
const modelNames = listModels('models', '.ts')
-
makeMockModels(yourCustomModels, customModelsFolder, customSuffix)
const models = makeMockModels({ User: { findOne: stub() } }, 'models', '.ts')
Development
Branches
Branch | Status | Coverage | Audit | Notes |
---|
develop | | | | Work in progress |
main | | | | Latest stable release |
Development Prerequisites
- NodeJS. I use
nvm
to manage Node versions — brew install nvm
.
Initialisation
npm install
Test it
npm test
— runs the unit testsnpm run test:unit:cov
— runs the unit tests with code coverage
Lint it
npm run lint
Contributing
Please see the contributing notes.
Thanks