Spy on default exported function with Jest - javascript

I need to test whether or not the logger() function is called in the addCampus method I am testing when it throws an error. I am fairly new to jest so I may be missing something simple
Logger.js
function logger(level, message) {
//logs message to console
//has no explicit return
}
export default logger;
AddCampusList.jsx
import logger from '../../Logger';
addCampus = campus => {
axios
.post('/api/campuses/', {
name: campus.campusName,
abbreviation: campus.campusAbbreviation,
})
.then(response => {
const { campuses } = this.state;
campuses.push(response.data);
this.setState({ campuses });
})
.catch(error => {
this.props.displayError(error);
logger('ERROR', error);
});
};
AddCampusList.test.js
import logger from '../../../src/Logger.js'
...
it('calls displayError() with error', async () => {
getSpy = jest.spyOn(axios, 'get').mockRejectedValueOnce(error);
const logger = jest.fn();
const loggerSpy = jest.spyOn(logger, 'default');
wrapper = await shallow(<AddCampusList
displayError={displayError}
onSelectCampus={onSelectCampus}
selectedCampus={selectedCampus}
isMobileViewport={isMobileViewport}
/>);
expect(displayError).toHaveBeenCalledWith(error);
expect(loggerSpy).toHaveBeenCalled();
});
The expect(displayError) is working properly, but the expect(loggerSpy) is not.
I have tried several different things, but this is the error I have run into most often
Cannot spy the default property because it is not a function; undefined given instead
> 105 | const loggerSpy = jest.spyOn(logger, 'default');

The package logger is already default import so it's not supposed to have default property.
A correct way to spy on it is:
import * as loggerMod from '../../../src/Logger'
...
const loggerSpy = jest.spyOn(loggerMod, 'default');
This may not work because ES modules are read-only and whether this is enforced depends on a setup.
A more correct way is to do this in module mock:
import logger from '../../../src/Logger'
jest.mock('../../../src/Logger.js', () => {
const loggerMod = jest.requireActual('../../../src/Logger');
return {
...loggerMod,
__esModule: true,
default: jest.fn(loggerMod.default)
};
});
...
expect(logger).toHaveBeenCalled();
Most times it's preferable to mock a function that does potentially undesirable side effects rather than spy on it, and this is much simpler, especially for a module with default export only:
jest.mock('../../../src/Logger', () => jest.fn());

Related

TypeError: is not a function with React, Typescript and Axios

So, I searched for an existing solution, but I could find nothing, or maybe I'm not searching the correct way, thus, sorry if there's an existing thread about it.
In sum, it seems my code is not instantiating an object correctly as a class when it comes from an Axios call to the backend. So, when I call some function, I'm getting the error Uncaught TypeError TypeError: object.method is not a function.
Example:
First, basically, a parent component will call a service that will make a request to the backend. The result is then passed to a child component.
// imports
const Component: React.FC<ComponentProps> = () => {
const { id } = useParams<{ id: string }>();
const [object, setObject] = useState<Class>(new Class());
useEffect(() => {
(async () => {
try {
const object = await Service.getById(id);
setObject(object);
} catch (err) {
//error handling
} finally {
cleanup();
}
})();
return () => {
// cleanup
};
});
return (
<Container title={object.name}>
<Child object={object} />
</Container>
);
};
export default Component;
Then, in child component, let's say I try to call a method that was defined in the Class, there I'm getting the not a function error:
// imports
interface Interface {
object: Class;
}
const Child: React.FC<Interface> = ({ object }) => {
object.callSomeFunction(); // error starts here
return (
<SomeJSXCode />
);
};
export default Child;
Example of the Class code, I tried to write the method as a function, arrow function, and a getter, but none worked. Also, as a workaround, I've been defining a method to instantiate the object and set all properties, but I don't think that's a good long-term solution, and for classes with many properties, it gets huge:
export class Class {
id: string = '';
name: string = '';
callSomeFunction = () => {
// do something;
}
static from(object: Class): Class {
const newInstance = new Class();
newInstance.id = object.id;
newInstance.name = object.name;
// imagine doing this for a class with many attributes
return newInstance;
}
}
Finally, the Service code if necessary to better understand:
// imports
const URL = 'http://localhost:8000';
const baseConfig: AxiosRequestConfig = {
baseURL: URL,
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
};
export const backend = axios.create({
...baseConfig,
baseURL: URL + '/someEndpoint',
});
export const Service = {
async getById(id: string): Promise<Class> {
try {
const { data } = await backend.get<Class>(`/${id}`);
return data;
} catch (err) {
throw new Error(err.response.data.message);
}
},
};
As I can't share the real code due to privacy, please let me know if this is enough or if more information is needed. Thanks in advance.
I thought it was some binding issue as here, but no.
So, I actually fixed this by updating the class validator in the back end, as the parsing was only necessary to parse the strings as number. But, by adding the annotation #Type(() => Number) to my dtos, I won't need to parse the strings anymore.

Trying to stub a function results in Descriptor for property is non-configurable and non-writable

I'm trying to write out a unit test that stubs the getSignedUrl function from the #aws-sdk/s3-request-presigner package, however when I try stub out the function with sinon, I receive the error:
TypeError: Descriptor for property getSignedUrl is non-configurable and non-writable
const s3RequestSigner = require("#aws-sdk/s3-request-presigner");
const expect = require('chai').expect;
const sinon = require('sinon')
....
it('should throw an error when getSignedUrl rejects', async function() {
const sandbox = sinon.createSandbox();
sandbox.stub(s3RequestSigner, "getSignedUrl").rejects("fakeUrl");
sandbox.restore();
})
I'm using node.js 16 and writing javascript rather than typescript. Is there a way to mock out my function, i'm struggling to write my tests otherwise?
I came up with the following workaround for ES6 modules. You can wrap getSignedUrl in your own module and mock that module instead. This approach should work for any modules where sinon is unable to mock a "non-configurable and non-writable" method.
For example:
my-s3-client-internals.js - Your custom wrapper module
// You'll need to import the original method, assign it to
// a new const, then export that const
import { getSignedUrl as getSignedUrl_orig } from '#aws-sdk/s3-request-presigner';
export const getSignedUrl = getSignedUrl_orig;
my-s3-client.js - Consumer of getSignedUrl
// Import the method instead from your custom file
import { getSignedUrl } from './my-s3-client-internals';
// Call it however you normally would, for example:
export const getUrl(bucket, key) {
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
return getSignedUrl(client, command, { expiresIn: 300 });
}
my-s3-client.spec.js - Unit tests for the consumer module
import { getUrl } from './my-s3-client';
import * as clientInternals from './my-s3-client-internals';
import sinon from 'sinon';
it('does something', () => {
// Mock the method exported from your wrapper module
sinon.stub(clientInternals, 'getSignedUrl')
.callsFake(async (client, command, options) => {
return 'fake-url';
});
// Then call your consumer method to test
const url = await getUrl('test-bucket', 'test-key');
expect(url).to.equal('fake-url');
});
So I won't make this the official answer, unless there are no better solutions, but this is what my research has brought about a solution.
The issue is related to this: https://github.com/sinonjs/sinon/issues/2377
Where sinon will throw an error when the Object.descriptor is non-configurable.
There is no obvious way around that currently, that I can find. The way to solve it is to use proxyquire:
const sinon = require('sinon')
const proxyquire = require('proxyquire')
...
it('should throw an error when getSignedUrl rejects', async function() {
const fakeurl = 'hello world'
const fakeURL = sinon.stub().resolves(fakeurl)
const handler = proxyquire(
'../../handlers/presigned_url',
{
'#aws-sdk/s3-request-presigner': {
'getSignedUrl': async () => {
return fakeURL()
}
}
}
)
This will then resolve with whatever you want fakeurl to be.
Another possible solution is to use mockery. E.g. to mock uuid
import { expect } from 'chai';
import mockery from 'mockery';
import sinon from 'sinon';
describe('domain/books', () => {
let createBook;
let uuidStub;
before(async () => {
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false,
});
uuidStub = sinon.stub();
mockery.registerMock('uuid', { v4: uuidStub });
({ createBook } = await import('../domain/books.js'));
});
afterEach(() => {
sinon.resetHistory();
});
after(() => {
sinon.restore();
mockery.disable();
mockery.deregisterAll();
});
describe('createBook', () => {
it('should save a book and return the id', () => {
const id = 'abc123';
uuidStub.returns(id);
const { id: bookId } = createBook({
title: 'My Book',
author: 'Jane Doe',
});
expect(bookId).to.equal(id);
});
});
});
The mockery setup is a bit tedious, but the library saved me a number of times.

Raven throws warnings when testing via Jest

I'm trying to test my GraphQL api through Jest and every time I run my tests I keep getting this alert:
raven#2.5.0 alert: This looks like a browser environment; are you sure you don't want Raven.js for browser JavaScript?
The cause:
I create a custom Error class that inherits from Error:
import logError from './errors';
class LoggedErrorClass extends Error {
constructor(error) {
logError(error);
const prototype = new.target.prototype;
if (typeof error === 'string') {
super(error);
} else {
super(error.message);
}
this.__proto__ = prototype;
}
}
LoggedError = LoggedErrorClass;
And use it like this:
if (!user || !Roles.userIsInRole(user._id, ['admin', 'customer'])) {
throw new LoggedError('Access denied');
}
logError is a function that uses Raven. Because I use Meteor I do LoggedError = LoggedErrorClass to make LoggedError accessible globally (notice, I don't export LoggedErrorClass)
My test looks like this:
import { graphql } from 'graphql';
import schema from '../../../graphql';
describe('getMobileSettings query', function() {
// global.LoggedError = class extends Error {
// constructor(...args) {
// super(...args);
// Error.captureStackTrace(this, Error);
// }
// };
it('should work', async () => {
const query = `
query getMobileSettings($app: String!) {
getMobileSettings(app: $app)
}`;
const [rootValue, context, params] = [{}, {}, { app: 'web' }];
await graphql(schema, query, rootValue, context, params);
});
});
I've tried setting LoggedError with the help of global but it didn't help. So, I can't just call jest.mock('path/to/file') because I don't export it. Also, it seems quite weird that Raven is here, because I use it in logError which I only import in a file where I create LoggedErrorClass
Ok, after some digging, I figured out the solution.
I decided not to mock LoggedError class but rather mock logError function that my class uses. As a result I came up with this code that mocks Raven behaviour:
const Raven = {};
const install = jest.fn();
const config = jest.fn();
Raven.install = install;
Raven.config = config;
// mocking chained function calls
install.mockImplementation((...args) => {
return Raven;
});
config.mockImplementation((...args) => {
return Raven;
});
export default Raven;
I've also updated my jest.conf.js by adding raven to moduleNameMapper:
module.exports = {
moduleNameMapper: {
'^meteor/(.*)': '<rootDir>/tests/.mocks/meteor/index.js',
raven: '<rootDir>/tests/.mocks/npm/raven.js',
},
automock: false,
clearMocks: true,
};

How to mock es6 class using Jest

I am attempting to mock a class Mailer using jest and I can't figure out how to do it. The docs don't give many examples of how this works. The process is the I will have a node event password-reset that is fired and when that event is fired, I want to send an email using Mailer.send(to, subject, body). Here is my directory structure:
project_root
-- __test__
---- server
------ services
-------- emails
---------- mailer.test.js
-- server
---- services
------ emails
-------- mailer.js
-------- __mocks__
---------- mailer.js
Here is my mock file __mocks__/mailer.js:
const Mailer = jest.genMockFromModule('Mailer');
function send(to, subject, body) {
return { to, subject, body };
}
module.exports = Mailer;
and my mailer.test.js
const EventEmitter = require('events');
const Mailer = jest.mock('../../../../server/services/emails/mailer');
test('sends an email when the password-reset event is fired', () => {
const send = Mailer.send();
const event = new EventEmitter();
event.emit('password-reset');
expect(send).toHaveBeenCalled();
});
and finally my mailer.js class:
class Mailer {
constructor() {
this.mailgun = require('mailgun-js')({
apiKey: process.env.MAILGUN_API_KEY,
domain: process.env.MAILGUN_DOMAIN,
});
}
send(to, subject, body) {
return new Promise((reject, resolve) => {
this.mailgun.messages().send({
from: 'Securely App <friendly-robot#securelyapp.com>',
to,
subject: subject,
html: body,
}, (error, body) => {
if (error) {
return reject(error);
}
return resolve('The email was sent successfully!');
});
});
}
}
module.exports = new Mailer();
So, how do I successfully mock and test this class, using Jest? Many thanks for helping!
You don't have to mock your mailer class but the mailgun-js module. So mailgun is a function that returns the function messages that return the function send. So the mock will look like this.
for the happy path
const happyPath = () => ({
messages: () => ({
send: (args, callback) => callback()
})
})
for the error case
const errorCase = () => ({
messages: () => ({
send: (args, callback) => callback('someError')
})
})
as you have this 2 cases it make sense to mock the module inside your test. First you have to mock it with a simple spy where we later can set the implementation for our cases and then we have to import the module.
jest.mock('mailgun-js', jest.fn())
import mailgun from 'mailgun-js'
import Mailer from '../../../../server/services/emails/mailer'
As your module uses promises we have 2 options either return the promise from the test or use async/await. I use the later one for more info have a look here.
test('test the happy path', async() => {
//mock the mailgun so it returns our happy path mock
mailgun.mockImplementation(() => happyPath)
//we need to use async/awit here to let jest recognize the promise
const send = await Mailer.send();
expect(send).toBe('The email was sent successfully!')
});
If you would like to test that the mailgun send method was called with the correct parameter you need to adapt the mock like this:
const send = jest.fn((args, callback) => callback())
const happyPath = () => ({
messages: () => ({
send: send
})
})
Now you could check that the first parameter for send was correct:
expect(send.mock.calls[0][0]).toMatchSnapshot()
Just for Googlers and future visitors, here's how I've setup jest mocking for ES6 classes.
I also have a working example at github, with babel-jest for transpiling the ES module syntax so that jest can mock them properly.
__mocks__/MockedClass.js
const stub = {
someMethod: jest.fn(),
someAttribute: true
}
module.exports = () => stub;
Your code can call this with new, and in your tests you can call the function and overwrite any default implementation.
example.spec.js
const mockedClass = require("path/to/MockedClass")();
const AnotherClass = require("path/to/AnotherClass");
let anotherClass;
jest.mock("path/to/MockedClass");
describe("AnotherClass", () => {
beforeEach(() => {
mockedClass.someMethod.mockImplementation(() => {
return { "foo": "bar" };
});
anotherClass = new AnotherClass();
});
describe("on init", () => {
beforeEach(() => {
anotherClass.init();
});
it("uses a mock", () => {
expect(mockedClass.someMethod.toHaveBeenCalled();
expect(anotherClass.settings)
.toEqual(expect.objectContaining({ "foo": "bar" }));
});
});
});

Jest - Testing Module Multiple Times in One Test Suite

I have a TypeScript module (should be irrelevant, as I think this affect JS too) and I'm trying to test a module I have. The module imports lots of data from external files and chooses which data should be returned based on the a variable.
I'm attempting to run some tests where I update that variable, re-require the module and run further tests in one file. But my issue is that the require of the file only runs once. I guess it's being cached. Is it possible to tell Jest's require function not to cache or to clear the cache between tests?
Here's some stripped back code of what I'm trying to achieve:
module.ts
import { getLanguage } from "utils/functions";
import * as messagesEn from "resources/translations/en";
import * as messagesFr from "resources/translations/fr";
// Determine the user's default language.
const language: string = getLanguage();
// Set messages based on the language.
let messages: LocaleMessages = messagesEn.default;
if (languageWithoutRegionCode === "fr") {
messages = messagesFr.default;
}
export { messages, language };
test.ts
import "jest";
// Mock the modules
const messagesEn = { "translation1": "English", "translation2": "Words" };
const messagesFr = { "translation1": "Francais", "translation2": "Mots" };
const getLangTest = jest.fn(() => "te-ST");
const getLangEn = jest.fn(() => "en-GB");
const getLangFr = jest.fn(() => "fr-FR");
jest.mock("resources/translations/en", () => ({"default": messagesEn}));
jest.mock("resources/translations/fr", () => ({"default": messagesFr}));
jest.mock("utils/functions", () => ({
getLanguage: getLangTest
})
);
describe("Localisation initialisation", () => {
it("Sets language", () => {
const localisation = require("./localisation");
expect(getLangTest).toHaveBeenCalled();
expect(localisation.language).toEqual("te-ST");
expect(localisation.messages).toEqual(messagesEn);
});
it("Sets english messages", () => {
// THIS GETS THE MODULE FROM THE CACHE
const localisation = require("./localisation");
expect(getLangEn).toHaveBeenCalled();
expect(localisation.language).toEqual("en-GB");
expect(localisation.messages).toEqual(messagesEn);
});
it("Sets french messages", () => {
// THIS GETS THE MODULE FROM THE CACHE
const localisation = require("./localisation");
expect(getLangFr).toHaveBeenCalled();
expect(localisation.language).toEqual("fr-FR");
expect(localisation.messages).toEqual(messagesFr);
});
});
I'm aware the second and third tests won't work anyway as I'd need to update the "utils/functions" mock. The issue is that the code in module.ts only runs once.
So, many thanks to the Jest folks on Discord. It's possible to actually clear the modules from the cache with the jest.resetModules() function.
So my test.ts file will look as follows:
describe("Localisation initialisation", () => {
beforeEach(() => {
jest.resetModules();
});
it("Sets language", () => {
const localisation = require("./localisation");
// Perform the tests
});
it("Sets english messages", () => {
const localisation = require("./localisation");
// Perform the tests
});
it("Sets french messages", () => {
const localisation = require("./localisation");
// Perform the tests
});
});
The beforeEach() call to jest.resetModules() ensures we're re-running the code in the module.

Categories