Search code examples
javascriptunit-testingjestjsnock

Expectations Error while unit testing Got Client


I need your help with this issue that I am having while unit testing a Got client hook where I do logging of HTTP requests. I am using Jest. I am getting an expectations error that is seeing the argument to .toBeCalledWith as object whereas it is a string when I console log it. Maybe I am doing something wrong here. Please let me know.

got-client.js below

const http = require('http');
const https = require('https');
const got = require('got');
const _ = require('lodash');

const { name: packageName, version: packageVersion } = require('../../package.json');

const keepAliveOptions = { keepAlive: true, keepAliveMsecs: 20000 };
let clients = {};


const allowedHeaders = ['user-agent', 'x-forwarded-for', 'referer', 'content-length'];
const filterHeaders = headers => _.pick(headers, allowedHeaders);

const gotLoggingHooks = (name, logger) => ({
  hooks: {
    beforeRequest: [
      options => {
        const { url, method, headers } = options;
        logger.debug({
          message: `${name} request ${options.method} ${options.url}`,
          http_request: {
            method,
            target: url,
            direction: 'OUT',
            headers: filterHeaders(headers)
          },
          request: _.pick(options, ['url', 'method', 'headers', 'body', 'json'])
        });
      }
    ],
    beforeRetry: [
      (options, error, retryCount) => {
        const {
          response: { statusCode, ip } = {},
          request: { options: { method, headers = {} } = {}, requestUrl: url } = {},
          timings: {
            // eslint-disable-next-line camelcase
            phases: { total: duration_ms } = {}
          } = {}
        } = error;
        logger.warn({
          message: `${name} will retry request, attempt ${retryCount}/${options.retry.limit} ${method} ${url} (${error.code} ${error.message})`,
          err: error,
          http_request: {
            method,
            target: url,
            status: statusCode,
            server_ip: ip,
            duration_ms,
            direction: 'OUT',
            protocol: headers.via,
            headers: filterHeaders(headers)
          }
        });
      }
    ],
    beforeError: [
      error => {
        const {
          response: { statusCode, ip } = {},
          request: { options: { method, headers } = {}, requestUrl: url } = {},
          timings: {
            // eslint-disable-next-line camelcase
            phases: { total: duration_ms } = {}
          } = {}
        } = error;
        if (!statusCode) {
          logger.error({
            message: `${name} request error ${method} ${url} (${error.code} ${error.message})`,
            err: error,
            http_request: {
              method,
              target: url,
              status: statusCode,
              server_ip: ip,
              duration_ms,
              direction: 'OUT',
              protocol: headers.via,
              headers: filterHeaders(headers)
            }
          });
        }
        // eslint-disable-next-line no-param-reassign
        error.serviceName = name;
        return error;
      }
    ],
    afterResponse: [
      response => {
        const {
          statusCode,
          body,
          url,
          ip,
          headers = {},
          request: { options: { method } = {} } = {},
          timings: {
            // eslint-disable-next-line camelcase
            phases: { total: duration_ms } = {}
          } = {},
          retryCount
        } = response;
        logger.debug({
          message: `${name} response ${method} ${url}`,
          response: { body, retryCount, headers },
          http_request: {
            method,
            target: url,
            status: statusCode,
            server_ip: ip,
            duration_ms,
            direction: 'OUT',
            protocol: headers.via,
            headers: filterHeaders(_.get(response, 'request.options.headers'))
          }
        });
        return response;
      }
    ]
  }
});

const gotClient = ({ name, logger, keepAlive = true, gotOptions = {} }) => {
  if (!clients[name]) {
    clients[name] = got
      .extend({
        headers: {
          'user-agent': `${packageName} ${packageVersion}`
        },
        ...(keepAlive && {
          agent: {
            http: new http.Agent(keepAliveOptions),
            https: new https.Agent(keepAliveOptions)
          }
        }),
        responseType: 'json',
        timeout: 5000
      })
      .extend(gotLoggingHooks(name, logger))
      .extend(gotOptions);
  }

  return clients[name];
};

gotClient.clearAll = () => {
  clients = {};
};

module.exports = gotClient;

got-client.spec.js below

const nock = require('nock');
const { name: packageName, version: packageVersion } = require('../../../package.json');
const gotClient = require('../../../src/lib/got-client');

const BASE_URL = 'https://subdomain.domain.com/';
const BASE_ENDPOINT = 'path';

const logger = {
  error: jest.fn(),
  debug: jest.fn(),
  info: jest.fn(),
  log: jest.fn(),
  warn: jest.fn(),
};
describe('got client', () => {

  afterEach(gotClient.clearAll);

  test('should log requests', async () => {
    const client = gotClient({
      name: 'test',
      logger,
      gotOptions: {
        prefixUrl: BASE_URL,
      },
    });

    nock(BASE_URL).get(`/${BASE_ENDPOINT}`).reply(200, { success: true });
    await client.get(BASE_ENDPOINT);
    // console.log('mock call 0', logger.debug.mock.calls[0][0]);
    // TODO: match message
    expect(logger.debug).toBeCalled();

    expect(logger.debug).toBeCalledWith(
      expect.objectContaining({
        message: expect.stringContaining(`request GET ${BASE_URL}${BASE_ENDPOINT}`),
      })
    );

    expect(logger.debug).toBeCalledWith(
      expect.objectContaining({
        message: expect.stringContaining(`response GET ${BASE_URL}${BASE_ENDPOINT}`),
      })
    );

    nock(BASE_URL).get(`/${BASE_ENDPOINT}/error`).reply(500, { success: false });
    try {
      await client.get(`${BASE_ENDPOINT}/error`, { retry: 0 });
    } catch (e) {}
    expect(logger.error).toBeCalledWith(
      expect.objectContaining({
        message: expect.stringContaining(`request error GET ${BASE_URL}${BASE_ENDPOINT}/error`),
      })
    );
  });
});

Failing Test Error below

Error: expect(jest.fn()).toBeCalledWith(...expected)

Expected: ObjectContaining {"message": StringContaining "request error GET https://subdomain.domain.com/path/error"}

Number of calls: 0

    at Object.<anonymous> (/Users/user/Documents/company/teams/team/project/test/unit/lib/got-clients.spec.js:62:26)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)

I will really appreciate help with this. Thank you very much in advance.


Solution

  • Working got-client.spec.js

    const nock = require('nock');
    const { name: packageName, version: packageVersion } = require('../../../package.json');
    const gotClient = require('../../../src/lib/got-client');
    
    const BASE_URL = 'https://subdomain.domain.com/';
    const BASE_ENDPOINT = 'path';
    
    const logger = {
      error: jest.fn(),
      debug: jest.fn(),
      info: jest.fn(),
      log: jest.fn(),
      warn: jest.fn(),
    };
    
    const defaultClient = gotClient({
      name: 'test',
      logger,
      gotOptions: {
        prefixUrl: BASE_URL,
      },
    });
    
    describe('got client', () => {
        afterEach(gotClient.clearAll);
        
        test('should log requests', async () => {
            nock(BASE_URL).get(`/${BASE_ENDPOINT}`).reply(200, { success: true });
            await defaultClient.get(BASE_ENDPOINT);
        
            expect(logger.debug).toBeCalledWith(
              expect.objectContaining({
                message: expect.stringContaining(`request GET ${BASE_URL}${BASE_ENDPOINT}`),
              })
            );
          });
        
          test('should log responses', async () => {
            nock(BASE_URL).get(`/${BASE_ENDPOINT}`).reply(200, { success: true });
            await defaultClient.get(BASE_ENDPOINT);
        
            expect(logger.debug).toBeCalledWith(
              expect.objectContaining({
                message: expect.stringContaining(`response GET ${BASE_URL}${BASE_ENDPOINT}`),
              })
            );
          });
        
          test('should log errors', async () => {
            const endpoint = `${BASE_ENDPOINT}/error`;
            nock(BASE_URL).get(`/${endpoint}`).replyWithError({
              message: 'something awful happened',
              code: 'ECONNRESET',
            });
            try {
              await defaultClient.get(endpoint, { retry: 0 });
            } catch (e) {}
            expect(logger.error).toBeCalledWith(
              expect.objectContaining({
                message: expect.stringContaining(`request error GET ${BASE_URL}${endpoint}`),
              })
            );
          });
        
          test('should log retries', async () => {
            nock(BASE_URL)
              .get(`/${BASE_ENDPOINT}`)
              .replyWithError({
                message: 'something awful happened',
                code: 'ECONNRESET',
              })
              .get(`/${BASE_ENDPOINT}`)
              .reply(500, { success: false })
              .get(`/${BASE_ENDPOINT}`)
              .reply(500, { success: false })
              .get(`/${BASE_ENDPOINT}`)
              .reply(200, { success: true });
        
            await defaultClient.get(BASE_ENDPOINT, { retry: { limit: 3, calculateDelay: () => 1 } });
        
            expect(logger.warn).toBeCalledTimes(3);
        
            expect(logger.warn).toBeCalledWith(
              expect.objectContaining({
                message: expect.stringContaining(`will retry request`),
              })
            );
          });
      });