I just finished writing my first Reactjs component and I am ready to write some tests (I used material-ui's Table and Toggle).
I read about jest and enzyme but I feel that I am still missing something.
My component looks like this (simplified):
export default class MyComponent extends Component {
constructor() {
super()
this.state = {
data: []
}
// bind methods to this
}
componentDidMount() {
this.initializeData()
}
initializeData() {
// fetch data from server and setStates
}
foo() {
// manuipulatig data
}
render() {
reutrn (
<Toggle
id="my-toggle"
...
onToggle={this.foo}
>
</Toggle>
<MyTable
id="my-table"
data={this.state.data}
...
>
</MyTable>
)
}
}
Now for the test. I want to write a test for the following scenario:
Feed initializeData with mocked data.
Toggle my-toggle
Assert data has changed (Should I assert data itself or it is better practice to assert my-table instead?)
So I started in the very beginning with:
describe('myTestCase', () => {
it('myFirstTest', () => {
const wrapper = shallow(<MyComponent/>);
}
})
I ran it, but it failed: ReferenceError: fetch is not defined
My first question is then, how do I mock initializeData to overcome the need of calling the real code that using fetch?
I followed this answer: https://stackoverflow.com/a/48082419/2022010 and came up with the following:
describe('myTestCase', () => {
it('myFirstTest', () => {
const spy = jest.spyOn(MyComponent.prototype, 'initializeData'
const wrapper = mount(<MyComponent/>);
}
})
But I am still getting the same error (I also tried it with componentDidMount instead of initializeData but it ended up the same).
Update: I was wrong. I do get a fetch is not defined error but this time it is coming from the Table component (which is a wrap for material-ui's Table). Now that I come to think about it I do have a lot of "fetches" along the way... I wonder how to take care of them then.
fetch is supported in the browser, but jest/enzyme run in a Node environment, so fetch isn't a globally available function in your test code. There are a few ways you can get around this:
1: Globally mock fetch - this is probably the simplest solution, but maybe not the cleanest.
global.fetch = jest.fn().mockResolvedValue({
json: () => /*Fake test data*/
// or mock a response with `.text()` etc if that's what
// your initializeData function uses
});
2: Abstract your fetch call into a service layer and inject that as a dependency - This will make your code more flexible (more boilerplate though), since you can hide fetch implementation behind whatever interface you choose. Then at any point in the future, if you decide to use a different fetch library, you can swap out the implementation in your service layer.
// fetchService.js
export const fetchData = (url) => {
// Simplified example, only takes 'url', doesn't
// handle errors or other params.
return fetch(url).then(res => res.json());
}
// MyComponent.js
import {fetchService} from './fetchService.js'
class MyComponent extends React.Component {
static defaultProps = {
// Pass in the imported fetchService by default. This
// way you won't have to pass it in manually in production
// but you have the option to pass it in for your tests
fetchService
}
...
initializeData() {
// Use the fetchService from props
this.props.fetchService.fetchData('some/url').then(data => {
this.setState({ data });
})
}
}
// MyComponent.jest.js
it('myFirstTest', () => {
const fetchData = jest.fn().mockResolvedValue(/*Fake test data*/);
const fetchService = { fetchData };
const wrapper = mount(<MyComponent fetchService={fetchService} />);
return Promise.resolve().then(() = {
// The mock fetch will be resolved by this point, so you can make
// expectations about your component post-initialization here
})
}
Related
Cypress has a way to expose the app's state to the test runner -- in React it usually looks like this:
class MyComponent extends React.Component {
constructor (props) {
super(props)
// only expose the app during E2E tests
if (window.Cypress) {
window.app = this
}
}
...
}
Then you could access your state in a test with
cy.window()
.its('app.state')
.should('deep.equal', myStateObject)
However, the setup for Remix projects relies on functional components. I've tried this in my root.tsx component with a useEffect call:
export default function App() {
useEffect(() => {
window.app = App;
}, []}
}
as well as in the root route (routes/index.tsx) by importing the <App /> component and using the logic in the useEffect function above. Neither of these options are working and I'm not sure where else to go here. Remix's GitHub issues are devoid of questions about this issue, so maybe I'm going about this the wrong way. Any help is appreciated! Thanks!
I haven't done much work with Remix, but there is a question here that might be useful:
React - getting a component from a DOM element for debugging.
Note the last paragraph
Function components
Function components don't have "instances" in the same way classes do, so you can't just modify the FindReact function to return an object with forceUpdate, setState, etc. on it for function components.
That said, you can at least obtain the React-fiber node for that path, containing its props, state, and such. To do so, modify the last line of the FindReact function to just: return compFiber;
There's a lib cypress-react-app-actions that implements this for Cypress
export const getReactFiber = (el) => {
const key = Object.keys(el).find((key) => {
return (
key.startsWith('__reactFiber$') || // react 17+
key.startsWith('__reactInternalInstance$') // react <17
)
})
if (!key) {
return
}
return el[key]
}
// react 16+
export const getComponent = (fiber) => {
let parentFiber = fiber.return
while (typeof parentFiber.type == 'string') {
parentFiber = parentFiber.return
}
return parentFiber
}
One of the example tests is
/// <reference types="cypress" />
import { getReactFiber, getComponent } from '../support/utils'
it('calls Example double()', () => {
cy.visit('/')
cy.get('.Example').within(() => { // select via className of component
cy.contains('[data-cy=count]', '0')
cy.get('[data-cy=add]').click().click()
cy.contains('[data-cy=count]', '2')
cy.root().then((el$) => {
const fiber = getReactFiber(el$[0])
console.log(fiber)
const component = getComponent(fiber)
console.log(component.stateNode)
cy.log('calling **double()**')
component.stateNode.double() // work with component for functional
})
cy.contains('[data-cy=count]', '4')
})
})
This example is for class components, but given the info in Function components section above, you would use the component object rather than component.stateNode.
I am currently trying to write a test to test what is inside of a higher order components
my test like so:
let Component = withEverything(Header);
let wrapper;
it('renders correctly', async () => {
wrapper = await mountWithSleep(
<Component componentProps={{ session: { id: '2' } }} />,
0.25
);
console.log(wrapper.debug());
});
});
outputs the following:
<Component>
<WithSession component={[Function: GlobalNav]} innerProps={{...}} />
</Component>
My with session file looks like the following:
import React, { Component, ComponentType } from 'react';
import { Session, session } from '#efa/web/src/modules/auth/authService';
import { Omit } from '#everlutionsk/helpers';
import { Subscription } from 'rxjs';
class WithSession extends Component<Props, State> {
state: State = {
session: undefined
};
private subscription: Subscription;
componentDidMount() {
this.subscription = session.subscribe(session => {
this.setState({ session });
});
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
if (this.state.session === undefined) return null;
const Component = this.props.component;
const props = { ...this.props.innerProps, session: this.state.session };
return <Component {...props} />;
}
}
/**
* Injects a current session to the given [component].
*/
export function withSession<P extends SessionProps>(
component: ComponentType<P>
): ComponentType<Omit<P, keyof SessionProps>> {
return props => <WithSession component={component} innerProps={props} />;
}
export interface SessionProps {
readonly session: Session | null;
}
interface Props {
readonly component: ComponentType;
readonly innerProps: any;
}
interface State {
readonly session: Session | null | undefined;
}
I have tried to do a jest.mock which gets me part of the way using this:
jest.mock('#efa/web/src/modules/auth/components/withSession', () => {
//#ts-ignore
const original = jest.requireActual(
'#efa/web/src/modules/auth/components/withSession'
);
return {
__esModule: true,
...original,
withSession: component => {
return component;
}
};
});
Using this module i can at least see a returned component instead of with session. But now the issue is i need to be able to set the session state. wondering if anyone can help?!
It is worth noting this is a project which we inherited i would not of implemented it this way ever!
You're correct that this type of code absolutely makes testing more difficult. The "best" way in this case its probably a bit of work because the nicest way to go about it would be to switch this thing to context; and then add options to your test framework mount method to populate that context how you want from individual tests. Though, you can reach something similar with old fashioned HOCs.
There's a cheaper way, and that would be to allow options to be passed to withEverything (if this is used by the app as well, you can create a mirror one called withTestHocs or similiar. I.e.
withTestHocs(Header, {
session: //... object here
})
Internally, this HOC would no longer call withSession whatsoever. Instead, it would call a HOC who's only purpose is to inject that session config object into the component for test reasons.
There's no reason to do complex mocking to get the session right on every test, its a waste of time. You only need that if you're actually testing withSession itself. Here you should be prioritising your test framework API that makes having custom session per test nice and simple. jest.mock is not easily parametrised, so that in itself is also another good reason to not go down that road. Again, the exception is when you're unit testing the actual session hoc but those tests are typically quite edge-casey and wont be using the core "test framework HOC" you'll use for all your userland/feature code -- which is what I'm focusing on here.
Note with this solution, you wouldn't need the complex jest mocking anymore (provided all tests were moved to the new way).
export const withEverything =
(Component, {session}) =>
({ providerProps, componentProps }) =>
(
<MockedProvider {...providerProps}>
<BrowserRouter>
<FlashMessage>
<Component {...componentProps} session={session} />
</FlashMessage>
</BrowserRouter>
</MockedProvider>
);
Now in your test
it('renders correctly', async () => {
wrapper = await mountWithSleep(
const withEverything(Header, { session: { id: '2' }}),
0.25
);
console.log(wrapper.debug());
});
If you need to be able to manipulate the session mid-test, you could do that by returning a method from withEverthing that allows the session to be set, but im not sure if you need it.
I have a React JS component MyComponent and I would like to test the following use case:
It should call updateSomething() when component on mount
And I've come up with the following code:
System Under Test (SUT)
export class MyComponent extends React.Component<Props, State> {
public componentDidMount() {
console.log("componentDidMount"); // Debug purpose.
this.fetchSomething()
.then(() => {
console.log("fetchSomething"); // Debug purpose.
this.updateSomething();
});
}
// These are public for simplicity
public async fetchSomething(): Promise<any> { }
public updateSomething() {
console.log("updateSomething"); // Debug purpose.
}
}
Test
it("should update something, when on mount", () => {
const props = { ...baseProps };
sinon.stub(MyComponent.prototype, "fetchSomething").resolves();
const spy = sinon.spy(MyComponent.prototype, "updateSomething");
shallow(<MyComponent {...props} />);
sinon.assert.calledOnce(spy);
});
The result is the test failed with AssertError: expected updateSomething to be called once but was called 0 times but all three console.log() printed.
My understanding is since I want to test the event when on mount, I have to spy/stub it before it's even created, therefore I have to spy on MyComponent.Prototype. Also, for fetchSomething(), I have to stub the async call and make it .resolves() to let it progress.
But I couldn't understand how it can still console.log("updateSomething") without being spied.
I don't know about sinon and I don't know about ts, but with simple js and jest it'd be like this:
fetchSomething() = Promise.resolve();
Then, in your test, you wouldn't have to mock it and just use:
const spy = jest.spyOn(MyComponent.prototype, 'updateSomething');
To see if it was called:
expect(spy).toHaveBeenCalled();
According to the comments/answer from this post, the assertion comes before .updateSomething have been called. To solve this problem, I would've to await the componentDidMount lifecycle method.
So the fixed program is as below:
// SUT
public async componentDidMount() {
//...
return this.fetchSomething()
.then(() => {
//...
});
}
// Test
it("should update something, when on mount", () => {
const props = { ...baseProps };
// Disable lifecycle here to enable stub in between.
const wrapper = shallow(<MyComponent {...props} />, { disableLifecycleMethods: true });
sinon.stub(wrapper.instance(), "fetchSomething").resolves();
const stub = sinon.stub(wrapper.instance(), "updateSomething");
// Actually call component did mount.
wrapper.instance().componentDidMount().then(() => {
sinon.assert.calledOnce(stub);
});
});
I'm using a plugin that renders out a form using json schema. For elements like input, button, etc, it uses a React component within the structure to render out the component. In our app, we receive schema json that describes the layout. For example, we could receive something like this (simplified to make it easier to read)
{
component: 'input'
}
and I have a convertor function that places the component in where one is detected in the structure. It will send something do like: (again, simplified)
import Table from './Table';
covert(schema) {
return {
component: Table // where table is: (props:any) => JSX.Element
}
}
I want to write a test for this, but having trouble with the comparing the result with the expected. In my test, I mock the Table component and send through a named mock function as the second param. Then I use the same named param in the expected results.
I get an error back: The second argument ofjest.mockmust be an inline function. I can change this to an inline function, but then how can I use this in the expected structure used for comparison?
// Test code
import React from 'react';
const mockComponent = () => <div>table</div>
jest.mock('./Table', mockComponent);
const schema = {
component: 'table'
}
describe('Component Structure', () => {
test('componentizes the schema structure', () => {
const results = convert(schema);
const expected = {
component: mockComponent
};
expect(results).toEqual(expected);
});
});
The error is because you are not mocking the component properly, the right way should be:
jest.mock('./Table', () => mockComponent);
given that you already have mockComponent defined as:
const mockComponent = () => <div>table</div>
or you could do:
jest.mock('./Table', () => () => <div />);
The proper mocking of the components would be something like this:
const mockComponent = () => <div>table</div>
jest.mock('./Table', () => mockComponent)
Using Jest and Enzyme, how can I test if this.props.functionToTest was run?
class TestComponent extends Component {
static propTypes = {
functionToTest: PropTypes.func
}
componentDidMount() {
this.props.functionToTest()
}
}
In Jest, I've tried creating mockProps and passing them in when mounting the component.
let props = {
functionToTest = jest.fn(() => {});
}
beforeEach(() => {
const wrapper = mount(<TestComponent {...props} />
}
A console.log in the componentDidMount function shows functionToTest as undefined. Obviously passing in the props during mount isn't working.
Question 1: How can I pass in mock props that will show in the componentDidMount function?
Question 2: Once that function is available, how do I gain access to the function so I can use spyOn or something similar to test if the function was run?
I don't know your exact setup, but this is how I would do that:
Mock the function with jest.fn() like you did
Pass mock to the component being mounted (like apparently you did)
Check whether it was run with expect(...).toBeCalled() or .toHaveBeenCalled() (varies between different Jest versions)
.
let props = {
functionToTest: jest.fn() // You don't need to define the implementation if it's empty
};
beforeEach(() => {
const wrapper = mount(<TestComponent {...props} />
}
// In the test code:
it('does something', () => {
expect(props.functionToTest).toBeCalled();
// OR... depending on your version of Jest
expect(props.functionToTest).toHaveBeenCalled();
});
The problem ended up being that TestComponent was only being exported within the Redux wrapper. Adding an export at the class level and destructuring it in the Jest test import, along with the solution Henrick posted above fixed it.