A collection of utilities to help with unit-testing Sequelize models and code that needs those models, using Jest. This is inspired by my work originally proposed in sequelize-test-helpers#302.
sequelize-test-helpers— Mocha/Chai utilities for testing Sequelize models;sequelize-jest-kitstarted as a fork of this project and targets Jest instead.sequelize-pg-utilities— Simple utilities that help you manage your Sequelize configuration.
- Jest — this package wires mocks with
jest-mock(jest.fn()), and the check helpers expect Jest’s globalexpect. - Optional:
proxyquire(or another stub loader) when you need to replacerequire('../models')in unit tests.
If you use Mocha-style context blocks, add a one-line setup file and register it with Jest’s setupFilesAfterEnv:
global.context = global.describe(This repo does the same in test/unitTestHelper.js.)
Add sequelize-jest-kit as a devDependency:
npm i -D sequelize-jest-kitNote: See below for how to test models created using Model.init
Let's say you have a Sequelize model User as follows:
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 = modelYou can use sequelize-jest-kit to unit-test this with Jest as follows:
const {
sequelize,
dataTypes,
checkModelName,
checkUniqueIndex,
checkNonUniqueIndex,
checkPropertyExists
} = require('sequelize-jest-kit')
const UserModel = require('../../../src/models/User')
describe('src/models/User', () => {
const User = UserModel(sequelize, dataTypes)
const user = new User()
checkModelName(User)('User')
describe('properties', () => {
;['age', 'firstName', 'lastName', 'email', 'token'].forEach(checkPropertyExists(user))
})
describe('associations', () => {
const Company = 'some dummy company'
beforeAll(() => {
User.associate({ Company })
})
it('defined a belongsTo association with Company', () => {
expect(User.belongsTo).toHaveBeenCalledWith(Company)
})
})
describe('indexes', () => {
describe('unique', () => {
;['email', 'token'].forEach(checkUniqueIndex(user))
})
describe('non unique (and also composite in this example)', () => {
;[['firstName', 'lastName']].forEach(checkNonUniqueIndex(user))
})
})
})Each check* helper registers a Jest it that runs an assertion. For use inside your own it or for negative tests, use the matching assert* export (for example assertModelName).
| 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. |
| Check | Note |
|---|---|
checkUniqueCompoundIndex |
Use either checkUniqueIndex or checkNonUniqueIndex |
The various association functions are Jest mocks (jest.fn()), so you can invoke the model’s associate function in a beforeAll / beforeEach block and use Jest’s matcher API to assert they were called with the correct values.
it("defined a hasOne association with Image as 'profilePic'", () => {
expect(User.hasOne).toHaveBeenCalledWith(Image, {
as: 'profilePic'
})
})it('defined a belongsTo association with Company', () => {
expect(User.belongsTo).toHaveBeenCalledWith(Company)
})it("defined a hasMany association with User as 'employees'", () => {
expect(Company.hasMany).toHaveBeenCalledWith(User, {
as: 'employees'
})
})it("defined a belongsToMany association with Category through CategoriesCompanies as 'categories'", () => {
expect(Company.belongsToMany).toHaveBeenCalledWith(Category, {
through: CategoriesCompanies,
as: 'categories'
})
})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)
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 = saveYou want to unit-test this without invoking a database connection (so you can't require('src/models') in your test).
This is where makeMockModels and proxyquire come in handy (use jest.fn() for any methods you need to stub or spy on).
const proxyquire = require('proxyquire')
const { makeMockModels } = require('sequelize-jest-kit')
describe('src/utils/save', () => {
const User = { findOne: jest.fn() }
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: jest.fn() }
let result
describe('user does not exist', () => {
beforeEach(async () => {
User.findOne.mockResolvedValue(undefined)
result = await save({ id, ...data })
})
it('called User.findOne', () => {
expect(User.findOne).toHaveBeenCalledWith(expect.objectContaining({ where: { id } }))
})
it("didn't call user.update", () => {
expect(fakeUser.update).not.toHaveBeenCalled()
})
it('returned null', () => {
expect(result).toBeNull()
})
})
describe('user exists', () => {
beforeEach(async () => {
fakeUser.update.mockResolvedValue(fakeUser)
User.findOne.mockResolvedValue(fakeUser)
result = await save({ id, ...data })
})
it('called User.findOne', () => {
expect(User.findOne).toHaveBeenCalledWith(expect.objectContaining({ where: { id } }))
})
it('called user.update', () => {
expect(fakeUser.update).toHaveBeenCalledWith(expect.objectContaining(data))
})
it('returned the user', () => {
expect(result).toEqual(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.
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 = factoryYou can test this using sequelize-jest-kit and proxyquire.
const proxyquire = require('proxyquire')
const { sequelize, Sequelize } = require('sequelize-jest-kit')
describe('src/models/User', () => {
const { DataTypes } = Sequelize
const UserFactory = proxyquire('../../../src/models/User', {
sequelize: Sequelize
})
let User
beforeAll(() => {
User = UserFactory(sequelize)
})
afterEach(() => {
User.init.mockClear()
})
it('called User.init with the correct parameters', () => {
expect(User.init).toHaveBeenCalledWith(
{
firstName: DataTypes.STRING,
lastName: DataTypes.STRING
},
{
sequelize,
modelName: 'User'
}
)
})
})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-jest-kit')
console.log(listModels()) // will spit out a list of your model names.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).
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: jest.fn() } }, 'models', '.ts')
| Branch | Status | Coverage | Audit | Notes |
|---|---|---|---|---|
develop |
Work in progress | |||
main |
Latest stable release |
- Node.js that satisfies the
enginesfield inpackage.json.
npm installnpm test— run the unit testsnpm run test:unit:cov— run the unit tests with code coveragenpm run lint— run the linters
Source repository: github.com/howard-e/sequelize-jest-kit.
Please see the contributing notes.
