How can I mock Redis when unit testing an Express node.js app?

Viewed 1773

I want to unit test my express app, parts of which set and get data from a Redis database. Investigating why some of my tests intermittently fail — and more frequently as I continue to develop my app — I realised that my tests still call the Redis server rather than the one I thought I had mocked in memory.

How can I successful mock a Redis client (ioredis), so that I can unit test portions of my code in isolation from each other and without calling the Redis server?

Here is what I have so far.

Code under test

I have an application model, which merges values from package.json with values from my Redis database, often with fallback values. Values can be added or retrieved.

// models/application.js

import fs from 'fs';
import {client} from '../config/database.js';

export const getAll = async () => {
  const data = await client.hgetall('application');
  const package_ = JSON.parse(fs.readFileSync('package.json'));

  const application = {
    name: data.name || 'IndieKit',
    version: package_.version,
    description: package_.description,
    repository: package_.repository,
    locale: data.locale || 'en',
    themeColor: data.themeColor || '#0000ee'
  };

  return application;
};

export const get = async key => {
  const application = await getAll();
  return application[key];
};

export const setAll = async values => {
  return client.hmset('application', values);
};

export const set = async (key, value) => {
  return client.hset('application', key, value);
};

client is called from a separate file, where I connect to the Redis server using ioredis:

// config/database.js

import Redis from 'ioredis';

export const client = new Redis(process.env.REDIS_URL);
export const expires = 3600;

client.on('error', error => {
  console.error('Redis', error);
});

Test

I’m using Ava as my testing framework, with rewiremock used to replace ioredis dependency with ioredis-mock… or so I thought!

// tests/models/application.js

import test from 'ava';
import {rewiremock} from '../helpers/rewiremock.js';
import {client} from '../../config/database.js';

test.beforeEach(async t => {
  t.context.applicationModel = await rewiremock.proxy(() => {
    return import('../../models/application.js');
  });
});

test.afterEach.always(() => {
  client.flushall();
});

test.serial('Gets a value', async t => {
  await t.context.applicationModel.set('name', 'foobar1');
  const result = await t.context.applicationModel.get('name');
  t.is(result, 'foobar1');
});

test.serial('Gets all values', async t => {
  await t.context.applicationModel.set('name', 'foobar2');
  const result = await t.context.applicationModel.getAll();
  t.is(result.name, 'foobar2');
});

test.serial('Sets a value', async t => {
  await t.context.applicationModel.set('name', 'foobar3');
  const result = await t.context.applicationModel.get('name');
  t.is(result, 'foobar3');
});

test.serial('Sets all values', async t => {
  await t.context.applicationModel.setAll({
    name: 'foobar4',
    locale: 'bazqux1'
  });
  const result = await t.context.applicationModel.getAll();
  t.is(result.name, 'foobar4');
  t.is(result.locale, 'bazqux1');
});

Looking at this again now, there are clearly a couple of issues. First, I was performing a flushAll command on client, which was interacting directly with the Redis server, not the mocked one in memory. Secondly, needing to use different values for each test should have alerted me to something being wrong; I wouldn’t need to this if I was truly flushing the database after each test.

Here’s the rewiremock helper I’m importing into this test:

import rewiremock from 'rewiremock/node.js';

rewiremock('ioredis')
  .by('ioredis-mock');

export {rewiremock};

So now I find myself at a loss; confused and bewildered by mocking, stubbing and dependency injection (both here and in other tests). What am I doing wrong? What’s the best way to unit test this sort of functionality?

0 Answers
Related