Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a test mode #962

Closed
entropitor opened this issue May 30, 2018 · 35 comments
Closed

Add a test mode #962

entropitor opened this issue May 30, 2018 · 35 comments

Comments

@entropitor
Copy link

It would be nice if bull had a test mode, just like kue: https://github.com/Automattic/kue#testing

This way it's much easier to write unit tests against bull workers

@manast
Copy link
Member

manast commented May 30, 2018

That would require more or less to mock all the calls we do to redis, not a simple task, would be the same as having a memory backend instead of Redis.

@aleccool213
Copy link
Contributor

@entropitor Not really unit testing at that point, more like integration. Its a neat idea though and would be a cool project some people could work on.

@tom-james-watson
Copy link

tom-james-watson commented Jun 18, 2018

Check out https://github.com/yeahoffline/redis-mock - that gives you your "memory backend" that should be able to be dropped in for testing

@tewebsolutions
Copy link

Also recommend: https://github.com/guilleiguaran/fakeredis

@apellizzn
Copy link

Was anyone able to setup bull to use https://github.com/yeahoffline/redis-mock ?

@tom-james-watson
Copy link

tom-james-watson commented Jun 25, 2018

Not me. Also, seeing as bull uses https://github.com/luin/ioredis, technically it would actually be https://github.com/stipsan/ioredis-mock. Unfortunately neither redis-mock, fakeredis nor ioredis-mock are feature-complete. All throw errors for missing methods when used with Bull. In the end I have switched to using a real test redis instance and have written some proper integration tests that way.

@apellizzn
Copy link

Leaving this here just in case.
I made this working by using redis-mock and adding info and defineCommand to the mocked client

const redis = require('redis-mock')
const client = redis.createClient()
client.info = async () => 'version'
client.defineCommand = () => {}
const queue = new Queue('my queue', { createClient: () => client })

@yojeek
Copy link

yojeek commented Oct 22, 2018

@apellizzn tried your solution, got error _this.client.client is not a function near __this.client.client('setname', this.clientName()) in worker.js

@nbarrera
Copy link

nbarrera commented Feb 6, 2019

I 've been trying to use ioredis-mock as a testbed to bull

I 've added lua support in a fork to it

But... still I needed to add also some other commands:

  • zcard
  • client setname
  • client getname
  • client list

Anyway I realized that a command ioredis-mock had already implemented was not behaving as expected, that's the case for: BRPOPLPUSH

The command was not blocking until a new element enters in the right list.

So I 've quit my attempt to make bull work on ioredis-mock... but if someone else wants to pick up the challenge you are welcome 👍

this is the branch where I 've added the extra commands needed by bull (and also the lua support)

https://github.com/nbarrera/ioredis-mock/tree/use-as-bull-testbed

@tom-james-watson
Copy link

Definitely easier to just spin up real redis in a docker container

@erickalmeidaptc
Copy link

erickalmeidaptc commented Jun 19, 2019

@apellizzn tried your solution, got error _this.client.client is not a function near __this.client.client('setname', this.clientName()) in worker.js

      // Moking redis connection 
      const redis = require('redis-mock')
      const client = redis.createClient()
      client.info = async () => 'version'
 =>   client.client = async () => ''
      client.defineCommand = () => {}
      const options = {
        createClient: () => client
      };

@oddball
Copy link

oddball commented Jul 29, 2019

I got something (and somethings not) working earlier this year and thought I share it, with the hope that someone improves it :)

// __mock__/queues.js" with Jest
import Queue from "bull";
import path from "path";
import { jobs } from "../jobs";
import Redis from "ioredis-mock";

export const redisMockClient = new Redis();

redisMockClient.info = async () => "version";

redisMockClient.client = (clientCommandName, ...args) => {
    if (!redisMockClient.clientProps) {
        redisMockClient.clientProps = {};
    }

    switch (clientCommandName) {
        case "setname": {
            const [name] = args;
            redisMockClient.clientProps.name = name;
            return "OK";
        }
        case "getname":
            return redisMockClient.clientProps.name;
        default:
            throw new Error("This implementation of the client command does not support", clientCommandName);
    }
};

export const jobQueue = new Queue("jobQueue", { createClient: () => redisMockClient });

jobQueue.process(path.join(__dirname, "..", "processor.js"));

const jobNames = Object.keys(jobs);
jobNames.forEach((jobName) => {
    jobQueue.process(jobName, 5, path.join(__dirname, "..", "processor.js"));
});

I was brought up to believe that you should decouple your tests. Spinning up a real redis instance or mongodb or whatever feels wrong. I used mongodb-memory-server for mongodb successfully for tests. I am surprised it was not as easy with Bull and Redis

@klouvas
Copy link

klouvas commented Sep 2, 2019

Is there any progress with this?

@aleksandarn
Copy link

I got something working using ioredis-mock. I am using Jest and here is my mock set-up for my tests:

jest.mock('ioredis', () => {
  const Redis = require('ioredis-mock');
  if (typeof Redis === "object") {
    // the first mock is an ioredis shim because ioredis-mock depends on it
    // https://github.com/stipsan/ioredis-mock/blob/master/src/index.js#L101-L111
    Redis.Command = { _transformer: { argument: {}, reply: {} } };
    return {
      Command: { _transformer: { argument: {}, reply: {} } }
    }
  }
  // second mock for our code
  return function (...args) {
    var instance = new Redis(args);
    instance.options = {};
    instance.info = async () => "version";
    instance.client = (clientCommandName, ...args) => {
      if (!instance.clientProps) {
        instance.clientProps = {};
      }

      switch (clientCommandName) {
        case "setname": {
          const [name] = args;
          instance.clientProps.name = name;
          return "OK";
        }
        case "getname":
          return instance.clientProps.name;
        default:
          throw new Error("This implementation of the client command does not support", clientCommandName);
      }
    };

    return instance;
  }
})

This is based on a code from this thread and a thread about using ioredis-mock with Jest. The issue I am facing is that ioredis-mock has Lua implementation version 5.3, where Redis uses 5.1 and this breaks some scripts. For example it is not possible to add delayed task because it uses bit.band() which is not available on Lua 5.3 as it supports bit-wise operations by internally.

@bwkiefer
Copy link

bwkiefer commented Nov 19, 2019

@aleksandarn

I've tried your set up, but have run into an issue where my processor is not picking up any of the jobs that are posted. I can see when I post a job that bull adds keys to the redis mock. Have you got simple job posting and processing working with the mock? Here's a simplified version of my test:

const wait = async function wait(time) {
  return new Promise(resolve => setTimeout(resolve, time));
};

jest.mock('ioredis', () => {
  // ioredis-mock v4.18.2
  const Redis = require('ioredis-mock');
  if (typeof Redis === "object") {
    // the first mock is an ioredis shim because ioredis-mock depends on it
    // https://github.com/stipsan/ioredis-mock/blob/master/src/index.js#L101-L111
    Redis.Command = { _transformer: { argument: {}, reply: {} } };
    return {
      Command: { _transformer: { argument: {}, reply: {} } }
    }
  }
  // second mock for our code
  return function (...args) {
    var instance = new Redis(args);
    instance.options = {};
    instance.info = async () => "version";
    instance.client = (clientCommandName, ...args) => {
      if (!instance.clientProps) {
        instance.clientProps = {};
      }

      switch (clientCommandName) {
        case "setname": {
          const [name] = args;
          instance.clientProps.name = name;
          return "OK";
        }
        case "getname":
          return instance.clientProps.name;
        default:
          throw new Error("This implementation of the client command does not support", clientCommandName);
      }
    };

    return instance;
  }
});

// bull v3.12.0
const Queue = require('bull');
// ioredis v4.14.0
const Redis = require('ioredis');

describe('test', () => {
  it('test', async () => {
    expect.assertions(2);

    let redis = new Redis();
    const queue = new Queue('test', {
      createClient: () => {
        return redis;
      }
    });

    const theJob = { 'status': true };

    queue.process(async (job, done) => {
      // does not get this far
      console.log('processing: ', job.data);
      expect(job.data).toMatchObject(theJob);

      done(null, job.data);
    });

    await queue.add(theJob);

    let keyRes = await redis.keys('*');
    // Prints [ 'bull:test:id', 'bull:test:1.0', 'bull:test:wait' ]
    console.log(keyRes);

    await wait(100);

    expect(1).toBe(1);
  });
});

@omairvaiyani
Copy link

For anyone that stumbles upon this issue - neither Bull 3, nor the upcoming Bull 4 currently natively support a test/in-memory environment for local development, and no mock clients (ioredis-mock, redis-mock) work with Bull. I've managed to find a working solution using redis-server

const startLocalRedisServer = async () => {
  const MOCK_SERVER_PORT = 6379;
  try {
    const RedisServer = require('redis-server');
    const server = new RedisServer();
    await server.open({ port: MOCK_SERVER_PORT });
  } catch (e) {
    console.error('unable to start local redis-server', e);
    process.exit(1);
  }
  return MOCK_SERVER_PORT;
};
// Bull setup 
const setupQueue = async () => {
  const port =  await  startLocalRedisServer();
  return new Queue('General', { redis: { port } });
}

@aleksandarn
Copy link

Probably this issue in ioredis-mock (apart from the Lua version) has something to do with the fact that bull is not working properly with it: stipsan/ioredis-mock#773

@prithvin
Copy link

prithvin commented Feb 12, 2020

This approach works perfectly for me:

In my src/__mocks__/ioredis.js, i included

/* @flow */
const Redis = require('ioredis-mock');

class RedisMock {
  static Command = { _transformer: { argument: {}, reply: {} } };
  static _transformer = { argument: {}, reply: {} };

  constructor(...args: Object) {
    Object.assign(RedisMock, Redis);
    const instance = new Redis(args);
    instance.options = {};
    // semver in redis client connection requires minimum version 5.0.0
    // https://github.com/taskforcesh/bullmq/blob/da8cdb42827c22fea12b9c2f4c0c80fbad786b98/src/classes/redis-connection.ts#L9
    instance.info = async () => 'redis_version:5.0.0';
    instance.client = (clientCommandName, ...addArgs) => {
      if (!instance.clientProps) {
        instance.clientProps = {};
      }

      switch (clientCommandName) {
        case 'setname': {
          const [name] = addArgs;
          instance.clientProps.name = name;
          return 'OK';
        }
        case 'getname':
          return instance.clientProps.name;
        default:
          throw new Error(`This implementation of the client command does not support ${clientCommandName}`);
      }
    };
    return instance;
  }
}

module.exports = RedisMock;

and in my bullmq instance setup file, i included

/* @flow */

const CONSTS = require('../config/config');
const { Queue } = require('bullmq');
const IORedis = require('ioredis');
const logger = require('../utils/logger');

const connection = new IORedis(CONSTS.BULL_MQ_REDIS); // obj with host, port
connection.on('connect', () => {
  logger.info('Connected to redis');
});
connection.on('error', (err) => {
  logger.info('Error connceting to bullmq', err);
});

const bullMQQueue = new Queue(
  'bullmqqueue',
  {
    connection,
  },
);

module.exports = bullMQQueue;

@phrozen
Copy link

phrozen commented Feb 21, 2020

This approach works perfectly for me:

I tried this and Bull still tries to connect to the default Redis instance in my tests. I'm using proxyquire and mocking the redis connection, but somehow BullMQ still tries to create a connection of it's own with IORedis.

Edit: OK, I made it work by adding the next lines:

instance.connect = () => {};
instance.disconnect = () => {};
instance.duplicate = () => instance;

Apparently this is what BullMQ 4.0 checks to see if it can reuse the connection:

https://github.com/taskforcesh/bullmq/blob/36726bfb01430af8ec606f36423fc187e4a06fb4/src/utils.ts#L41

@prodoxx
Copy link

prodoxx commented Feb 25, 2020

I don't know if it's useful but Shoryuken which is used for SQS queues in Ruby has something called an inline adapter used to run jobs inline. So, instead of the task getting added to a queue, it just runs the perform function. Maybe this is a good idea to do with bull? Here is a reference: https://github.com/phstc/shoryuken/wiki/Shoryuken-Inline-adapter

@jcorkhill
Copy link

jcorkhill commented Mar 1, 2020

@tom-james-watson Hello. You mentioned that you use a real Redis instance for your integration tests. I'm looking to do the same, and I was hoping I could ask you a question about your implementation.

Suppose you just have a simple POST endpoint that enqueues a job with Bull. Suppose also that you use something like Supertest to make a POST Request to that endpoint. When the POST Request returns 201 or 202, you can't immediately start asserting that side-effects of the processed jobs have occurred correctly because there could be a small delay between the POST response and the actual execution of the job. I was wondering how you handle that with your setup.

Thanks.

@tom-james-watson
Copy link

The short answer is to listen to the queue's completed (or whatever else) event in your test. E.g. queue.on('completed', (job, result) => {...

@jcorkhill
Copy link

jcorkhill commented Mar 1, 2020

@tom-james-watson Thanks very much, so I take it, with Jest for example, you'd use something like expect.assertions(x) or even the done callback to keep the test running until the job is executed?

In the event that you have deffered jobs (say 15 minutes in the future), would you just try to get away with asserting that the job was added with the correct delay and not worry about its execution?

Thanks again.

@tom-james-watson
Copy link

Let's not derail this thread - every time you comment you're sending notifications to everyone who has participated in this thread. But yes, most of my tests simply check check that jobs are added with the expected parameters. For these I mock the add function so that it doesn't actually try and execute the job. Then I have separate tests specifically for the internals of the jobs where nothing is mocked and I let everything run for real on redis and wait for the job to complete.

@jcorkhill
Copy link

Thanks. I agree we're sightly off topic from the original objective of the thread, but I figured the information was somewhat relevant for those performing integration tests with Bull.

Anyway, I'll leave it at that and thanks for your time.

@jaschaio
Copy link

jaschaio commented Jun 6, 2020

Trying to use @prithvin suggested solition using ioredis-mock but with bull:v3. If I add a job with options like queue.add( 'name', {}, { delay: 900000 } ) I get a weird error:

Error trying to loading or executing lua code string in VM: [string "--[[..."]:60: attempt to index a nil value (global 'bit')
    at Object.luaExecString (/node_modules/ioredis-mock/lib/lua.js:29:13)
    at RedisMock.customCommand2 (/node_modules/ioredis-mock/lib/commands/defineCommand.js:124:8)
    at safelyExecuteCommand (/node_modules/ioredis-mock/lib/command.js:114:32)
    at /node_modules/ioredis-mock/lib/command.js:139:43
    at new Promise (<anonymous>)
    at RedisMock.addJob (/node_modules/ioredis-mock/lib/command.js:138:45)
    at Object.addJob (/node_modules/bull/lib/scripts.js:41:19)
    at addJob (/node_modules/bull/lib/job.js:66:18)
    at /node_modules/bull/lib/job.js:79:14

@stigvanbrabant
Copy link

Someone that got this to work with bull v3? For some reason when i try @prithvin solution my test suite gets stuck (without any errors getting thrown).

@jaschaio
Copy link

@stigvanbrabant I didn’t and ended up switching to bullmq and just starting a Docker container with redis before running the test suite.

@peterp
Copy link

peterp commented Nov 9, 2020

I'm not doing anything sophisticated with bull, so I created this mock that executes the job the moment a job is added, maybe someone can expand on this.

// __mocks__/bull.ts
export default class Queue {
  name: string
  constructor(name) {
    this.name = name
  }

  process = (fn) => {
    console.log(`Registered function ${this.name}`)
    this.processFn = fn
  }

  add = (data) => {
    console.log(`Running ${this.name}`)
    return this.processFn({ data })
  }
}

@thisismydesign
Copy link

My use case is not to execute jobs scheduled during testing. I was hoping to solve this by instantiating Bull using different default job options, such as attempts: 0. But that doesn't seem to work. I'm currently using an arbitrary high delay.

How about supporting attempts: 0 on JobOpts? Or whatever else flag that simply skips processing.

I'm using NestJS and also posted a question on SO: https://stackoverflow.com/questions/67489690/nestjs-bull-queues-how-to-skip-processing-in-test-env/67489691

@boredland
Copy link

boredland commented Dec 7, 2021

Edit: OK, I made it work by adding the next lines:

instance.connect = () => {};
instance.disconnect = () => {};
instance.duplicate = () => instance;

Apparently this is what BullMQ 4.0 checks to see if it can reuse the connection:

https://github.com/taskforcesh/bullmq/blob/36726bfb01430af8ec606f36423fc187e4a06fb4/src/utils.ts#L41

DISCLAIMER: this applies to bullmq (which you should use), not bulljs (v3).

So we would just need to mock that one, I think. This currently works for me just fine:

import * as bullUtils from 'bullmq/dist/utils';

jest.mock('ioredis', () => require('ioredis-mock/jest'));
const bullMock = jest.spyOn(bullUtils, 'isRedisInstance');

bullMock.mockImplementation((_obj: unknown) => {
  return true;
});

If ioredis-mock is missing ioredis-features that we need to test bullmq properly, we should raise that upstream with them, thats imo nothing bullmq should solve.

@jcarlsonautomatiq
Copy link

prithvin solution almost works though there were some extra pain points if you try to pause the queues in a unit test. This is for Bull 4.0 which does some extra disconnect / connect things.

import Redis from 'ioredis-mock';

export default class RedisMock {
      static Command = { _transformer: { argument: {}, reply: {} } };
      static _transformer = { argument: {}, reply: {} };

      constructor(...args) {
        Object.assign(RedisMock, Redis);

        // Sketchy but bull is using elements that are NOT in ioredis-mock but ARE on Redis itself.
        let instance: any = new Redis(args);

        instance.options = {};
        // semver in redis client connection requires minimum version 5.0.0
        // https://github.com/taskforcesh/bullmq/blob/da8cdb42827c22fea12b9c2f4c0c80fbad786b98/src/classes/redis-connection.ts#L9
        instance.info = async () => 'redis_version:5.0.0';
        instance.client = (clientCommandName, ...addArgs) => {
          if (!instance.clientProps) {
            instance.clientProps = {};
          }
          switch (clientCommandName) {
            case 'setname': {
              const [name] = addArgs;
              instance.clientProps.name = name;
              return 'OK';
            }
            case 'getname':
              return instance.clientProps.name;
            default:
              throw new Error(`This implementation of the client command does not support ${clientCommandName}`);
          }
        }

        // ioredis-mock does not emit an 'end' when it calls quit, this causes issues for bull
        // which will not resolve promises that set the queue to paused.
        let reallyDie = instance.quit.bind(instance);
        async function pleaseDie() {
          setTimeout(() => {
            instance.emit('end');
          }, 100);
          try {
            let quit = await reallyDie();
            return quit;
          } catch(error) {
            console.error("Failed to actually quit", error);
          }
        }
        instance.quit = pleaseDie;

        let reallyDC = instance.disconnect.bind(instance);
        instance.disconnect = function() {
          // ioredis-mock does not set connected to false on a disconnect which also freaks bull out.
          instance.connected = false;
          instance.status = "disconnected";  // You do not want reconnecting etc
          return reallyDC();
        };
        return instance;
      }
    }

Then before your jest tests

jest.mock('ioredis', () => {
  return RedisMock;
});

@null-prophet
Copy link

prithvin solution almost works though there were some extra pain points if you try to pause the queues in a unit test. This is for Bull 4.0 which does some extra disconnect / connect things.

import Redis from 'ioredis-mock';

export default class RedisMock {
      static Command = { _transformer: { argument: {}, reply: {} } };
      static _transformer = { argument: {}, reply: {} };

      constructor(...args) {
        Object.assign(RedisMock, Redis);

        // Sketchy but bull is using elements that are NOT in ioredis-mock but ARE on Redis itself.
        let instance: any = new Redis(args);

        instance.options = {};
        // semver in redis client connection requires minimum version 5.0.0
        // https://github.com/taskforcesh/bullmq/blob/da8cdb42827c22fea12b9c2f4c0c80fbad786b98/src/classes/redis-connection.ts#L9
        instance.info = async () => 'redis_version:5.0.0';
        instance.client = (clientCommandName, ...addArgs) => {
          if (!instance.clientProps) {
            instance.clientProps = {};
          }
          switch (clientCommandName) {
            case 'setname': {
              const [name] = addArgs;
              instance.clientProps.name = name;
              return 'OK';
            }
            case 'getname':
              return instance.clientProps.name;
            default:
              throw new Error(`This implementation of the client command does not support ${clientCommandName}`);
          }
        }

        // ioredis-mock does not emit an 'end' when it calls quit, this causes issues for bull
        // which will not resolve promises that set the queue to paused.
        let reallyDie = instance.quit.bind(instance);
        async function pleaseDie() {
          setTimeout(() => {
            instance.emit('end');
          }, 100);
          try {
            let quit = await reallyDie();
            return quit;
          } catch(error) {
            console.error("Failed to actually quit", error);
          }
        }
        instance.quit = pleaseDie;

        let reallyDC = instance.disconnect.bind(instance);
        instance.disconnect = function() {
          // ioredis-mock does not set connected to false on a disconnect which also freaks bull out.
          instance.connected = false;
          instance.status = "disconnected";  // You do not want reconnecting etc
          return reallyDC();
        };
        return instance;
      }
    }

Then before your jest tests

jest.mock('ioredis', () => {
  return RedisMock;
});

Thankyou so much for this, do you have any examples how your testing your workers queues?

Just all new to me and could use some pointers on meaningful unit tests.

@basememara
Copy link

I ended up using https://github.com/mhassan1/redis-memory-server and worked great

@manast
Copy link
Member

manast commented Nov 14, 2024

Closing this as no more actions from our side and it seems there are solutions available.

@manast manast closed this as completed Nov 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests