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/
Related
I'm new to React and confused about all the testing libraries. I got my test code to work but it seems redundant to have to call create() from react-test-renderer in order to use its toMatchSnapshot() and have to call render() from #testing-library/react in order to use its assertions such as getByLabelText().
import {render} from '#testing-library/react';
import {act, create} from 'react-test-renderer';
it('renders a login screen', () => {
let mockInitialState: AppState = {
auth: initialAuthState
};
let component = <Root initialState={mockInitialState}/>;
let tree = null;
act(() => {
tree = create(component);
});
expect(tree).toMatchSnapshot();
const {getByLabelText, getByText} = render(component);
expect(getByLabelText(/Email Address.*/));
expect(getByLabelText(/Password*/));
expect(getByText('Sign in'));
});
As a newbie, it's hard for me to understand the difference between all these React libraries. But I'm thinking there must be a simpler way.
How can I simplify my test code so I only have to call one thing that renders the component so that I can do snapshot testing and more specific assertions?
I got the answer from Ziad Saab at Codementor.io:
create() allows you test against the virtual DOM (i.e. the "React DOM")
render() comes from react testing library and renders your tree but also allows you to have all the get*() assertions. It allows you to test against the DOM.
Here's how the code can be simplified:
it('renders a login screen', () => {
let mockInitialState: AppState = {
auth: initialAuthState
};
const {container, getByLabelText, getByText} = render(<Root initialState={mockInitialState}/>);
expect(container.firstChild).toMatchSnapshot();
expect(getByLabelText(/Email Address.*/));
expect(getByLabelText(/Password*/));
expect(getByText('Sign in'));
});
Ziad let me know that there was no reason to have act(), it was something to work around a bug in create(). Now that the code doesn't used create() there is no need for act().
As a result, my snapshot now contains class instead of className because class is what's in the actual HTML DOM whereas className is its equivalent in React's "Virtual DOM".
(Before) Snapshot with create() based on React's Virtual DOM:
className="MuiBox-root MuiBox-root-256"
(After) Snapshot with render() based on HTML DOM:
class="MuiBox-root MuiBox-root-256"
If you're using Create React App then I'd stick with react-testing-library since it comes with it.
Instead of container, you can also use asFragment for snapshot testing.
const {container} = render(<Root initialState={mockInitialState}/>);
expect(asFragment).toMatchSnapshot();
I have been having trouble getting 100% test coverage on one of my buttons (A React functional components.) Basically when it is clicked, it executes some code and then also calls another method from within this onClick called resetButtons. This method will find all the buttons like it in the app and remove a class. This is a preemptive behavior so that only one button at a time can be active.
So far I have tested the click using .simulate, passing in a mocked domElement. And then test that the domElement.classList.add method is called with 'active'.
Obviously this being a DOM centered operation, I am finding it very difficult to test the resetButtons method that lies within the component. especially considering it doesn't have any methods.
I have tried defining the resetButtons method outside of the component and then exported it so the jest test could import it. However I have been unable to test the method as it seems to want it to be a spy or mock, and not the method itself. (Matcher error: received value must be a mock or spy function
)
Here is the react Functional Component:
import React from 'react';
import PropTypes from 'prop-types';
import classes from './MainButton.module.scss';
const MainButton = (props) => {
const resetButtons = () => {
const elements = document.getElementsByClassName('mainButton');
for (let i = 0; i < elements.length; i += 1) {
elements[i].classList.remove('active');
}
};
const handleClick = (event) => {
if (!event.target.classList.contains('active')) {
resetButtons();
event.target.classList.add('active');
props.setVisualState(props.className.split('-')[0]);
}
};
return (
<button
onClick={handleClick}
type="button"
className={`${classes.mainButton} ${props.className}`}
>
{props.children}
</button>
);
};
MainButton.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
setVisualState: PropTypes.func.isRequired,
};
MainButton.defaultProps = {
children: 'Button',
className: '',
};
export default MainButton;
Here is the Test
import React from 'react';
import { shallow } from 'enzyme';
import MainButton from './MainButton';
describe('MainButton', () => {
const domElement = { classList: { contains: jest.fn(), remove: jest.fn(), add: jest.fn() } };
const setVisualStateMock = jest.fn();
const mainButton = shallow(<MainButton setVisualState={setVisualStateMock} />);
it(' is rendered properly', () => {
expect(mainButton).toMatchSnapshot();
});
describe('when clicked', () => {
beforeEach(() => {
mainButton.find('button').simulate('click', { target: domElement });
});
it('it runs `classlist.add` to assign `active` class', () => {
expect(domElement.classList.add).toHaveBeenCalledWith('active');
});
it('it runs set visual state to update `Allergen` container `state`', () => {
expect(setVisualStateMock).toHaveBeenCalled();
});
});
});
Currently the coverage report is reporting 92% coverage, but the branch is at 50 and the line that is causing the trouble is on line 9 (the elements[i].classList.remove('active'); line.
I know at 90% I should probably just move on but this is something I want to be able to figure out. Feel like getting head around this will make me a better tested.
Hope you guys can help!
Fumbling around in the DOM yourself is an anti-pattern. That's React's job. Instead of manipulating the dom with target.classList.add you should have a state property that holds the status which of your inputs is currently active. Then, while rendering you can say className={isActiveInput ? "active": null}.
Because the state is not specific to your MainButton component you would lift the state up. If you have the state somewhere in the parent you don't have to crudely search for DOM elements by classname and manipulate the dom yourself.
Simply put, the rule of React is: You define how things are supposed to look like, React takes care that your definition becomes reality in the dom. If you manipulate the DOM yourself - you're doing it wrong.
When all of this is done, you will have no problem at all with tests, because all you have to do is provide the proper state and props, which is easy, and check that your callback is triggered onClick.
EDIT: Advanced version would be to use Context, but I'd go with state lifting first.
You should be able to mount multiple MainButtons, click one and expect that the other(s) had domElement.classList.remove called on them.
However, user konqi is right in that React provides better ways of manipulating elements/components.
You could replace this test:
expect(domElement.classList.add).toHaveBeenCalledWith('active');
with a test that checks that the button has (or does not have) the active className (instead of checking that the function was called with the right argument). With that test in place, if you like, you could refactor this in the way that konqi suggests.
Say I have a component like so -
// #flow
import React, { PureComponent } from 'react';
export default class ReplaceLink extends Component {
containerRef = React.createRef();
componentDidMount() {
const links =
Array.from(this.containerRef.current.getElementsByTagName('a'));
links.forEach(a => a.setAttribute('href', 'dummylink'));
}
render = () => <div ref={this.containerRef}>{this.props.children}</div>;
}
which replaces href of links placed within it. But even when doing full dom rendering in enzyme, when I do a wrapper.debug() to see the result of the render, I still see original links only.
I've tried doing a force wrapper.update and using setTimeouts, but it just doesn't reflect the expected link.
One of reasons why direct DOM access is discouraged in React is that it makes testing more complicated.
The component can be rendered with skipped componentDidMount:
const wrapper = shallow(<ReplaceLink/>, { disableLifecycleMethods: true })
Then a ref can be mocked and componentDidMount can be called manually:
const setAttribute = jest.fn();
const getElementsByTagName = jest.fn().mockImplementation(() => [{ setAttribute }]);
wrapper.instance().containerRef.current = { getElementsByTagName };
wrapper.instance().componentDidMount();
Then stubbed DOM functions can be asserted that they were called.
Found the best way to test something like this is through the getDOMNode method.
First, make sure to use mount to render the wrapper, so we have a simulated DOM environment to query against.
Next, use wrapper.getDOMNode() to get the underlying DOM node.
Any changes made during the lifecycle methods to the underlying DOM will be reflected in this DOM reference.
Use .querySelector, or <insert-dom-query-method> to make assertions.
const wrapper = mount(
<ReplaceLink>
Google
</ReplaceLink>
);
const linkTags = wrapper.getDOMNode().querySelectorAll('a');
linkTags.forEach(tag => {
expect(tag.getAttribute('href')).toBe('dummy');
});
I want to test if my props are passed, but without using Enzyme,
I tried to find documentation on the internet, but all the tutorials are with Enzyme.
describe('App Component', () => {
it('renders the Counter wrapper', () => {
const wrapper = shallow(<App />);
expect(wrapper.find(Counter)).to.have.length(1);
});
it('passes all props to Counter wrapper', () => {
const wrapper = shallow(<App />);
let counterWrapper = wrapper.find(Counter);
expect(counterWrapper.props().counter).to.equal(0);
wrapper.setState({ counter: -1 });
counterWrapper = wrapper.find(Counter);
expect(counterWrapper.props().counter).to.equal(-1);
});
Could someone help me? Can I do this with jest? Or I do need a third-party library like 'react-testing-library'?
Enzyme shallow is primarily intended for isolated unit tests like the one that was listed, only the implementation of tested unit is asserted. It offers some instrumentation (and also non-compliant behaviour) that doesn't exist in React itself.
react-testing-library is primarily intended for blackbox functional tests, the effects of units on resulting DOM are asserted.
React's own ReactTestUtils also offer some basic functionality, a subset of Enzyme and react-testing-library features, including shallow renderer for isolated tests.
It's possible to do isolated tests and assert the implementation without Enzyme, this results in boilerplate code. The approach isn't specific to testing library, it could be achieved with any renderer, including React's test renderer's snapshot testing. Everything (Counter component) but tested unit (App component) should be mocked with Jest, e.g.:
it('passes all props to Counter wrapper', async () => {
const CounterMock = jest.fn(() => 'Counter');
jest.mock('./counter-module', () => CounterMock);
const App = await import('./app-module');
// render <App/> and assert that the result is <div>Counter</div>
expect(CounterMock).toBeCalledWith({ counter: 0 });
});
I've tried everything with enzyme, however, I can't find the correct way of testing these properties below. Keep in mind that this component is wrapped in a dummy Provider component so that I can pass the necessary props (i.e. Store) down for mounting purposes.
1) After mounting, a property is set on the instance (e.g. this.property)
2) An event listener has been added
3) On the event listener, someFunction is being called
class SampleComponent extends Component {
componentDidMount() {
this.property = 'property';
window.addEventListener('scroll', this.someFunction, true);
}
someFunction = () => {
return 'hello';
};
render() {
return <h1>Sample</h1>;
}
}
export default EvalueeExposureList;
Ok, I have updated my answer based on discussion with OP. The component under test has a redux provider and connected component as child therefore we are opting for the usage of enzymes shallow API.
In regards to tracking and testing the addEventListener you can use the sinon library to create a spy, which temporarily "replaces" the window.addEventListener. This grants you access to the call count as well as the arguments it was called with.
Using enzyme and mocha I created the following tests which were passing for me. The first two test covers all your cases above and for good measure I added another on how to test the output of the someFunction.
import React from 'react';
import { expect } from 'chai';
import sinon from 'sinon';
import { shallow } from 'enzyme';
// Under test.
import SampleComponent from './SampleComponent';
describe('SampleComponent', () => {
let addEventListenerSpy;
beforeEach(() => {
// This replaces the window.addEventListener with our spy.
addEventListenerSpy = sinon.spy(window, 'addEventListener');
});
afterEach(() => {
// Restore the original function.
window.addEventListener.restore();
});
// This asserts your No 1.
it(`should set the property`, () => {
const wrapper = shallow(<SampleComponent />);
wrapper.instance().componentDidMount(); // call it manually
expect(wrapper.instance().property).equal('property');
});
// This asserts your No 2 and No 3. We know that by having
// passed the someFunction as an argument to the event listener
// we can trust that it is called. There is no need for us
// to test the addEventListener API itself.
it(`should add a "scroll" event listener`, () => {
const wrapper = shallow(<SampleComponent />);
wrapper.instance().componentDidMount(); // call it manually
expect(addEventListenerSpy.callCount).equal(1);
expect(addEventListenerSpy.args[0][0]).equal('scroll');
expect(addEventListenerSpy.args[0][1]).equal(wrapper.instance().someFunction);
expect(addEventListenerSpy.args[0][2]).true;
});
it(`should return the expected output for the someFunction`, () => {
const wrapper = mount(<SampleComponent />);
expect(wrapper.instance().someFunction()).equal('hello');
});
});
It may be worth noting that I run my tests on node, but I have a jsdom setup in my mocha configuration, which is probably the candidate responsible for creating the window.addEventListener in for use in my test environment. Are you running your tests via the browser or node? If node you may need to do something similar to me.