Test that React prop method has been called with Jest - reactjs

I have an Input component, which accepts a prop method and calls it when the user types something in. Code itself works as expected, but for some reasons, test fails. It thinks that prop method wasn't called. Why is it happening? For testing purposes, I use Jest and react-testing-library.
And second question. In real application, my idea is to test parameters that were passed to that prop method. Is it considered to be an implementation testing (I know that I should test it)?
Input.js
export default function Input({ onChange }) {
return <input onChange={onChange} />;
}
Test
import React from "react";
import { render, act, cleanup, fireEvent } from "react-testing-library";
import Input from "./input";
describe("Input tests", () => {
afterEach(cleanup);
it("Should call prop function", () => {
const onChange = jest.fn();
const { getByTestId } = render(<Input onChange={onChange} />);
const input = getByTestId("input");
act(() => {
fireEvent.change(input, { target: { value: "Q" } });
});
expect(onChange).toHaveBeenCalled();
});
});
https://codesandbox.io/s/y229669nvx

After reading this, it looks like it's by design to not assert against events handlers. Although it appears to work in React 16.5, however, using 16.8.x fails. I'd suggest moving to enzyme if you want to test such features.
Testing with react-testing-library fails (however, as you'll notice, when running the test, the input's value will actually change): https://codesandbox.io/s/n3rvy891n4
Testing with enzyme succeeds: https://codesandbox.io/s/lx34ny41nl

The reason why your test doesn't work is that you're using getByTestId to find your element. getByTestId looks for a DOM node that has a data-testid attribute.
In order to make your test pass, you have various options.
You could add a data-testid to your input: <input data-testid="input" onChange={onChange} />. This would work, however, it's better to avoid test ids whenever you can.
In a real application, your input would be rendered with a label, we can take advantage of that:
const { getByLabelText } = render(
<label>
My input
<Input onChange={onChange} />
</label>
)
const input = getByLabelText('My input')
Another solution is to use container which is one one of the values returned by render. It's a DOM node—like everything else in RTL—so you can use the usual DOM APIs:
const { container } = render(<Input onChange={onChange} />)
// Any of these would work
const input = container.firstChild
const input = container.querySelector('input')
As a side note, I agree that RTL tests seem more complicated if compared to Enzyme. There's a good reason for it. RTL pushes you to test your application as if it were a black box. This is a bit harder to do in the beginning but ultimately leads to better tests.
Enzyme, on the other hand, mocks most things by default and allows you to interact with your components implementation. This, in my experience, looks easier in the beginning but will produce brittle tests.
I encourage you to join the spectrum channel if you need help getting started.

Related

Not simulating 'change' using enzyme

I have a Reactjs component and it has one button and a date-range picker.
I want to simulate onclick and onchange events of button and picker respectively.
I'm able to simulate onclick of the button. but on change of datepicker is not working
I have tried this
headerComponent.find(`#prev_button`).at(1).simulate("click");
headerComponent.find(`#dropdown`).at(1).simulate("change", { value: "t" });
please see this sandbox click here for full code and test file
Based on Enzyme documentation you make a mistake on your second argument on simulate function.
To simulate changes on the input, you should change it like this :
headerComponent.find(`#dropdown`).at(1).simulate("change", { target: { value: "t" } });
Testing with enzyme is tricky. You should try not to test dependencies because you trust those are already tested. Having said that, you could shallow render instead of mounting and look for the RangePicker component in the shallow tree, get the handler you are passing in the onChange prop and call it manually, then check the callback prop you pass to your component is called with the expected value.
describe.only("test", () => {
it("should render", () => {
const callBackToSetDates = jest.fn();
const callBackToSetFilter = jest.fn();
const wrapper = shallow(
<Header
{...headerProps1}
callBackToSetDates={callBackToSetDates}
callBackToSetFilter={callBackToSetFilter}
/>
);
const rangePickerOnChange = wrapper.find("RangePicker").prop("onChange");
rangePickerOnChange("someValue");
expect(callBackToSetDates).toHaveBeenCalledWith("someValue");
});
});
the purpose is to test only the logic you add inside your component, i.e., you transform the value you get from the RangePicker to something else
<RangePicker
...
onChange={(value) => {
callBackToSetDates(`I'm transforming ${value}`);
}}
/>
and in your test
rangePickerOnChange("someValue");
expect(callBackToSetDates).toHaveBeenCalledWith("I'm transforming someValue");
you can see it working here https://codesandbox.io/s/cool-rosalind-uec6t?file=/src/tests/index.test.js
If you really want to keep testing what the actual user sees, you'll need to fire the events that the user does when using the component. In this case: you need to click the input, look for a date, click it, then click another date to completely fire the onChange event of the RangePicker component. You might look at how antd test it and copy the necessary jest configuration they have to mock some DOM APIs

Testing React Hooks side effects that depends on other side effects (or other tests)

I have a React Hook that has a simple input and button, and the button is disabled if there is no input, and that same button executes a fetch when it is enabled:
function MyComponent() {
const [ value, setValue ] = useState('')
function apiRequest() {
if (!value) {
return
}
axios.get('url')
.then(console.log)
.catch(console.log)
}
return (
<div>
<input onChange={e => setValue(e.target.value)} value={value} />
<button disabled={!value} onClick={apiRequest}>
Submit
</button>
</div>
)
}
I wrote two tests with Enzyme. The first one to test if the disabled prop is correct, and the second one to see if it actually fetches.
it('sets the disabled prop appropriately', function() {
const wrapper = mount(<MyComponent />)
const input = wrapper.find('input')
const btn = wrapper.find('button')
expect(btn.prop('disabled')).toBeTruthy()
input.simulate('change', 'abc123')
expect(btn.prop('disabled')).toBeFalsy()
})
it('fetches on submit', function () {
const wrapper = mount(<MyComponent />)
const input = wrapper.find('input')
const btn = wrapper.find('button')
input.simulate('change', 'abc123')
btn.simulate('click')
expect(axios.get).toHaveBeenCalled()
})
But unfortunately for the second test to work, the button needs to be enabled so text has to be inputted first. So in reality, the second test is also unintentionally testing the disabled prop as well because it will fail (the onClick will not fire) if the disabled prop isn't set correctly.
I followed React's recommended approach of
test React components without relying on their implementation details
which is react-testing-library's core principle, so I'm purely testing side effects. I'm using enzyme instead of that because my team is currently using Enzyme
How would I be able to rewrite my second test so I can only test for the fetch? Thanks in advance.
Edit: Or rather, a couple ways to rewrite this to properly test the fetch?
One thing you could do is replace the <div> with a <form> and add the onSubmit={e => apiRequest(value)} to it so the button can remain disabled and you can still move forward with your tests without introducing unnecessary external factors.
Also, move your function apiRequest() {...} outside of the component. It can take value as an argument instead of relying on the surrounding scope.
// You could even export this separately and make a test exclusively for this
// without also having to test the form itself
function apiRequest ( value ) {
if (!value) {
return
}
axios.get('url')
.then(console.log)
.catch(console.log)
}
function MyComponent() {
const [ value, setValue ] = useState('')
return (
<form onSubmit={e => { e.preventDefault(); apiRequest(value); }}>
<input onChange={e => setValue(e.target.value)} value={value} />
<button disabled={!value}>
Submit
</button>
</form>
)
}
To my believe you definitely are testing behavior, not implementation details.
It would be relying on implementation details if you say were setState()(sure it does not work to functional component that's why it's a bad pattern).
Using RTL you still had to change input prior clicking button, no differences here.
The only thing is rather implementation details it's relying on axios itself. You may use nock to handle any request made with any library. But I'm not sure if that worth it.

How to create an unit test for UncontrolledTooltip from reactstrap that does not handle state management directly?

I implemented simple UncontrolledTooltip from reactstrap. The doc (https://reactstrap.github.io/components/tooltips/) says
uncontrolled component can provide the functionality wanted without the need to manage/control the state of the component
If I want to implement an unit test (e.g. jest + enzyme) for testing its state as either open or close, how can I create a unit test without manually tinkering with state value? Is this possible to achieve it? It seems only possible with regular Tooltip component but I like to hear advice from seasoned engineers.
[Update]:
Upon request I include here tooltip and unit test I am trying to execute. At the moment, I want to simulate hover on the tooltip however mockHover.mock.calls.length returns as 0 which I interpret as mock function was not triggered.
Here is my Tooltip.
import React from 'react';
import { UncontrolledTooltip } from 'reactstrap';
export default class MyTooltip extends React.Component {
render() {
const { metaData, wg } = this.props;
return (
<div>
<UncontrolledTooltip placement="bottom" trigger={'hover'} target={wg}>
{metaData}
</UncontrolledTooltip>
</div>
);
}
}
Here is my unit test that use jest and enzyme:
describe('<MyTooltip />', () => {
it('Tooltip unit test', () => {
const mockHover = jest.fn();
const wrapper = shallow(<MyTooltip trigger={mockHover} />);
expect(wrapper.find(UncontrolledTooltip));
wrapper.find(UncontrolledTooltip).simulate('hover');
expect(mockHover.mock.calls.length).toEqual(1);
});
});
There are few important things to start from:
UncontrolledTooltip is part of 3rd party package so you won't test it explicitly.
Instead you better focus on testing your wrapper around UncontrolledTooltip.
simulate is nothing related to events browser's system. It's just a syntax sugar to do props().onHover(...). So if target component has such a prop - and it's a callback-function - it will be called. If there is no such a prop - it would be up to defaultProps what's going on. Anyway nothing like 'emulating mouse cursor over the element'.
shallow() will stop rendering at level of UncontrolledTooltip(its internals will not be rendered)
Keeping that in mind I see you able only:
your component finally renders UncontrolledTooltip with expected constant prop values
both metaData and wg props are passed down to UncontrolledTooltip
it('renders UncontrolledTooltips under the hood', () => {
const wg = '1';
const metaData = (<span>2</span>);
const wrapper = shallow(<MyTooltip wg={wg} metaData={metaData} />);
const innerTooltip = wrapper.find(UncontrolledTooltip);
/*
I don't validate `find(UncontrolledTooltip).toHaveLength(1)`
since assertion on `.find(..).props()` would throw exception otherwise
*/
expect(innerTooltip.props().placement).toEqual('bottom');
expect(innerTooltip.props().trigger).toEqual('hover');
expect(innerTooltip.props().wg).toEqual(wg);
expect(innerTooltip.props().metaData).toEqual(metaData);
});

Why doesn't my React onChange method match my arrow function in enzymes containsAllMatchingElements test

I have written a test case which is trying to test if react is rendering all my elements correctly.
Code that is being tested:
...
eventDateChange(moment, dateType) {
const {handleEventChange} = this.props;
let {event} = this.state;
event[dateType] = moment.format("YYYY-MM-DD HH:mm");
this.setState({event});
handleEventChange(event);
};
render() {
return (
<div className="event-input">
<DateTime
onChange={moment => this.eventDateChange(moment,'startDate')}
inputProps={{placeholder: 'From:'}}
dateFormat="YYYY-MM-DD"/>
</div>
)
}
...
Test code:
import React from "react";
import {expect} from "chai";
import {shallow} from "enzyme";
import EventInput from "../../../blog/event/EventInput.jsx";
import DateTime from "react-datetime";
describe('EventInput', () => {
it('is rendering an calendar icon', () => {
const wrapper = shallow(<EventInput/>);
expect(wrapper.containsAllMatchingElements([
<DateTime
onChange={moment => wrapper.instance.eventDateChange(moment,'startDate')}
inputProps={{placeholder: 'From:'}}
dateFormat="YYYY-MM-DD"/>
])).to.equal(true);
});
});
The problem is that my onChange method is failing the tests. If I remove the onChange method from the code and the test, the test is succeeding.
As you can see I was using Mocha, Chai, Enzyme in the tests.
From what I can see all the props are the same except for the onChange where I can't use this in the test and need to change it to the instance.
Looking at the implementation of how enzyme compares nodes (https://github.com/airbnb/enzyme/blob/d34630e9c3e07ca7983d37695d5668377f94a793/src/Utils.js#L102), it looks like enzyme requires props of type "function" to exactly match when doing this comparison (right[prop] === left[prop]) and will return false if this condition is not met.
Your lambda functions are not identical, so the === comparison will fail. Whenever you use lambda, you are creating a new anonymous function, so even if the parameters and body are the same between two lambda declarations, they are actually two separate function instances.
To get around this problem, you could either 1) create a reference to the exact function instance used in your component so that your test can reference it, or 2) use a different enzyme API here. I would suggest #2. It seems like you are overtesting a bit by checking all property values, and you could use something like this instead:
it('is rendering an calendar icon', () => {
const wrapper = shallow(<EventInput/>);
expect(wrapper.matchesElement(
<div>
<DateTime />
</div>
)).to.equal(true);
});
If you want to test individual props on the DateTime, you could additionally do something like: expect(wrapper.find("DateTime").props().dateFormat).to.equal("YYYY-MM-DD").

React Native Testing - How to TDD and test components beyond snapshots?

I'm really struggling with how to test React Native components beyond using snapshots from jest. Those are great for testing the look of something based on some data passed into a component, but it doesn't seem like there is a way to test events or event handlers, checking if event listeners were setup or disconnected correctly in lifecycle methods, etc...like it feels like I'm either missing something or the tooling is not complete.
Also on a side note snapshot testing feels backwards in terms of TDD since you can only write your tests once you have your application code...any thoughts on this?
With snapshot test you can only check the values in props and styles etc. To check some logic in container i used 'shallow' from enzyme ;)
You can still use snapshots with events for things like: click a button and verify the rendered output after the click:
it('should render Markdown in preview mode', () => {
const wrapper = shallow(
<MarkdownEditor value="*Hello* Jest!" />
);
expect(wrapper).toMatchSnapshot();
wrapper.find('[name="toggle-preview"]').simulate('click');
expect(wrapper).toMatchSnapshot();
});
To test that event handler was called property you can do something like that:
it('should pass a selected value to the onChange handler', () => {
const value = '2';
const onChange = jest.fn();
const wrapper = shallow(
<Select items={ITEMS} onChange={onChange} />
);
expect(wrapper).toMatchSnapshot();
wrapper.find('select').simulate('change', {
target: { value },
});
expect(onChange).toBeCalledWith(value);
});
(Both examples are from my article on testing React components with Jest.)

Resources