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

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.)

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

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);
});

Test that React prop method has been called with Jest

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.

Are Enzyme / React shallow renders expensive?

We're having a discussion at work about Enzyme shallow renders and the time per test to re-run shallow on each test. Be it methods, clicks, selector lengths, etc., I'm suggesting that our tests might run faster if we shallow render the component one time before the tests run versus each time.
Are there any experts who can point out which way would be faster and if there are any pitfalls in either way? These examples are using the AVA runner (and slightly contrived for the sake of discussion).
For example, here's one way (A)...
import TagBox from '../TagBox';
const props = { toggleValue: sinon.spy() };
let wrapper = {};
test.before(t => {
wrapper = shallow(<TagBox />);
});
test('it should have two children', t => {
t.is(wrapper.children().length, 2);
});
test('it should safely set props', t => {
wrapper.setProps({...props});
t.is(wrapper.children().length, 2);
});
test('it should call when clicked', t => {
wrapper.setProps({...props});
wrapper.find({tagX : true}).last().simulate('click');
t.true(props.toggleValue.calledOnce);
});
And here's the other (B)...
import TagBox from '../TagBox';
test('it sets value to null ...', t => {
const props = {multiple: false};
const wrapper = shallow(<TagBox {...props} />);
t.is(wrapper.state('currentValue'), null);
});
test('it sets value to [] if multiple', t => {
const props = {multiple: true};
const wrapper = shallow(<TagBox {...props} />);
t.deepEqual(wrapper.state('currentValue'), []);
});
test('it does not use value if ...', t => {
const props = = {value: 3};
const wrapper = shallow(<TagBox {...props} />);
t.is(wrapper.state('currentValue'), null);
});
// etc. etc.
Notice that in test B, there is a new shallow wrapper for each test when essentially nothing has changed but props.
Over the course of 100 tests, what would you expect to be the difference in time to completion?
Also is there any chance shallow rendering once (test A) in the higher scope would pollute the test state?
Shallow renderer is designed to be fast, because it renders only single component. So, usually you will not get any performance troubles, when you create new component for each test.
Also, your example A can work incorrectly if TagBox component has inner state. That't why example B is more preferable way to write tests.
The shallow is probably not your problem here since it's designed to be the fastest way to render a component without cascading all of it's children renders.
You may consider changing your test running engine then, AVA is kinda slow compared to Jest for example. I did this change a year ago and it is a LOT faster. Jest also provides in it's base kit more useful stuff like mocking functions of example.
More here: https://facebook.github.io/jest/

TDD with Enzyme shallow and mount?

When I render a component using shallow or mount, the component is rendered in memory and is not attached to DOM.
This means that, while I run my tests, I don't actually see any output in the browser.
How am I supposed to do Test Driven Development if I can't see if the component I'm developing looks as it should? (css style, sizes etc)
If you want your component to be rendered an mounted une the "browser", than use the render() method of Enzyme. Be sure to have a window available (see jsdom to fake a window). But IMO, you should be able to do all of your test with shallow or mount, the API is nice
How am I supposed to do Test Driven Development if I can't see if the
component I'm developing looks as it should? (css style, sizes etc)
The purpose of Enzyme is not visual regression test, for that you will have to use tools like PhantomJS, related article https://css-tricks.com/visual-regression-testing-with-phantomcss/
You can achieve some kind of styling testing by checking if your component has the right selectors when they are rendered. e.g.
it( 'should add the "selected" class when a click happend to one of the Elements', () => {
const wrapper = mount( <Elements /> );
const option = wrapper.find( 'h5' );
expect( option.hasClass( 'selected' ) ).to.equal( false );
option.simulate( 'click' );
expect( option.hasClass( 'selected' ) ).to.equal( true );
} );

Resources