I have code in which I make a call to elasticsearch indices stats. Tried mocking it following way but did not work.
esIndicesStatsStub = sinon.stub(Client.indices.prototype, 'stats').returns({
promise: () => new Error()
})
The error I get is
TypeError: Cannot read property 'prototype' of undefined
I've source code like this
const { Client } = require('#elastic/elasticsearch')
const esClient = new Client({
node:'some https url'
})
esClient.indices.stats(params, function (err, data) {
if (err) {
reject(err);
} else {
if (data) {
//do something
}
//do some other thing
}
});
How do I mock elasticsearch properly with sinon? Because i'm not allowed to use #elastic/elasticsearch-mock
Since ES SDK defines some APIs such as indices via the getter function:
Object.defineProperties(API.prototype, {
//...
indices: {
get () { return this[kIndices] === null ? (this[kIndices] = new IndicesApi(this.transport)) : this[kIndices] }
},
//...
})
see v8.5.0/src/api/index.ts#L372
We should use stub.get(getterFn) API to replace a new getter for this stub.
For these APIs such as .bulk, .clearScroll which are assigned directly to API.prototype, you can call sinon.stub(Client.prototype, 'bulk') to stub them.
At last, since your code is defined in the module scope, the code will be executed when you import/require it. We should arrange our stubs first, then import/require the module. As you can see, I use dynamic import().
E.g.
index.ts:
import { Client } from '#elastic/elasticsearch';
const esClient = new Client({ node: 'http://localhost:9200' });
esClient.indices.stats({ index: 'a' }).then(console.log);
index.test.ts:
import sinon from 'sinon';
import { Client } from '#elastic/elasticsearch';
import { IndicesStatsResponse } from '#elastic/elasticsearch/lib/api/types';
describe('74949441', () => {
it('should pass', async () => {
const indicesStatsResponseStub: IndicesStatsResponse = {
_shards: {
failed: 0,
successful: 1,
total: 2,
},
_all: {
uuid: '1',
},
};
const indicesApiStub = {
stats: sinon.stub().resolves(indicesStatsResponseStub),
};
sinon.stub(Client.prototype, 'indices').get(() => indicesApiStub);
await import('./');
sinon.assert.calledWithExactly(indicesApiStub.stats, { index: 'a' });
});
});
Test result:
74949441
{
_shards: { failed: 0, successful: 1, total: 2 },
_all: { uuid: '1' }
}
✓ should pass (609ms)
1 passing (614ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.ts | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
package versions:
"#elastic/elasticsearch": "^8.5.0",
"sinon": "^8.1.1",
"mocha": "^8.2.1",
Related
I'm writing a toy app to learn more about Serverless Framework and AWS AppSync etc.
I'm trying to do TDD as much as possible. I'm using mock-apollo-client to mock the ApolloClient, and I've run into a problem. When trying to write a test to make sure the arguments to the query are passed, the test always returns a 401 Unauthorized error. It seems as though the real end point is still being called, because when a valid x-api-key is added to the instantiation of the ApolloClient, the test returns the real value from the AppSync server, and not the mock value I'm expecting. I'm using a mock, not spy, so I'm not expecting the real end point to actually be hit. Furthermore When I do add a valid x-api-key the test fails because the function is never called.
api › recipes › Given a valid recipe id › Should call query with the id as a param
expect(jest.fn()).toBeCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
I'm expected the test to fail, because the query currently isn't called with any arguments, but instead it fails because the mock function is never called.
What am I doing wrong?
Code File
import { ApolloClient, gql, InMemoryCache } from '#apollo/client';
const client = new ApolloClient({
uri: 'https://redacted.appsync-api.redacted.amazonaws.com/graphql',
headers: {
'x-api-key': 'key-redacted',
},
cache: new InMemoryCache(),
});
export const GET_RECIPE_QUERY = gql`
query {
getRecipe (title:"Lemon Cheese Cake") {
title,
ingredients{
name,
amount,
unit
},
steps
}
}
`;
const gqlQuery = (title) => {
return client
.query({
query: GET_RECIPE_QUERY,
variables : { title }
});
};
export const getRecipe = async (id) => {
const result = await gqlQuery(id);
return result.data.getRecipe;
};
Test file
import { createMockClient } from 'mock-apollo-client';
import { GET_RECIPE_QUERY, getRecipe } from './recipes';
const mockRecipe = {
title: 'Luke\'s Chocolate Orange',
ingredients: [
{
name: 'Orange',
amount: 1,
},
{
name: 'Chocolate',
amount: 250,
unit: 'grams',
},
],
steps: [
'Peel orange',
'Open chocolate',
'Eat chocolate',
'Throw orange away',
],
};
const mockClient = createMockClient();
const queryHandler = jest.fn().mockResolvedValue({data: {recipe: mockRecipe}});
mockClient.setRequestHandler(GET_RECIPE_QUERY, queryHandler);
describe('api', () => {
describe('recipes', () => {
describe('Given a valid recipe id', () => {
it('Should call query with the id as a param', async () => {
const id = 'Luke\'s Chocolate Orange';
const result = await getRecipe(id);
expect(queryHandler).toBeCalledTimes(1);
expect(queryHandler).toBeCalledWith(id);
});
});
});
});
Packages
Versions
#apollo/client
3.5.10
graphql
16.3.0
#testing-library/jest-dom
5.16.2
#testing-library/react
12.1.4
#testing-library/user-event
13.5.0
jest
27.5.1
mock-apollo-client
1.2.0
mock-apollo-client always use the with ApolloProvider, so that you pass the mock apollo client via React context Provider to descendant components.
However, your code cannot pass the mock apollo client to the component in this way. Your code initiates requests directly from the Apollo Client. We need to intercept these GraphQL requests. There are several ways to do this such as msw. However, I'll continue to use the mock-apollo-client library to demonstrate.
You need to mock ApolloClient class of the #apollo/client module. We need to use Mocking Partials, we don't want to mock other things exported from #apollo/client. Since the mock-apollo-client library already provides createMockClient function to create mocked apollo client, we don't need to mock by ourself.
An working example:
recipes.ts:
import { ApolloClient, gql, InMemoryCache } from '#apollo/client';
const client = new ApolloClient({
uri: 'https://redacted.appsync-api.redacted.amazonaws.com/graphql',
headers: {
'x-api-key': 'key-redacted',
},
cache: new InMemoryCache(),
});
export const GET_RECIPE_QUERY = gql`
query {
getRecipe(title: "Lemon Cheese Cake") {
title
ingredients {
name
amount
unit
}
steps
}
}
`;
const gqlQuery = (title) => {
return client.query({
query: GET_RECIPE_QUERY,
variables: { title },
});
};
export const getRecipe = async (id) => {
const result = await gqlQuery(id);
return result.data.getRecipe;
};
recipes.test.ts:
import { createMockClient } from 'mock-apollo-client';
const mockRecipe = {
title: "Luke's Chocolate Orange",
ingredients: [
{ name: 'Orange', amount: 1, unit: 'abc' },
{ name: 'Chocolate', amount: 250, unit: 'grams' },
],
steps: ['Peel orange', 'Open chocolate', 'Eat chocolate', 'Throw orange away'],
};
const mockClient = createMockClient();
describe('api', () => {
describe('recipes', () => {
describe('Given a valid recipe id', () => {
beforeEach(() => {
jest.resetModules();
});
it('Should call query with the id as a param', async () => {
jest.doMock('#apollo/client', () => {
return {
...jest.requireActual('#apollo/client'),
ApolloClient: jest.fn(() => mockClient),
};
});
const queryHandler = jest.fn().mockResolvedValue({ data: { getRecipe: mockRecipe } });
const { GET_RECIPE_QUERY, getRecipe } = require('./recipes');
mockClient.setRequestHandler(GET_RECIPE_QUERY, queryHandler);
const title = "Luke's Chocolate Orange";
const result = await getRecipe(title);
expect(result).toEqual(mockRecipe);
expect(queryHandler).toBeCalledWith({ title });
});
});
});
});
Test result:
PASS src/stackoverflow/71612556/recipes.test.ts
api
recipes
Given a valid recipe id
✓ Should call query with the id as a param (91 ms)
------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------|---------|----------|---------|---------|-------------------
All files | 90.91 | 100 | 66.67 | 90.91 |
mocks | 75 | 100 | 0 | 75 |
handlers.js | 66.67 | 100 | 0 | 66.67 | 14
server.js | 100 | 100 | 100 | 100 |
stackoverflow/71612556 | 100 | 100 | 100 | 100 |
recipes.ts | 100 | 100 | 100 | 100 |
------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.775 s
You can find the source code here
I have a config.ts which returns an object:
// Config is an interface that I use to know which values are expected
export default function getConfig(): Config {
return {amount: 50}
}
I have a class (../src/models/item.model) that has a dependency to the config.ts:
import getConfig from '../config/config';
class Item{
_id: number;
amount: number;
constructor(_id: number) {
this._id = _id;
this.amount = getConfig().amount;
}
}
export default Item
I would like to write some tests with a different amount value. The default value is 50 (set in config.ts), but in my item.test.ts I would like to use a value of 100. I'm trying to achieve this by using Proxyquire:
it('should use voxelsize of custom config', (done) => {
const itemModel = proxyquire('../src/models/item.model', {
'../config/config': function getConfig() {
return {amount: 100};
}
}).default;
const testItem = new itemModel(1)
expect(testItem.amount).to.equal(100);
done()
})
testItem.amount is in reality 50 (so it still uses the original configuration file). This should be 100.
How can I let the test pass?
You are using es6 export default function getConfig() {}, so you should assign the mocked getconfig() function to the default attribute of the ./config commonJS module.
E.g.
config.ts:
export default function getConfig() {
return { amount: 50 };
}
item.model.ts:
import getConfig from './config';
class Item {
_id: number;
amount: number;
constructor(_id: number) {
this._id = _id;
this.amount = getConfig().amount;
}
}
export default Item;
item.model.test.ts:
import { expect } from 'chai';
import proxyquire from 'proxyquire';
describe('66691249', () => {
it('should use voxelsize of custom config', () => {
const itemModel = proxyquire('./item.model', {
'./config': {
default: function getConfig() {
return { amount: 100 };
},
},
}).default;
const testItem = new itemModel(1);
expect(testItem.amount).to.equal(100);
});
});
test result:
66691249
✓ should use voxelsize of custom config (1742ms)
1 passing (2s)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 83.33 | 100 | 50 | 83.33 |
config.ts | 50 | 100 | 0 | 50 | 2
item.model.ts | 100 | 100 | 100 | 100 |
---------------|---------|----------|---------|---------|-------------------
There is code in our codebase like below:
#Validate(Param1)
async post(request, responseHandler) {
// some code
}
I Am trying to test the post function. But want to avoid evaluating the #Validate function. The Validate is a function in another module.
// validator.ts
export const Validate = () => {
// some code
}
How to? .
You could use jest.mock(moduleName, factory, options) create the mocked Validate decorator instead of using the real Validate decorator which may have a lot of validation rules.
E.g.
index.ts:
import { Validate } from './validator';
export class Controller {
#Validate('params')
async post(request, responseHandler) {
console.log('real post implementation');
}
}
validator.ts:
export const Validate = (params) => {
return (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
const oFunc = descriptor.value;
descriptor.value = function inner(...args: any[]) {
console.log('real validator decorator implementation');
// lots of validation
const rval = oFunc.apply(this, args);
return rval;
};
};
};
index.test.ts:
import { Validate } from './validator';
import { mocked } from 'ts-jest/utils';
jest.mock('./validator');
describe('63531414', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should pass', async () => {
mocked(Validate).mockImplementationOnce((params) => {
return (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
const oFunc = descriptor.value;
descriptor.value = function inner(...args: any[]) {
console.log('mocked validator decorator implementation');
const rval = oFunc.apply(this, args);
return rval;
};
};
});
const { Controller } = require('./');
const logSpy = jest.spyOn(console, 'log');
const ctrl = new Controller();
await ctrl.post({}, () => {});
expect(Validate).toBeCalledWith('params');
expect(logSpy).toBeCalledWith('real post implementation');
});
});
unit test result with coverage report:
PASS src/stackoverflow/63531414/index.test.ts (12.634s)
63531414
✓ should pass (154ms)
console.log node_modules/jest-mock/build/index.js:860
mocked validator decorator implementation
console.log node_modules/jest-mock/build/index.js:860
real post implementation
--------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------|----------|----------|----------|----------|-------------------|
All files | 45.45 | 100 | 25 | 45.45 | |
index.ts | 100 | 100 | 100 | 100 | |
validator.ts | 14.29 | 100 | 0 | 14.29 | 2,3,4,5,7,8 |
--------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 14.354s
source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/63531414
I'm working on some tests for an electron app I'm building. I'm running into the error below. I'm new to jest, so I imagine it's due to an incorrect setup. Any idea where I'm going wrong?
Error: Cannot find module 'ipcMain' from 'ipcMainEvents.spec.js'
//myEvents.ts
import { ipcMain } from 'electron'
export class IpcMainEvents {
constructor() {
ipcMain.on('openPlaywright', this.openPlaywright)
ipcMain.on('openPreviewBrowser', this.openPlaywright)
}
openPlaywright(event, arg) {
console.log('openPlaywright')
}
openPreviewBrowser(event, arg) {
console.log('openPreviewBrowser')
}
}
//myEvents.spec.ts
import {IpcMainEvents} from './ipcMainEvents'
import {ipcMain} from 'electron'
jest.mock('ipcMain')
describe('Should test the ipcMain events', () => {
let component;
let addSpy
beforeEach(()=>{
component = new IpcMainEvents()
})
it('should attach the eventListeners', () => {
expect(component.ipcMain.on.calls.all()[0].args[0]).toEqual('openPlaywright'); //<----Errors here
expect(component.ipcMain.on.calls.all()[1].args[0]).toEqual('openPreviewBrowser');
expect(component.ipcMain.on.calls.count()).toEqual(2);
});
});
Firstly, you should mock electron package, not ipcMain function.
Second, you should access the calls property of mocked function via .mock property.
E.g.
myEvents.ts:
import { ipcMain } from 'electron';
export class IpcMainEvents {
constructor() {
ipcMain.on('openPlaywright', this.openPlaywright);
ipcMain.on('openPreviewBrowser', this.openPlaywright);
}
openPlaywright(event, arg) {
console.log('openPlaywright');
}
openPreviewBrowser(event, arg) {
console.log('openPreviewBrowser');
}
}
myEvents.spec.ts:
import { IpcMainEvents } from './myEvents';
import { ipcMain } from 'electron';
jest.mock(
'electron',
() => {
const mockIpcMain = {
on: jest.fn().mockReturnThis(),
};
return { ipcMain: mockIpcMain };
},
{ virtual: true },
);
describe('Should test the ipcMain events', () => {
let component;
let addSpy;
beforeEach(() => {
component = new IpcMainEvents();
});
it('should attach the eventListeners', () => {
expect(ipcMain.on.mock.calls[0][0]).toEqual('openPlaywright');
expect(ipcMain.on.mock.calls[1][0]).toEqual('openPreviewBrowser');
expect(ipcMain.on.mock.calls).toHaveLength(2);
});
});
unit test results with coverage report:
PASS stackoverflow/61562193/myEvents.spec.js (10.657s)
Should test the ipcMain events
✓ should attach the eventListeners (3ms)
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 80 | 100 | 50 | 77.78 |
myEvents.js | 80 | 100 | 50 | 77.78 | 10,14
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.917s
I am using mocha, chai and sinon for my testing purposes. I've got a class like below:
class ClassToTest {
person;
constructor(person) {
this.setPerson(person);
}
setPerson(person) {
if (typeof person.firstName === 'undefined') {
throw Error('Person has to have first name.');
}
this.person = person;
}
}
How I can test setPerson function? When I create new ClassToTest object setPerson gets called by constructor and Error gets thrown immediately. I've tried creating stubs with Sinon but no luck so far.
Should I even test setPerson function to begin with? I was thinking about:
1. moving validation (typeof if) to other function (e.g. validatePerson) and test that
2. testing only if constructor throws Error or sets person
Here is the unit test solution:
index.ts:
export class ClassToTest {
person;
constructor(person) {
this.setPerson(person);
}
setPerson(person) {
if (typeof person.firstName === 'undefined') {
throw new Error('Person has to have first name.');
}
this.person = person;
}
}
index.test.ts:
import { ClassToTest } from './';
import sinon from 'sinon';
import { expect } from 'chai';
describe('57091171', () => {
afterEach(() => {
sinon.restore();
});
describe('#constructor', () => {
it('should set person', () => {
const setPersonStub = sinon.stub(ClassToTest.prototype, 'setPerson');
const person = { firstName: 'sinon' };
new ClassToTest(person);
sinon.assert.calledWithExactly(setPersonStub, person);
});
});
describe('#setPerson', () => {
it('should set person', () => {
const stub = sinon.stub(ClassToTest.prototype, 'setPerson').returns();
const person = { firstName: 'sinon' };
const ins = new ClassToTest(person);
stub.restore();
ins.setPerson(person);
expect(ins.person).to.deep.equal(person);
});
it('should handle error if firstName is not existed', () => {
const stub = sinon.stub(ClassToTest.prototype, 'setPerson').returns();
const person = {};
const ins = new ClassToTest(person);
stub.restore();
expect(() => ins.setPerson(person)).to.throw('Person has to have first name.');
});
});
});
unit test results with 100% coverage:
57091171
#constructor
✓ should set person
#setPerson
✓ should set person
✓ should handle error if firstName is not existed
3 passing (43ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.ts | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------