NodeJS, In integration test currently I have httpClient which calls real service. Now trying to change it to mock API, but having "Cannot read property 'mockResolvedValue' of undefined"
Nutrition.test.js
const HttpClient = require('../../../../src/utils/httpClient');
const { NutritionService } = require('../../../../src/services');
const { LoggerWithContext } = require('../../../../src/utils/logger');
jest.mock('../../../../src/utils/httpClient');
describe('nutrition integration test', () => {
// skipping for now because the product nutrition is no longer found. We need to have a product that will always be available in the test.
// eslint-disable-next-line jest/no-disabled-tests
it('new basic CABL test', async () => {
const ctx = {
state: {
user: {
username: 'user',
opco: '067',
},
applicationInfo: {
correlationId: '2',
},
},
request: {
headers: {
requestId: '1',
},
},
};
const logger = new LoggerWithContext(ctx.state.user, ctx.request.header);
const httpClient = new HttpClient(ctx, logger);
const mockedResponse = {data: {userName: "test-user", address: "India"}}
httpClient.get.mockResolvedValue(mockedResponse);
const nutritionService = new NutritionService(ctx, httpClient, logger);
const nutritionInfo = await nutritionService.getNutritionInfo({
sellerId: 'CABL',
productId: '0036681',
});
expect(typeof nutritionInfo.ingredientStatement).toBe('string');
});
nutritionService.js - this is a service which is making call to real service during test in method "requestNutritionData" which looks like need to be mocked
const get = require('lodash/get');
const config = require('../../../config');
const responseConverter = require('../../utils/responseConverter');
const DataSourceError = require('../../utils/exceptions/dataSourceError');
const { getErrorInfo } = require('../../utils/error');
const acceptLanguage = {
key: 'Accept-Language',
value: 'accept-language',
};
class NutritionService {
constructor(context, httpClient, logger) {
this.userName = context.state.user.username;
this.httpClient = httpClient;
this.logger = logger;
this.SPAN_NAME = 'nutrition';
this.locale = context.request.headers[acceptLanguage.value];
this.headers = {
[acceptLanguage.key]: this.locale,
Authorization: config.credentials.nutritionKey,
};
}
async getNutritionInfo({ sellerId, productId }) {
try {
const response = await this.requestNutritionData(productId, sellerId);
if (response.data.data) {
response.data.data = responseConverter(response.data.data, this.locale);
return this.transformResponse(response.data.data);
}
return null;
} catch (error) {
this.logger.error(
`Error fetching product nutrition data for [sellerId=${sellerId}, productId=${productId}]. Error: ${error.message}`,
{ error },
);
throw new DataSourceError(
getErrorInfo({
errorMessage: `${error.message} for [sellerId=${sellerId}, productId=${productId}]`,
statusCode: get(error.response, 'status', error.code),
dataSource: 'Platform Product Service - Product Nutrition API',
}),
);
}
}
async requestNutritionData(productId, sellerId) {
// sample data
// productId = '3507035';
// sellerId = 'seller-0';
const url = `${config.services.productInfo.url}sellers/${sellerId}/products/${productId}/nutrition`;
return this.httpClient.get(url, { headers: this.headers, spanName: this.SPAN_NAME, isTokenRequired: false });
}
error - this is the error which I have when I run it
Cannot read property 'mockResolvedValue' of undefined
TypeError: Cannot read property 'mockResolvedValue' of undefined
at Object.<anonymous> (D:\Users\ksaz6883\IdeaProjects\cx-product-graph\tests\integration\v1\nutrition\nutrition.test.js:35:20)
httpClient - this client was written manually, not like used library.
const { get } = require('lodash');
const axios = require('axios');
const hoek = require('@hapi/hoek');
const getToken = require('@syscolabs/cx-token-service-js');
const tracer = require('dd-trace');
const { SYY_AUTHORIZATION } = require('./constants');
const traceGetToken = async () => tracer.trace('getToken', async () => getToken());
/**
* This module is used to send http requests. It executes http calls against api central with an api central auth token, only when required.
*
*/
class HttpClient {
constructor(ctx, logger) {
this.ctx = ctx;
this.logger = logger;
/**
* This method is used to send http requests, when the http method is either POST, PUT or PATCH
*
* @param {String} url The endpoint of the request
* @param data The request body
* @param options The options are spanName, timeout, isTokenRequired
* @param {String} spanName The name to be used for the span
* @param {number} timeout The timeout in milliseconds. Default value is 5000.
* @param {boolean} isTokenRequired The flag to identify whether api central auth token should be retrieved or not. Default value is true.
*
* @returns response Http response for the request
* @throws error This will be thrown when response could not be retrieved
*/
['post', 'put', 'patch'].forEach((method) => {
this[method] = async (url, data, options) => {
const validatedOptions = this.getValidatedOptions(options);
const validatedHeaders = this.getValidatedHeaders(options);
const configObj = {
url,
method,
data,
headers: validatedHeaders,
timeout: validatedOptions.timeout,
};
try {
const axiosConfig = await this.constructConfig(configObj, validatedOptions);
const getResponse = async () => axios[method](url, data, axiosConfig);
const response = await tracer.trace(options && options.spanName, getResponse);
this.logger.info(`Request successful | response: ${JSON.stringify(response.data)}`);
return response;
} catch (error) {
this.logger.error(`Request failed. Error: ${error.message}`, { error });
throw error;
}
};
});
/**
* This method is used to send http requests, when the http method is either GET or DELETE
*
* @param {String} url The endpoint of the request
* @param options The options are spanName, timeout, isTokenRequired
* @param {String} spanName The name to be used for the span
* @param {number} timeout The timeout in milliseconds. Default value is 5000.
* @param {boolean} isTokenRequired The flag to identify whether api central auth token should be retrieved or not. Default value is true.
*
* @returns response Http response for the request
* @throws error This will be thrown when response could not be retrieved
*/
['get', 'delete'].forEach((method) => {
this[method] = async (url, options) => {
const validatedOptions = this.getValidatedOptions(options);
const validatedHeaders = this.getValidatedHeaders(options);
const configObj = {
url,
method,
headers: validatedHeaders,
timeout: validatedOptions.timeout,
};
try {
let axiosConfig = { timeout: validatedOptions.timeout };
if (options && options.spanName) {
axiosConfig = await this.constructConfig(configObj, validatedOptions);
}
const getResponse = async () => axios[method](url, axiosConfig);
const response = await tracer.trace(options && options.spanName, getResponse);
this.logger.info(`Request successful | response: ${JSON.stringify(response.data)}`);
return response;
} catch (error) {
this.logger.error(`Request failed. Error: ${error.message}`, { error });
throw error;
}
};
});
}
/**
* This method will construct final axios configs, by modifying the header parameters.
*
* @param configObj The config object required for calling axios method
* @param options The options object
* @returns configObj The config object with modified header parameters
*/
async constructConfig(configObj, options) {
let token;
if (options.isTokenRequired) {
token = await traceGetToken();
}
const authHeader = (token && { Authorization: `Bearer ${token}` }) || {};
const defaults = {
headers: {
...authHeader,
'SYY-Correlation-Id': this.ctx.state.applicationInfo.correlationId,
'SYY-Request-Id': this.ctx.request.headers.requestId,
Accept: 'application/json',
'Content-Type': 'application/json',
},
};
if (options.isCXService) {
defaults.headers[SYY_AUTHORIZATION] = get(this.ctx, ['request', 'headers', SYY_AUTHORIZATION], null);
}
return hoek.applyToDefaults(defaults, configObj);
}
getValidatedOptions(options) {
const timeout = get(options, 'timeout', process.env.DEFAULT_TIMEOUT);
const isTokenRequired = get(options, 'isTokenRequired', true);
const isCXService = get(options, 'isCXService', false);
return {
timeout,
isTokenRequired,
isCXService,
};
}
getValidatedHeaders(options) {
return (options && options.headers) || {};
}
}
module.exports = HttpClient;
CodePudding user response:
I think you're using the whole class when you try to mock the response, intead of using the instance you created there.
it should be:
httpClient.get.mockResolvedValue(mockedResponse);
instead of
HttpClient.get.mockResolvedValue(mockedResponse);
const HttpClient = require('../../../../src/utils/httpClient');
const { NutritionService } = require('../../../../src/services');
const { LoggerWithContext } = require('../../../../src/utils/logger');
jest.mock('../../../../src/utils/httpClient');
describe('nutrition integration test', () => {
// skipping for now because the product nutrition is no longer found. We need to have a product that will always be available in the test.
// eslint-disable-next-line jest/no-disabled-tests
it('new basic CABL test', async () => {
const ctx = {
state: {
user: {
username: 'user',
opco: '067',
},
applicationInfo: {
correlationId: '2',
},
},
request: {
headers: {
requestId: '1',
},
},
};
const logger = new LoggerWithContext(ctx.state.user, ctx.request.header);
const httpClient = new HttpClient(ctx, logger);
const mockedResponse = {data: {userName: "test-user", address: "India"}}
// Shouldn't this be using the instance instead of the class?
// I modified HttpClient to httpClient
httpClient.get.mockResolvedValue(mockedResponse);
const nutritionService = new NutritionService(ctx, httpClient, logger);
const nutritionInfo = await nutritionService.getNutritionInfo({
sellerId: 'CABL',
productId: '0036681',
});
expect(typeof nutritionInfo.ingredientStatement).toBe('string');
});
UPDATE:
I just realized you're trying to call "mockResolveValue" from a common object and not an spy. What about doing?
jest.spyOn(httpClient, 'get').mockResolvedValue(mockedResponse);