Use whenever a Node.js or TypeScript service uses BullMQ and its queues or workers need tests: producers, worker processors, retries, backoff and repeat jobs, FlowProducer parent-child jobs and QueueEvents, against real Redis (Docker, Testcontainers) or ioredis-mock.
The canonical home for this skill is testland/bullmq-tests
Per github.com/taskforcesh/bullmq:
"The fastest, most reliable, Redis-based distributed queue for Node. Carefully written for rock solid stability and atomicity."
BullMQ's testing model differs from Sidekiq / Celery: there is no
"fake mode" or in-memory queue substitute. Tests use real Redis
(Docker / Testcontainers) or stub it via ioredis-mock. Tests
typically validate: queue add → worker processor → completion event.
bullmq (add ioredis-mock for stricter unit isolation) - see Install.ioredis-mock for pure unit tests of producer logic.queue.getJobs(['waiting']); drain() between tests and close() in afterAll (see Worked example).Worker + QueueEvents for integration (see Worked example).Per bm-gh:
yarn add bullmq
# or
npm install bullmqFor tests, add ioredis-mock for in-memory Redis simulation:
npm install --save-dev ioredis-mockPer bm-gh (verbatim):
import { Queue } from 'bullmq';
const queue = new Queue('Paint');
queue.add('cars', { color: 'blue' });import { Worker } from 'bullmq';
const worker = new Worker('Paint', async job => {
if (job.name === 'cars') {
await paintCar(job.data.color);
}
});The Queue produces; the Worker consumes. Tests typically import
and invoke both within the test process.
An order flow: assert that placing an order enqueues a job, then that the processor ships it.
First, test the producer - enqueue, then read the queue by state:
import { Queue } from 'bullmq';
import { redisConfig } from './test-config'; // Docker Redis or ioredis-mock
describe('order producer', () => {
let queue: Queue;
beforeAll(() => { queue = new Queue('orders', { connection: redisConfig }); });
beforeEach(async () => { await queue.drain(); }); // clear queue between tests
afterAll(async () => { await queue.close(); });
it('enqueues an order job', async () => {
await placeOrder({ customerId: 1, items: [...] });
const jobs = await queue.getJobs(['waiting']);
expect(jobs).toHaveLength(1);
expect(jobs[0].data.customerId).toBe(1);
});
});queue.getJobs(['waiting']) retrieves jobs in a specific state.
Other states: 'active', 'completed', 'failed', 'delayed',
'paused'.
Then test the processor. For unit tests, call the processor function directly (avoid spinning up a real Worker):
const processOrder = async (job: Job<OrderData>) => {
await chargeCard(job.data);
await sendConfirmationEmail(job.data);
return { status: 'shipped' };
};
it('processes an order successfully', async () => {
const job = { data: { customerId: 1, total: 100 } } as Job<OrderData>;
const result = await processOrder(job);
expect(result.status).toBe('shipped');
});For integration tests, instantiate a real Worker and assert via QueueEvents:
import { Queue, Worker, QueueEvents } from 'bullmq';
it('processes via real worker', async () => {
const queue = new Queue('orders', { connection: redisConfig });
const worker = new Worker('orders', processOrder, { connection: redisConfig });
const events = new QueueEvents('orders', { connection: redisConfig });
await new Promise<void>((resolve) => {
events.on('completed', ({ jobId, returnvalue }) => {
expect(JSON.parse(returnvalue).status).toBe('shipped');
resolve();
});
queue.add('order', { customerId: 1 });
});
await worker.close(); await queue.close(); await events.close();
});| Anti-pattern | Why it fails | Fix |
|---|---|---|
Skip await queue.drain() between tests | Stale jobs leak across tests; flaky | Drain in beforeEach (Worked example) |
| Spin up Worker for every unit test | Slow + Redis-coupled | Test processor function directly (Worked example) |
queue.add() without await | Race: test exits before job is enqueued | Always await queue ops |
Skip worker.close() / queue.close() in afterAll | Hangs CI; Redis connections leaked | Close in afterAll (Worked example) |
ioredis-mock for QueueEvents tests | Mock has gaps in pub/sub command emulation | Use real Redis for events |
ioredis-mock covers 90% of BullMQ usage but can produce
false-passing tests on edge-case Redis commands.sidekiq-tests,
celery-tests,
sqs-tests,
rabbitmq-tests - sister toolscron-job-test-author,
idempotency-test-author -
build-an-X authorstestland/qa
aa16752
Canonical home
since Jul 23, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.