How could my boss say, all rock. Since I didn’t think anything smarter, we’ll stop at that.
Actually this materialchik does not necessarily pretend to teach something to others. Perhaps I will gather good enough in the comments to learn myself instead) The task will be described here, how I am presenting its solution now and why.
I have been working with the reactor for a couple of months, basically, my background is backing, and here it’s like eliminating illiteracy. Redux and other auxiliary concepts have not yet been introduced into the equation.
')
There was a task to try to test a small application made. Well, any services quite in the usual style, you can test some jasmine. With components it is more difficult if you also want to stay within the framework of the concept of unit testing. The idea is to test the accepted contracts, and not the implementation, that is, the tests should have the form "poked a button - the application tried to do something."
All right, I'm tied up with the introduction.
one.The component’s reaction to user actions (or timers, or something else) can be twofold: it can make some changes inside itself, and it can change something else (go to another page or another part of the SPA, download the file. ..). In the case of ReactJS, “within itself” is correctly implemented through either changing the state of the component or notifying the parent components of the occurrence of some event (so that the parent can re-render the component with other props). And the changes "outside of yourself", too, we will assume that they are implemented by calling a certain function that the parent lets down to the component: in the classical sense, this can be an event handler, or it can be a "delegate" to perform an action (going to another page, for example). I still have the impression that something like this is usually done under ReactJS.
It turns out that testing of reactions is reduced to “imitated the user’s action, checked which methods (from those injected into it) and with what parameters the component caused”. There really is a moment here that we do not inject the setState into the component; that is, you either need to figure out how to intercept setState (in general, it seems to me that the same jasmine will cope with this), or instead of setState, give the component some other way to change its state. To this we will return a little lower - there it will become clear why.
2The question still remains, and how, in fact, to imitate user actions. I read the Internet a little and found 1)
here they offer to show methods like increment to the public api component and call them via component.getInstance (), 2)
now - and there they are looking for controls in a constructed tree by some criteria and they are pressed . The second way is bad because the test is attached to the markup where it is generally not needed for the test logic (and creates an extra dependence on the markup in this way, and distracts from the essence of the test), and also because it is not quite correct (actually, a user is often triggered by several events at once, and even if only one component is interested in a component, it is somehow ugly to do an “incomplete imitation” The first one is bad because, firstly, there is no reason to output the increment into public api (the component generally does not have to have any api other than that which the reactor needs, including injection of the props), and secondly, if there is something in onClick something more complicated than {increment} - for example, {() => if (this.state.count> 0) decrement ();} - then we will not test this additional binding.
For the time being, it seems to me that in order to get a reasonable answer, one must choose the right point of view. Nontrivial handlers inside the markup should be discarded; they are tempting from the point of view of brevity, allowing on-site translation of the “internal” interpretation of the event (click on the + button) into interpretation in terms of the component's assignment (call to increase the counter), without dividing a separate method for this, but it hits testability. In the example with increment, it is wrong to imitate user actions by calling increment, since increment is a user action already expressed in terms of the component’s purpose, and contracts for components (which we check) usually in terms of terms look like “when you press such a button, that "; therefore, part of the contract is precisely the event “pressing the + button”, and not “a command to increase the counter.”
And since we recognize events as part of a contract, then suddenly they have the right to be public. That is, in fact, the component is divided into markup and the controller, and we test them separately; and therefore the controller has its own api, which must be visible from the markup and therefore public. And if we consider a class (on the basis of which a component is created) precisely as a controller task, then it is this class that can publish it; that is, it is quite reasonable to call these "control signals" of the controller via "getInstance (). onPlusButtonClick ()". However, in the general case, then you need to create an event object (and from the perfectionism ideas, more or less correct), which will be submitted to the input. But in many cases this can be avoided: let “translate” events directly in the markup should not be written, but such things as (event) => onTextChange (event.value) look, perhaps, innocuously enough not to test them, but then the signal can be fed to the event not just the text.
But perhaps this is all heading up in the clouds, and if your components are small and simple, it's easier not to bathe and write anything you like directly in the label, and then find the buttons and poke them. It seems that what I suggested above should not bring tangible discomfort, but in essence, except on tests, the decision made here will not affect anything, and the beauty of tests is probably not that important - you can go the way “less restrictions freedom of developers. " Let's see what the progressive public will write :)
3But the generated markup is also part of the component contract =) But here again the question is - to what extent? In part, markup is just an implementation. It is still unclear to me how to separate the important from the unimportant in the markup (well, apart from making the specifics of the design in CSS). In principle, if the whole markup is considered a contract, then jest offers regression testing by comparing with the standard; but if we know which parts of the markup are important to us, we can check them by analyzing the generated DOM. Here are just a very verbose analysis work. So far, I still tend to compare with the standard, although it is not very clean.
Developing a markup analysis method is not the only task that needs to be solved in order to test the markup. After all, we are testing how the component looks at some moment of work - after some actions. And the actions themselves are not very correct to launch during the same test (at least - even if it is clear how to do it). It seems to me that since the markup is the product of the computation of the state function and the props, then you should simply give it a state and a props, which simulates the performance of these previous actions; that is, the test is formulated as “check how the markup looks in the state when the second tab is selected in the application and the table shows the second page of the reference data set”. And here the question arises, how to describe such a state in a test: 1) where does the test know the structure of the component state, is it part of the implementation, not the contract, 2) how it should form the correct state (should this be an uncontrolled creation of an object by simple enumeration properties and values, or should be provided — not even for tests, but for real life — some kind of builder, with whom the component guarantees the correctness of the state being formed).
As for privacy, the point of view again arises. If the state of the component is a black box, then the parent really either doesn’t deal with the state of the child components at all, or it gives the children access to some function that allows reading or changing the state, but at the same time does not know about the composition of the state itself. But another approach is possible, similar to the one used in the .VVM paradigm in .Net: the state in this case is a kind of ViewModel model describing the view, and the components of this view are tied to the parts of this model that interest them. Then the ViewModel structure is self-valued: by controlling it, we manage the state of the components, reading it - read the state saved by the controls. And then it becomes natural to make the ViewModel properties public - not in the sense that all child controls freely access the model and read from anywhere and write anywhere, but in the fact that at some top level where the ViewModel is stored, we know what it looks like (what properties it describes and in what format) the state of each component, and we can, including in the test, set a state in which we want to check how the component is rendered.
Above, at the end of part 1, I wrote about the variant when instead of setState the component applies some other mechanism, and the model described here is just a good example of this approach. Somewhere the ViewModel is stored, the child components are given parts of it in props, and so that the component can affect some X property from the ViewModel, it can be passed to the props called setX some f (x), which essentially makes viewModel.prop1 : = x. Of course, in fact, f (x) should be more cunning - not just synchronously changing the state and everything, but acting somehow like setState. As one of the options, you can probably have a real state at the top level of components, and children can lower accessors that will be implemented through the setState of this top component. Another option is some well-known external storage mechanism like Redux.
But how to create a guaranteed correct state - I have not thought it through yet. If it was only about testing, God would be with him. But since the ViewModel has a publicly known structure and allows changes to be made to it from outside the view (in our case, from tests), then from a formal point of view, it would be nice to provide some state manipulation methods such that they receive no more parameters than necessary, and guaranteed to put a consistent state. Something like gotoFirstPage (), which itself understands that the current page number should be 1, and also knows that the “previous page number” should be set to null in this case (just an example invented).