stipsan / ioredis-mock

Emulates ioredis by performing all operations in-memory.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cannot read property '_transformer' of undefined

liangchunn opened this issue · comments

Description

I cannot use ioredis-mock with Jest module mocking, it keeps erroring back the below error. I've tried to pinpoint it down but couldn't find anything. Any ideas?

 FAIL  src/__tests__/Redis.ts
  ● Test suite failed to run

    TypeError: Cannot read property '_transformer' of undefined
    > 1 | const ioredis = require('ioredis-mock')        
        |                                            ^
      2 | module.exports = ioredis

      at Object.<anonymous> (node_modules/ioredis-mock/lib/index.js:148:34)
      at Object.<anonymous> (__mocks__/ioredis.ts:1:44)
      at Object.<anonymous> (src/Redis.ts:35:17)

Source files

/__mocks__/ioredis.ts

const ioredis = require('ioredis-mock')
module.exports = ioredis

/src/__tests__/Redis.ts

import { Redis } from "../Redis";

// uncommenting the line below will require the actual ioredis
// jest.unmock('ioredis')
describe('RedisAdapter', () => {
  it('can get', async () => {
    const redis = new Redis();
    expect(await redis.get('KEY')).toBe(null)
  })
})

/src/Redis.ts

import * as ioredis from 'ioredis'

export class Redis {
    private instance: ioredis.Redis | null
    constructor() {
        this.instance = null
    }
    private async getInstance() {
        if (!this.instance) {
            this.instance = new ioredis({
                host: '127.0.0.1',
                port: 6379,
                retryStrategy: (times) => false
            })
            return this.instance
        } else {
            return this.instance
        }
    }
    public async get(k: string) {
        return this.getInstance().then(instance => instance.get(k))
    }
}

Sample Repo

https://github.com/liangchunn/ioredis-transform-error

Hey!

This is because ioredis-mock is importing some code from ioredis.
Thus for jest mocking to work I recommend you put create another small module that imports ioredis so that you can mock that module by path. Allowing ioredis-mock to import code from ioredis without jest's mocking system intervening 😄

Something like:
/src/__mocks__/RedisClient.ts

const ioredis = require('ioredis-mock')
module.exports = ioredis

/src/__tests__/Redis.ts

import { Redis } from "../Redis";

describe('RedisAdapter', () => {
  it('can get', async () => {
    const redis = new Redis();
    expect(await redis.get('KEY')).toBe(null)
  })
})

/src/RedisClient.ts

import * as ioredis from 'ioredis'

export default redis

/src/Redis.ts

import ioredis from './RedisClient'

export class Redis {
    private instance: ioredis.Redis | null
    constructor() {
        this.instance = null
    }
    private async getInstance() {
        if (!this.instance) {
            this.instance = new ioredis({
                host: '127.0.0.1',
                port: 6379,
                retryStrategy: (times) => false
            })
            return this.instance
        } else {
            return this.instance
        }
    }
    public async get(k: string) {
        return this.getInstance().then(instance => instance.get(k))
    }
}

Hope this helps 😄

@stipsan Thank you so much, it worked! Updating the example repo just in case anybody stumbles across this issue.

Great to hear! 👍

For what it worth, if you don't want to change to introduce this client file, here is what you can do:

    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
        return {
          Command: { _transformer: { argument: {}, reply: {} } }
        }
      }
      // second mock for our code
      return function(...args) {
        return new Redis(args)
      }
    })

If you need access to the ioredis-mock instance:

    const mocks = { redis: null }
    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/2ba837f07c0723cde993fb8f791a5fcfdabce719/src/index.js#L100-L109
        return {
          Command: { _transformer: { argument: {}, reply: {} } }
        }
      }
      // second mock for our code
      return function(...args) {
        const instance = new Redis(args)
        mocks.redis = instance
        return instance
      }
    })

Reopening this as it would be really helpful to have this information in the readme 🤔

I'm currently struggling with this, and I just wanted to add my thoughts.
There is another way to add ioredis-mock to jest, like this:

jest.setMock('ioredis', require('ioredis-mock'));
const ioredis = require('ioredis');

This will then mock out any references to ioredis in the codebase.
This is quite a nice way of doing this, however this is problematic because a Redis instance is not created in the test, but in the codebase, and I can't see how to add test data to the mock redis instance.
It would be nice if it was this simple to mock ioredis, and there was an easy way to add data at this point so that when the Redis instance is instantiated the data is then applied. I have no idea how to achieve this.

This seemed to work:

in __mocks__/ioredis.js

const ioredisMock = require('ioredis-mock')

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
    return {
      Command: { _transformer: { argument: {}, reply: {} } },
    }
  }
  // second mock for our code
  return Redis
})

module.exports = ioredisMock

None of the above worked for us, the following did:

// On the jest setup file: __test__/setup/index.ts

const ioredisMock = require('ioredis-mock')
jest.setMock('ioredis', ioredisMock)

@santiagofm solution worked for me.
I had to make sure that I put it in a script which is configured in my jest.config.js
setupFiles NOT setupFilesAfterEnv.

@sjclemmy Your method of mocking Redis allows you to write directly to Redis using the mock client within the test itself. For example if you're using Jest you create a test harness to preload data before each test using all the standard ioredis methods and the data would be written in-memory.

I prefer this over passing a data object to the RedisMock constructor. In my service/application I have specialized DAOs/DTOs to read/write data to Redis. Not sure what your application does but if you follow this pattern and create your own set of DAOs and DTO's then you can very easily test by using ioredis-mock to mock all those calls out to Redis.

what is the best way to clean up redis data among tests?

@sibelius I'd recommend doing a FLUSHALL between each test depending on your requirements.

Starting in v5.4 Jest module mocking has first class support:

jest.mock('ioredis', () => require('ioredis-mock/jest'));

@stipsan even with that I still get

TypeError: Cannot read properties of undefined (reading '_transformer')

      at Object.<anonymous> (../node_modules/ioredis-mock/lib/command.js:69:34)
      at Object.<anonymous> (../node_modules/ioredis-mock/lib/commands/defineCommand.js:16:39)