Home Testing of web services ReactJS – my understanding of testing

ReactJS – my understanding of testing

by admin

As my boss might say, everyone is a rock. Since I couldn’t think of anything smarter, that’s where we’ll stop.
Actually this material does not necessarily claim to teach others anything. Perhaps I’ll gather enough good stuff in the comments to teach myself instead). This will describe the problem, how I see it solved now, and why.
I’ve been working with reakt for a couple of months now, mostly my backgrounder is backend, and it’s kinda like eliminating illiteracy here. Redux and other auxiliary concepts have not yet been brought into the equation.
The task was to try and test a small application that was made. Well, all sorts of services in the usual style can be tested by some jasmine. It is more difficult with components, if you want to stay within the concept of unit testing too. In idea it is usual to test contracts, not applications, that is tests should have a view "you have pressed the button – the application tried to do something".
That’s it, I’m done with the intro.
The reaction of a component to user actions (or timers or something else) can be twofold: it can make some changes within itself, or it can change something else (go to another page or another part of the SPA, perform a file download…). In ReactJS the "inside itself" is properly implemented through either changing the component state, or notifying the parent components when some event happens (so that the parent can re-render the component with other props). And changes "outside itself", let’s assume, are also implemented by calling some function, which the parent sends to the component: it can be in the classical sense, the event handler, or it can be a "delegate" to perform an action (leaving to another page, for example). My impression so far is that this is roughly how it’s usually done under ReactJS.
So, testing of reactions comes down to "simulate a user action, check which methods (from those injected into it) and with which parameters the component called". There’s the point, though, that we don’t inject setState into component; so, either we need to think up how to intercept setState (in general, I think the same jasmine will handle it), or instead of setState give component some other way to change its state. We’ll come back to this later on – it will become clear what for.
Another question is how to simulate user actions. I read the internet a little bit and found 1) here – they suggest that you should call methods like increment in the public api of a component and call them via component.getInstance(), 2) here’s – and there they look for controls in the built tree by some criteria and click them. The second way is bad because the test is tied to the markup where it actually is not necessary for the logic of the test (and creates unnecessary dependence on the markup this way, and distracts from the essence of the test), and also because it’s not quite correct (in reality user actions often cause several events at once, and even if a component interested in only one of them, do "partial simulation" somehow ugly). The first one is bad because firstly there is no reason to output increment to public api (in general, the component shouldn’t have any api except for the one we need, including for injection of props) and secondly, if onClick contains something more complicated than {increment} – for example {() => if (this.state.count > 0) decrement();} – then we don’t test this extra wrapper in this way.
So far, it seems to me that to get a reasonable answer here you have to pick the right point of view. Non-trivial handlers inside markup should be abandoned; they are tempting from the point of view of brevity, allowing to immediately translate "internal" interpretation of the event (click on the + button) into interpretation in terms of component destination (call increment counter), without building a separate method for it, but it hits the testability. In the example with increment, it’s wrong to imitate user actions by calling increment, because increment is a user action already expressed in terms of the component’s purpose, while contracts for components (which we’re testing) usually look like "when you press so-and-so button, something happens"; so it’s "press + button" event that’s part of the contract, not "command to increment the counter".
And since we recognize events as part of the contract, suddenly they have the right to be public. That is, in fact, the component splits into markup and controller, and we test them separately; and therefore the controller has its own api, which must be visible from the markup and is therefore public. And if we think of the class (upon which the component is built) as the controller’s job, then this is the class that can publish this api; that is, it makes sense to call those "control signals" of the controller through "getInstance().onPlusButtonClick()". True, in the general case then you have to create an event object (and from a perfectionism meeting, a more or less correct one) to be served as input. But in many cases even this can be avoided: even if you shouldn’t write "translation" of events directly in the markup, but things like (event) => onTextChange(event.value) probably look harmless enough not to test them, and then you can feed not event but text directly to signal input.
But maybe this is all a daydream, and if your components are small and simple, it’s easier not to bother and write directly in the markup whatever you want, and then find buttons and click them. It seems that what I suggested above should not cause significant discomfort, but in essence the decision made here will not affect anything except tests, and perhaps the attractiveness of tests is not so important – you can go in the direction of "less restrictions on developer freedom". Let’s see what the progressive public will write 🙂
But the generated markup is also part of the component contract =) But here again the question is to what extent? In part, the markup is just an implementation. It’s still not clear to me how to separate the important from the unimportant in the markup (well, aside from putting design specifics into CSS). Basically, if the whole markup is considered a contract, then jest suggests regression testing against a benchmark; but if we know which parts of the markup are important to us, we can test them specifically by analyzing the generated DOM. Except it would be a very verbose analysis. So far I’m leaning towards comparing with the benchmark, although it’s not very clean.
Figuring out how to analyze a markup isn’t the only task we need to do to test a markup. We’re testing what a component looks like at some point in time – after some action. But it’s not very correct to run the actions themselves during the same test (at least – even if you know how to do it). It seems to me that since markup is a product of calculating state and props functions, you should just give it as input a state and props that simulates the execution of these previous actions; that is, the test is formulated as "check how markup looks when the second tab is selected in the application and it shows the second page of the reference data set in the table". And here we have a question, how to describe such a state in a test: 1) how does the test know the structure of component states, because it’s part of the implementation, not the contract, 2) how it should form a correct state (should it be an uncontrolled creation of object by simply listing properties and values, or should be provided – not for tests, but for real life – some builder, which guarantees the correctness of the formed state by the component).
About privacy, there is again the question of point of view. If a component’s state is a black box, then indeed the parent either doesn’t deal with the state of the children at all, or it gives the children access to some function that allows them to read or change the state, but again, it doesn’t know about the state’s composition. But another approach is possible, similar to the one that is used in .Net in the MVVM paradigm: the state in this case is some ViewModel that describes the view and components of this view are mapped to the parts of this model that interest them. Then the structure ViewModel is self-valuable: controlling it, we control the state of components, reading it, we read the state saved by controllers. And then it becomes natural to make properties of ViewModel public – not in the sense that all child controls freely access the model and read and write anywhere from anywhere, but in the sense that on some upper level where ViewModel is stored, we know how it looks like (what properties are described and in what format) the state of each component and can set in the test such state where we want to check how the component is rendered.
Above at the end of part 1 I wrote about a variant, when instead of setState a component uses some other mechanism, and the described model is just a good example of this approach. Somewhere a ViewModel is stored, child components are given parts of it in props, and in order for a component to affect some property X from ViewModel, it can be given some f(x) in props called setX, which essentially makes viewModel.prop1 := x. Of course, f(x) should actually be trickier – not just synchronously change state and all, but act somehow similar to setState. As one option, you could probably have a real state on the top component level and let the children descend accessors, which will be implemented through the setState of this top component. Another option is some well-known external storage mechanism like Redux.
But how to form a guaranteed correct state – that’s something I haven’t thought through yet. If the question was only about testing, I wouldn’t worry about it. But since ViewModel has publicly known structure and allows changes to it from outside of view (in our case – from tests), then from formal point of view it would be nice to provide some methods of manipulating state so that they get no more parameters than needed and guaranteed to form consistent state. Something like gotoFirstPage(), which itself knows that current page number should become 1 and also knows that "previous page number" should be set to null in this case (just a made-up example).

You may also like