Querying Mock Objects
Sequelize Mock utilizes a special QueryInterface
to be utilized for testing. This interface is provided and attached to give you full control over how results for queries may be returned and allows you to tweak the particulars of returned results as you need for your testing.
When you call into a function that would run a DB query in standard Sequelize, this function instead calls into our special test QueryInterface
object to get the results for the query. Query results can be either manually queued up based on what your code may expect next, automatically filled in from the Model
's defined default values, or dynamically generated.
Each query will return the first available result from the list.
- The value generated by a query handler
- If not available, the next result queued for the object the query is being run on
- If not available, it will return the next result queued for any parent object. For Models, this is the
Sequelize
object the Model was defined with (usingdb.define
) - If not available and being called on a
Model
, it will return an automatically generated result based on the defaults of theModel
being queried, unless configured otherwise - Any fallback function defined in the configuration for the object
If none of the above resolve to a returnable result, a EmptyQueryQueueError
excepction will be thrown.
Configuration
There are a few available configuration options that can be used to modify how query results are resolved.
Sequelize Configuration
When declaring a new SequelizeMock
object, configuration can be passed in the options
parameter.
Option | Type | Description |
---|---|---|
autoQueryFallback |
Boolean | Flag indicating if defined Model objects should fallback to the automatic query result generation functionality. Defaults to true. Can be overridden by Model configuration |
stopPropagation |
Boolean | Whether Model 's defined on this should propagate queries up to this object if they are not resolved by the Model themselves. Defaults to false. Can be overridden by Model configuration |
Model Configuration
When declaring a new Model
, configuration can be passed in through the options
parameter.
Option | Type | Description |
---|---|---|
autoQueryFallback |
Boolean | Flag indicating if Model should fallback to the automatic query result generation functionality. Defaults to true unless inherited from SequelizeMock object |
stopPropagation |
Boolean | Whether queries should attempt to propagate up to the defining SequelizeMock if they are not resolved by the Model themselves. Defaults to false unless inherited from SequelizeMock object |
Query handlers
Query results can be generated using query handlers. When multiple handlers are added to the QueryInterface, they will be called in order until one of them returns a valud result. If no handler returns a result, then QueryInterface will get the value from the list of queued results.
The handler will receive two arguments
- The name of the original query method: "findOne", "update", "destroy"...
- The list of arguments passed to the original query method.
Those arguments can be used to filter the results. For example:
User.$useHandler(function(query, queryOptions, done) {
if (query === 'findOne') {
if (queryOptions[0].where.id === 42) {
// Result found, return it
return User.build({ id: 42, name: 'foo' });
} else {
// No results
return null;
}
}
});
User.findOne({where: {id: 42}}).then(function (user) {
user.get('id'); // === 1337
user.get('name'); // === 'foo'
});
User.findOne({where: {id: 1}}).then(function (user) {
user // === null
});
The handler must return a value with the result of the query, or undefined
if the handler can't generate a value for that query. If a promise is returned, the status of that promise will be preserved by the query:
User.$useHandler(function(query, queryOptions, done) {
if (query === "findOne") {
return Promise.resolve(myValue);
} else if (query === "destroy") {
return Promise.reject(new Sequelize.Error("DB down"));
} else {
// This handler can handle this query
return;
}
});
User.findOne().then(function (user) {
user; // === myValue
});
User.destroy().catch(function (error) {
error.message; // === "DB down";
});
Multiple handlers can be added. They are called in sequence until one of them returns a value:
User.$useHandler(function(query, queryOptions, done) {
if (query === "findOne") return User.build({id: 1});
});
User.$useHandler(function(query, queryOptions, done) {
if (query === "findById") return User.build({id: queryOptions[0]});
});
User.$useHandler(function(query, queryOptions, done) {
if (query === "findOrCreate") return User.build({id:1000});
});
User.findOne().then(function (user) {
user.get('id'); // === 1
});
User.findById(123).then(function (user) {
user.get('id'); // === 123
});
User.findOrCreate().then(function (user) {
user.get('id'); // === 1000
});
If a handler wants to return undefined
as the actual result of the query, it must be wrapped in a promise:
User.$useHandler(function(query, queryOptions, done) {
return Promise.resolve(undefined)
});
User.$useHandler(function(query, queryOptions, done) {
// This handler is never called because the previous handler returned a value (a promise)
});
User.find().then(function (user) {
typeof user; // === 'undefined'
});
Queued Results
Query results can be queued against an object to be returned upon the triggering of a query based function. Results will be returned in the order they are added to the queue and can contain any values or objects.
User.$queueResult( User.build({ id: 1337 }) );
User.findOne().then(function (user) {
user.get('id'); // === 1337
});
Results are returned without modification so this functionality can be used to return multiple rows for a single query.
// You can also return multiple rows for a single query this way
User.$queueResult( [User.build(), User.build(), /* ... */] );
User.findAll().then(function (users) {
users.length; // === length of the array above
});
Some functions require additional parameters or configuration. You can specify behavior by passing in the options
parameter when queueing the results. These values will only be passed along when the function requires more than one value in the return, and will otherwise be ignored.
User.$queueResult( User.build(), { wasCreated: true } );
User.findOrCreate().spread(function (user, wasCreated) {
wasCreated; // === true
});
Testing Errors
You can also use this method to test for errors from the server. If the next queued item is a failure result, the query promise will be rejected with the given Error
.
User.$queueFailure( 'My test error' );
User.findOne().catch(function (err) {
err.message; // === 'My test error'
});
By default, if the objects passed in are not Error
objects, they are converted to Sequelize.Error
objects. You can disable this functionality if you need by passing in the option { convertNonErrors: false }
.
Recommendations
At the end of each test, it is highly recommended you clear the queue of pending query results and query handlers. This will help limit the number of false failures in your test results due to left over query results.
afterEach(function () {
User.$clearResults();
// Equivalent to calling:
// User.$clearQueue();
// User.$clearHandlers();
})
Automated Results
Automated results are generated based on a combination of the default values specified when declaring your mock Model
s, any auto-generated values like id
or createdAt
, and the query being run. For example, for a findOne()
query, it will use the where
properties to build a new object to return to you.
User.findOne({
where: {
name: 'Marty McFly'
},
}).then(function (user) {
user.get('id'); // === Auto-generated ID
user.get('name'); // === 'Marty McFly'
});