Ovidiu Chereshes, the author of the article, the translation of which we are publishing today, wrote thousands of tests of user interfaces. He says that testing should instill in the developer confidence that his programs work exactly as he expects, and that they will continue to do their work even after they are modified and expanded. However, testing user interfaces seldom gives confidence. Instead, it often leads to frustration and thoughts about the unproductiveness of program work.
He says that he raises the question of testing interfaces written in React, since he has encountered problems typical of this process years before the wide distribution of this framework, and spent a lot of time solving them. He thinks that he was able to find the tools and working methods that make testing React components as simple as creating them.
He will begin the story with two basic principles, understanding of which is important for finding the right approach to testing, further examples will be given and one useful tool will be presented that can make life easier for those who have to test React applications.
Principle # 1: consider the failure of abstraction component = f (props, state)
In theory, the construction given in the title looks beautiful, however, everything goes awry, if, relying on this abstraction, try to test the real components. As soon as you try to separately load the component, the following becomes clear:
')
component=f(props, state, context)
Or, more precisely, it turns out like this:
component=f(props, state, Redux, Router, Theme, Intl, etc)
However, even such a construction is still far from reality. Namely, this is what it is about:
component=f(props, state, context, globals)
Of course, these are not global variables, perhaps only some monster will use them. I mean global APIs.
component=f(props, state, context, fetch, localStorage, window size)
As you can see, the reality is much more interesting than the theory.
Testing React components is a matter that involves the constant solution of complex problems. However, very few say about this. Official examples of testing are shown on the simplest components, but as for the "monsters" of the world of components, they mostly do not write anything. Today, together we will look at these monsters in the eye and talk about a new simple API that allows you to "pacify" any component and perfectly combines with existing tools like Jest and Enzyme.
The above text is a fragment of a speech with which I once wanted to get to the conference. I did not get there (it is appropriate to include sad music here), but every word here matters and I still want to talk about it.
The following examples, by default, use Jest and Enzyme, but neither the one nor the other is necessary to apply the techniques that I will discuss.
Simple sample test
The usual test case you can come across is quite simple. Perhaps this is a button with a callback function:
const UnrealisticComponent = ({ onReply }) => ( <button onClick={() => onReply('Ja')}>Alles gut?</button> )
Now let's check if the component works as expected from it:
const onReply = jest.fn(); const wrapper = shallow(<UnrealisticComponent onReply={onReply} />) test('kindly asks if everything is alright', () => { expect(wrapper.text()).toBe('Alles gut?') }) test('receives positive response upon click', () => { wrapper.find('button').simulate('click') expect(onReply).toHaveBeenCalledWith('Ja') })
As you can see, everything is very simple and clear, and the component is quite trivial. However, everything changes if you try to test this component. Imagine the simplest component used for authentication, which has a field for entering a username and a submit button. In the field enter the name, click on the button, this causes the query
fetch
with the entered data. The request is completed, the name is transferred to the Redux storage and cached in
localStorage
. In general, all this is also not so difficult.
Now we will try to send this component to the test rendering. Unfortunately, the test will not even start. We will see the following:
Could not find "store" in either the context or props of Connect… ReferenceError: fetch is not defined ReferenceError: localStorage is not defined
Usually at such times programmers call on the higher forces for help.
Here you can point out that this example is conditional, and that in a serious application there would be some kind of abstraction for authentication, which should be tested. However, as we will soon see, this has nothing to do with component testing. We need the ability to write tests that, when used for multilevel tests of an evolving application, do not have to be rewritten all the time.
At the moment, based on your experience, you can make two serious observations to the above example:
- Are you really testing “containers”?
- Do your components interact with global APIs (fetch, for example)?
Yes. Why not? And besides, I do not use shallow rendering. This brings us to the following principle.
Principle number 2: remember the dangers of auxiliary elements of the application, linking its components together
Components, even when using a simple component model of React, are complex entities that are far from normal functions. Therefore, there is a temptation to allocate them into separate modules, making them as isolated as possible from the rest of the systems. However, the smaller these modules, the more code is needed to tie the project together, and the more room is open for integration errors.
For example, we usually export components without their wrappers, in order to test only the components themselves. This state of affairs is considered normal, as higher-order components have already been tested and we can test their internal mechanisms. We test reducers, actions and selectors. What can I say, you should even
mapStateToProps
and
mapDispatchToProps
to ensure that the project code is as completely as possible. I, at one time, followed a similar approach, but once I understood its main shortcomings. Here they are:
- Testing takes more effort than implementation. Here you can only wish good luck to new developers. On the first day, they will write their first component, and it will take them another three days to write tests for it.
- Testing prevents refactoring. For me, this is probably the main problem. In the process of writing code, we often, as they say, dig a grave for ourselves, and there is no better way to turn a typical project into a rigid unwieldy structure than meticulous testing of each function.
- Increases the likelihood of errors. It is enough to forget to test some trifle (like some kind of selector) and the whole component may not work properly, although the tests it passes are normal.
- Unsynchronized input and output data. A component can correctly respond to some properties, and
mapStateToProps
can produce the correct data based on a certain state, but the exported component will fail if its input and output data do not match.
It is tempting, in search of comfort, to turn to neat little modules. But if we test only the basic parts of the application, we lose sight of the complexity of the project - the very connecting parts, thanks to which it works. In addition, the programmer may succumb to the attractive idea of ​​transferring concern for testing the integration of components to other professionals, such as those involved in quality control. However, end-to-end testing is a “airbag” that operates at a much higher level.
The fewer subsystems that bind the application together, are left outside of the component tests, the more I am sure that the testing is correct, and, besides, the reusability of the modules that make up the application. A real module is one that we plan, for example, to transfer and reuse to someone. It carries a certain meaning for the end user. It is precisely such constructions that we need to test, and not what is easier to test.
However, what about the complexity of setting up a test environment?
About the component testing environment
Most of the code in the test files and most of the effort involved in preparing them is set up by the providers and the layout data for the “intelligent” components. Writing statements is, after connecting the components, a trifle. In this regard, I have a question: how to simplify the setup of the test environment and give developers the opportunity to deal, mainly, in creating statements to test the behavior of components? If only there was a simple way to simulate the states of the components, supplying some conditional data to their inputs ...
And, in fact, there is such a way. Infrastructure elements (fixtures) of the
Cosmos library are designed to simulate any input values ​​and render the component in any combination of states. And, since all this has been used for a long time only in the Playground interface, it becomes obvious that the use of Cosmos, in addition, can replace the complex setup of the test environment. Before us, as a result, something like the sentence “two in one”.
Cosmos playgroundCosmos library features
First, let's remember what the JSX tags look like (or how the
React.createElement
command
React.createElement
).
<Button disabled={true}>Click me maybe</Button>
Before us is an ad. In this case, it reads like this: we need a
Button
element (button) with
{ disabled: true }
properties and a
Click me maybe
child object.
Cosmos infrastructure elements can be thought of as something like the “steroid-charged” components of React. In addition to information about the components, properties, and child objects, such elements can receive data about the local state, the state of Redux, or the URL of the router. In addition, infrastructure elements can mimic the
fetch
,
XHR
or
localStorage
.
All of these features are available through plug-ins that make the infrastructure elements a platform for simulating component states.
Infrastructure elements are ordinary JS objects, like this:
{ props: {} url: '/dashboard', localStorage: { name: 'Dan' }, reduxState: {}, fetch: [ { matcher: '/api/login', response: { name: 'Dan' } } ] }
After you get used to writing such code, you will not only get component-oriented development tools, but also greatly simplify your task of writing component tests. All this will help you recently released
Cosmos Test API .
Here, for example, looks like testing the Cosmos interface using the Cosmos Test API.
Testing at CosmosBut how does the work with the API look like? Before trying it, it will be useful to read the
documentation .
import createTestContext from 'react-cosmos-test/enzyme'; import fixture from './logged-in.fixture'; const { mount, getWrapper, get } = createTestContext({ fixture }); beforeEach(mount); test('welcomes logged in user by name', () => { expect(getWrapper('.welcome').text()).toContain('Dan'); }); test('redirects to home page after signing out', () => { getWrapper('.logout-btn').simulate('click'); expect(get('url')).toBe('/'); });
All this is done so that, with a minimum number of auxiliary elements, to comply with the standard approaches to testing. I'm not quite happy with the naming scheme used here, this “context”, but I am pleased to inform you that the API has been tested by many programmers and tested at ScribbleLive, which I am currently consulting.
And now, so to speak, under a curtain, I will tell a couple of interesting cases from my practice.
So, even in already quite mature projects with an established code base, I usually write specialized prototypes when implementing new application components. Instead of connecting, from the very beginning, to the Redux repository, I can start working from a local state and bring the new functionality to the working level. More than once, I went to a fully tested prototype and then ported the local state to Redux, without rewriting a single test.
Once, the
Form
component without a state has been used on many screens. This is a pleasant abstraction, but each instance required a data mapping template and procedures associated with its life cycle. The project needed refactoring. I created an abstraction for repeating operations with these forms and called it
FormConnect
. After that, I reworked more than a dozen screens without rewriting a single test. Why? Because, in terms of software work with them, these screens have not changed.
Results
As you can see, Cosmos API can significantly improve the situation in the field of complex testing of components of React-projects. In addition, the pursuit of such a system architecture, during the implementation of which the tests, when making changes to the system, does not have to be constantly rewritten, allows for project flexibility and user experience. We hope that the ideas of the author of the material and the Cosmos API will be useful to you in testing React-applications.
Dear readers! How do you test projects created using React?