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.

  1. The value generated by a query handler
  2. If not available, the next result queued for the object the query is being run on
  3. 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 (using db.define)
  4. If not available and being called on a Model, it will return an automatically generated result based on the defaults of the Model being queried, unless configured otherwise
  5. 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 Models, 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'
});