I'm trying to figure out how to write a Typescript/React app which uses socket.io to communicate to a server and thus other clients. However I'd like to write some tests in doing so.
In my sample app I have:
import io, { Socket } from 'socket.io-client';
const App = () => {
let socket: Socket;
const ENDPOINT = 'localhost:5000';
const join = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
event.preventDefault();
socket = io(ENDPOINT);
socket.emit('join', { name: 'Paola', room: '1' }, () => {});
};
return (
<div className="join-container">
<button className="join-button" onClick={join} data-testid={'join-button'}>
Sign in
</button>
</div>
);
};
export default App;
And my test looks like:
import App from './App';
import { render, screen, fireEvent } from '#testing-library/react';
import 'setimmediate';
describe('Join', () => {
let mockEmitter = jest.fn();
beforeEach(() => {
jest.mock('socket.io-client', () => {
const mockedSocket = {
emit: mockEmitter,
on: jest.fn((event: string, callback: Function) => {}),
};
return jest.fn(() => {
return mockedSocket;
}
);
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('joins a chat', () => {
// Arrange
render(<App />);
const btn = screen.getByTestId('join-button');
// Act
fireEvent.click(btn);
// Assert
expect(btn).toBeInTheDocument();
expect(mockEmitter).toHaveBeenCalled();
});
});
I just want to make sure I can mock socket.io-client so that I can verify that messages are being sent to the client and that it (later) reacts to messages sent in.
However the test is failing and it doesn't seem to be using my mock.
Error: expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
In the manual-mocks#examples doc, there is a note:
Note: In order to mock properly, Jest needs jest.mock('moduleName') to be in the same scope as the require/import statement.
So, there are two solutions:
app.tsx:
import React from 'react';
import io, { Socket } from 'socket.io-client';
const App = () => {
let socket: Socket;
const ENDPOINT = 'localhost:5000';
const join = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
socket = io(ENDPOINT);
socket.emit('join', { name: 'Paola', room: '1' }, () => {});
};
return (
<div className="join-container">
<button className="join-button" onClick={join} data-testid={'join-button'}>
Sign in
</button>
</div>
);
};
export default App;
Option 1: Call jest.mock and import ./app module in module scope of the test file.
app.test.tsx:
import App from './App';
import { render, screen, fireEvent } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import React from 'react';
let mockEmitter = jest.fn();
jest.mock('socket.io-client', () => {
return jest.fn(() => ({
emit: mockEmitter,
on: jest.fn(),
}));
});
describe('Join', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('joins a chat', () => {
// Arrange
render(<App />);
const btn = screen.getByTestId('join-button');
// Act
fireEvent.click(btn);
// Assert
expect(btn).toBeInTheDocument();
expect(mockEmitter).toHaveBeenCalled();
});
});
Option 2: Since you call the jest.mock in beforeEach hook, require the './app' module in beforeEach hook function scope as well.
app.test.tsx:
import { render, screen, fireEvent } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import React from 'react';
describe('Join', () => {
let mockEmitter = jest.fn();
let App;
beforeEach(() => {
App = require('./app').default;
jest.mock('socket.io-client', () => {
const mockedSocket = {
emit: mockEmitter,
on: jest.fn(),
};
return jest.fn(() => mockedSocket);
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('joins a chat', () => {
// Arrange
render(<App />);
const btn = screen.getByTestId('join-button');
// Act
fireEvent.click(btn);
// Assert
expect(btn).toBeInTheDocument();
expect(fakeEmitter).toHaveBeenCalled();
});
});
package version:
"jest": "^26.6.3",
"ts-jest": "^26.4.4"
jest.config.js:
module.exports = {
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'jsdom'
}
Related
I'm trying to mock an axios get function but I'm getting TypeError: moduleName.startsWith is not a function when mocking "axios" What would be the right way to mock it?
Error:
FAIL src/tests/Sample.test.jsx
● Test suite failed to run
TypeError: moduleName.startsWith is not a function
5 | import { backendUrl } from "../helper/constants"
6 | describe("test useEffect and axios", () => {
> 7 | const mockGet = jest.mock(axios.get)
Sample.test.jsx
import { mount } from "enzyme"
import { screen, render, act } from "#testing-library/react"
import Sample from "../pages/Sample"
import axios from "axios"
import { backendUrl } from "../helper/constants"
describe("test useEffect and axios", () => {
const mockGet = jest.mock(axios.get) // THROWS ERROR HERE
let wrapper
it("should call axios", async () => {
await act(async () => {
mockGet.mockImplementationOnce(() => Promise.resolve({}))
wrapper = mount(<Sample />)
})
wrapper.update()
await expect(mockGet).toHaveBeenCalledWith(backendUrl)
})
})
Sample.jsx
import axios from "axios"
import { useState, useEffect } from "react"
import { backendUrl } from "../helper/constants"
const Sample = () =>{
const [pets, setPets] = useState([])
useEffect(() => axios.get(backendUrl)
.then(({data}) =>setPets(data.entries))
.catch((err)=>console.log(err)), [])
return (
<>
<p>I h8 all of you</p>
{pets.map((e, i) =><h2 key={i}>{e.Link}</h2>)}
</>
)
}
export default Sample
You have to use the right syntax like this jest.mock('axios') so in your case:
jest.mock('axios')
describe("test useEffect and axios", () => {
const mockGet = jest.fn()
let wrapper
it("should call axios", async () => {
await act(async () => {
mockGet.mockImplementationOnce(() => Promise.resolve({}))
wrapper = mount(<Sample />)
})
wrapper.update()
await expect(mockGet).toHaveBeenCalledWith(backendUrl)
})
})
I have a plugin that console.logs data.
logData.spec.js
import Vue from 'vue'
import { createLocalVue } from '#vue/test-utils'
import logData from './logData'
describe('logData plugin', () => {
const localVue = createLocalVue()
it('adds a $logData method to the Vue prototype', () => {
expect(Vue.prototype.$logData).toBeUndefined()
localVue.use(logData)
expect(typeof localVue.prototype.$logData).toBe('function')
})
it('console.logs data passed to it', () => {
const data = 'data to be logged'
const localVue = createLocalVue()
localVue.use(logData)
expect(localVue.prototype.$logData(data)).toBe('data to be logged')
})
})
logData.js
export function logData (dataToLog) {
const isLoggingData = localStorage.getItem('isLoggingData')
if (isLoggingData) {
console.log(dataToLog)
}
}
export default {
install: function (Vue) {
Vue.prototype.$logData = logData
}
}
The error I get is in my unit test is Expected: 'data to be logged", Received: undefined. Why is the second test being read as undefined?
It's expected behavior since console.log() returns undefined. To get desired result you should add this line of code to your lodData function:
return dataToLog
export function logData (dataToLog) {
const isLoggingData = localStorage.getItem('isLoggingData')
if (isLoggingData) {
console.log(dataToLog)
return dataToLog
}
}
NOTICE: Also you don't have localStorage in your test environment.
I'm trying to mock an async function that is exported as a default export but all I get is TypeError: Cannot read property 'then' of undefined
What I'm trying to mock is config.js:
const configureEnvironment = async (nativeConfig) => {
return { await whatever() }
}
The file I'm testing is Scene.js:
import configureEnvironment from './config';
class Scene extends React.Component {
constructor(props) {
nativeConfig = {};
configureEnfironment(nativeConfig).then((config) => {
// Do stuff
}
}
}
And my test file is Scene.test.js:
let getScene = null;
const configureEnvironmentMock = jest.fn();
describe('Scene', () => {
jest.mock('./config', () => configureEnvironmentMock);
const Scene = require('./Scene').default;
getScene = (previousState) => {
return shallow(
<Scene prevState={previousState}>
<Fragment />
</Scene>,
);
};
it('calls configureEnvironment with the nativeConfig', async () => {
expect.assertions(1);
const nativeConfig = {};
getScene(nativeConfig);
expect(configureEnvironmentMock).toHaveBeenCalledWith(nativeConfig);
});
});
However, the result of running the test is:
TypeError: Cannot read property 'then' of undefined
I understand the issue is on the way I mock configureEnvironment but I cannot get it working.
I also tried to mock the function like:
jest.mock('./config', () => {
return {
default: configureEnvironmentMock,
};
});
But it results on:
TypeError: (0 , _config2.default) is not a function
A clean and simple way to mock the default export of a module is to use jest.spyOn in combination with functions like mockImplementation.
Here is a working example based on the code snippets above:
config.js
const whatever = async () => 'result';
const configureEnvironment = async (nativeConfig) => await whatever();
export default configureEnvironment;
Scene.js
import * as React from 'react';
import configureEnvironment from './config';
export class Scene extends React.Component {
constructor(props) {
super(props);
configureEnvironment(props.prevState).then((config) => {
// Do stuff
});
}
render() {
return null;
}
}
Scene.test.js
import React, { Fragment } from 'react';
import { shallow } from 'enzyme';
import { Scene } from './Scene';
import * as config from './config';
describe('Scene', () => {
const mock = jest.spyOn(config, 'default'); // spy on the default export of config
mock.mockImplementation(() => Promise.resolve('config')); // replace the implementation
const getScene = (previousState) => {
return shallow(
<Scene prevState={previousState}>
<Fragment />
</Scene>,
);
};
it('calls configureEnvironment with the nativeConfig', async () => {
expect.assertions(1);
const nativeConfig = {};
getScene(nativeConfig);
expect(mock).lastCalledWith(nativeConfig); // SUCCESS
});
});
You can mock anything with jest, like this
jest.mock('#material-ui/core/withWidth', () => ({
__esModule: true,
isWidthUp: jest.fn((a, b) => true),
default: jest.fn(fn => fn => fn)
}))
I have this components that renders the routes of an app: https://jsbin.com/bahaxudijo/edit?js, I'm trying to mock the BrowserRouter and the Route to do the test, this are my test:
import React from 'react';
import renderer from 'react-test-renderer';
import Router from '../../../components/Router/Component';
jest.mock('react-router-dom', () => ({
BrowserRouter: ({ children }) => <div>{children}</div>,
Route: ({ children }) => <div>{children}</div>,
}));
jest.mock('../../../components/Nav/index', () => '<MockedNav />');
jest.mock('../../../components/ScheduleManager/index', () => '<MockedScheduleManager />');
const props = {
token: '',
loginStaff: jest.fn(),
};
describe('<Router />', () => {
describe('When is passed a token', () => {
it('renders the correct route', () => {
const component = renderer.create(<Router {...props} />);
expect(component).toMatchSnapshot();
});
});
});
But I'm mocking wrong the BrowserRouter and the Route, so the test passes but the snapshots are only empty divs. How can I properly mock the BrowserRouter and the Route?
jest.mock('react-router-dom', () => {
// Require the original module to not be mocked...
const originalModule = jest.requireActual('react-router-dom');
return {
__esModule: true,
...originalModule,
// add your noops here
useParams: jest.fn(),
useHistory: jest.fn(),
};
});
Yet another way:
const rrd = require('react-router-dom');
jest.spyOn(rrd, 'BrowserRouter').mockImplementation(({children}) => children);
Sources:
https://medium.com/#antonybudianto/react-router-testing-with-jest-and-enzyme-17294fefd303
https://stackoverflow.com/a/56565849/1505348
const reactRouter = require('react-router-dom');
const { MemoryRouter } = reactRouter;
const MockBrowserRouter = ({ children }) => (
<MemoryRouter initialEntries={['/']}>
{ children }
</MemoryRouter>
);
MockBrowserRouter.propTypes = { children: PropTypes.node.isRequired };
reactRouter.BrowserRouter = MockBrowserRouter;
Using the code from this answer to solve clicking outside of a component:
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
}
setWrapperRef(node) {
this.wrapperRef = node;
}
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
this.props.actions.something() // Eg. closes modal
}
}
I can't figure out how to unit test the unhappy path so the alert isn't run, what i've got so far:
it('Handles click outside of component', () => {
props = {
actions: {
something: jest.fn(),
}
}
const wrapper = mount(
<Component {... props} />,
)
expect(props.actions.something.mock.calls.length).toBe(0)
// Happy path should trigger mock
wrapper.instance().handleClick({
target: 'outside',
})
expect(props.actions.something.mock.calls.length).toBe(1) //true
// Unhappy path should not trigger mock here ???
expect(props.actions.something.mock.calls.length).toBe(1)
})
I've tried:
sending through wrapper.html()
.finding a node and sending through (doesn't mock a event.target)
.simulateing click on an element inside (doesn't trigger event listener)
I'm sure i'm missing something small but I couldn't find an example of this anywhere.
import { mount } from 'enzyme'
import React from 'react'
import ReactDOM from 'react-dom'
it('Should not call action on click inside the component', () => {
const map = {}
document.addEventListener = jest.fn((event, cb) => {
map[event] = cb
})
const props = {
actions: {
something: jest.fn(),
}
}
const wrapper = mount(<Component {... props} />)
map.mousedown({
target: ReactDOM.findDOMNode(wrapper.instance()),
})
expect(props.actions.something).not.toHaveBeenCalled()
})
The solution from this enzyme issue on github.
The selected answer did not cover the else path of handleClickOutside
I added mousedown event on ref element to trigger else path of handleClickOutside
import { mount } from 'enzyme'
import React from 'react'
import ReactDOM from 'react-dom'
it('Should not call action on click inside the component', () => {
const map = {}
document.addEventListener = jest.fn((event, cb) => {
map[event] = cb
})
const props = {
actions: {
something: jest.fn(),
}
}
//test if path of handleClickOutside
const wrapper = mount(<Component {... props} />)
map.mousedown({
target: ReactDOM.findDOMNode(wrapper.instance()),
})
//test else path of handleClickOutside
const refWrapper = mount(<RefComponent />)
map.mousedown({
target: ReactDOM.findDOMNode(refWrapper.instance()),
})
expect(props.actions.something).not.toHaveBeenCalled()
})
I found the case/solution where the usage of ReactDOM.findDOMNode can be avoided. Treat the following example:
import React from 'react';
import { shallow } from 'enzyme';
const initFireEvent = () => {
const map = {};
document.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
});
document.removeEventListener = jest.fn(event => {
delete map[event];
});
return map;
};
describe('<ClickOutside />', () => {
const fireEvent = initFireEvent();
const children = <button type="button">Content</button>;
it('should call actions.something() when clicking outside', () => {
const props = {
actions: {
something: jest.fn(),
}
};
const onClick = jest.fn();
mount(<ClickOutside {...props}>{children}</ClickOutside>);
fireEvent.mousedown({ target: document.body });
expect(props.actions.something).toHaveBeenCalledTimes(1);
});
it('should NOT call actions.something() when clicking inside', () => {
const props = {
actions: {
something: jest.fn(),
}
};
const wrapper = mount(
<ClickOutside onClick={onClick}>{children}</ClickOutside>,
);
fireEvent.mousedown({
target: wrapper.find('button').instance(),
});
expect(props.actions.something).not.toHaveBeenCalled();
});
});
Versions:
"react": "^16.8.6",
"jest": "^25.1.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2"
The simplest thing just dispatchEvent on body
mount(<MultiTagSelect {...props} />);
window.document.body.dispatchEvent(new Event('click'));
Use sinon to track the handleClickOutside is called or not. By the way, I just now released our project where I need this unit-test in the Nav component . Indeed when you click outside, all submenus should be closed.
import sinon from 'sinon';
import Component from '../src/Component';
it('handle clicking outside', () => {
const handleClickOutside = sinon.spy(Component.prototype, 'handleClickOutside');
const wrapper = mount(
<div>
<Component {... props} />
<div><a class="any-element-outside">Anylink</a></div>
</div>
);
wrapper.find('.any-element-outside').last().simulate('click');
expect(handleClickOutside.called).toBeTruthy();
handleClickOutside.restore();
})