📜 ⬆️ ⬇️

TDD React.js Applications

image
Hetzel edition of 20000 Lieues Sous les Mers

A note about how we are “reanimators” in terms of tests (who is familiar with the work of Howard Phillips Lovecraft, he will understand).

In continuation of the testing and testing topics , I would like to write a little about our approach, how it looks on our Single Page Applications (SPA) , written in React.js, how Test-Driven Development (TDD) helped us and why we came to the fact that gearboxes and API-services to cover the tests also need.
')
Immediately I will say that if you expect to see jest , snapshot testing or storyshots here , then immediately close this note. If you expect to find here something from fresh libraries or approaches, then also close immediately. We did not use any of the above. Perhaps we will enter the new project with these tools, but for now it turned out the way it did.

To how our tests look now, we came by ourselves, although many of these techniques are described on various websites and forums. As an addition, I will provide these links below.

Instruments


image

We have a very standard and, one might say, gentlemanly toolkit.

To run and describe (in the BDD style) tests, we use the mocha library. Stubs and spies are provided to us by sinon , we use function-checks from the chai library, we mount GUI elements with the help of the enzyme . To create functional or end-to-end tests, we use the protractor c jasmine framework .

Unfortunately, we did not have adequate emphasis on the pentests , the only thing we used in this part to search for vulnerabilities is the nsp module and nodesecurity.io . However, we did not find anything on the client, but we found something in the server modules:

image

Also, one of the testing steps is to check the format and style of the code using the eslint tool.

Component-Driven Development through Test-Driven Development


In development and testing, we still adhere to the famous test-first principle. And, as a rule, many client developers have a question about how to write tests that check the HTML structure of a document, when this document does not exist yet, when you still have no idea what tags and styles you will have to use in the development process.

Modern client application development is based on the concept of a component . Any layout designer, in the process of decomposition, is divided into composite and fundamental components. When we talk about TDD on the client, we mean the component level, it is not very important for us whether this GUI element is implemented with the p tag or the div tag, it is important that it is implemented. Those. not having yet the implementation of the following composite component (page):

image

we can already start with a test that would check for the presence of this component as such, as well as its internal skeleton:

image

Do not forget that tests are also part of your application, and in this part, no less than in others, the principles of “Don't Repeat Yourself” (DRY) , “You ain't gonna need it” (YAGNI) , “ Keep it simple, stupid ”(KISS) and clean code .

Any of our test files begins with a description of the factory method that mounts the component under test and hangs spies for possible actions that the user can take when working with this component. Most often this applies, of course, to components that are subscribed to a change in data storage. Actions, conditionally, can be divided into state-changing storage (they, in turn, are divided into persistent and non-persistent) and page navigation.

image

For convenience, the factory method returns mounted, using the enzyme library, components and action functions that are replaced by sinon spies. Since this is a composite component that is subscribed to the repository, we apply one trick in order not to emulate the repository in the component file, we export it in two ways:

image

The first one is for tests, and the second one is wrapped up with a higher order component ( Higher-Order Component ), directly for signing on to changes in the state of the repository.

And here is the first “red” test.

image

image

Now we will implement the render method of the component, working on all the canons of red-green refactoring . After, run the test again:

image

Next, we can test the properties of nested components, their current state and characteristics depending on forward values ​​from the parent test element (logical flags, button names, test data) in more detail:

image

The next step is to check the actions. This can also be done in test-first style. We assume that the action “Get active projects” will be triggered inside the component's life cycle methods when it is mounted. At the same time, the transition action is called when the button is pressed. Those. need to simulate pressing a button. These assumptions allow us to implement the next couple of tests before the implementation of the logic itself in the component. Another trick is that for actions we use a separate imported namespace , this makes the import section in the target component cleaner and provides convenience for testing for mocks, stubs and spies.

image

TDD in action, so to speak. In the same vein, we move on until we fully implement this component. With this approach, we have developed all our components, ranging from simple and fundamental, to composite and subscribed to the repository.

“It seems to me that I like developing tests even more than writing the application code itself”
Andrei Antonyuk, developer

And yes, I almost forgot to mention, so as not to clutter up the tests with code, we bring the test data and test structures into separate files:

image

image

Functional Tests


Everything looks as if we are quite exhaustively testing the target component with modular and system tests. However, assembling a component is an artificial environment ( TestBed ), unfortunately in this way we do not imitate a real use case. For this kind of emulation, we have functional tests and, long-proven, protractor.

In the project we are actively using the popular PageObject template. Working with the page as an object that provides us with methods for accessing its elements, we can still continue to work in the style of test-first (PageObject-First / POF).

“I like the PageObject template, with it the tests become clean, simple and understandable.” Dmitry Santotsky, developer

image

At the stage of writing tests, we do not need to focus on specific selectors and the HTML structure. If we test the header, then we just call the getHeaderLabel method. In this way, we can easily work on TDD.

image

At the stage of red-green refactoring , realizing the page, we will indicate specific selectors:

image

Gearboxes through Unit Testing


It makes sense to test everything that contains some kind of logic, some conditions. Gearboxes, as a rule, are implemented as pure functions . What could be more perfect for tests? There are no side effects or dependencies that need to be handled by mocks or plugs, only input and output data. Sin does not cover them with tests!

But seriously, in the gearbox we have at least a trivial, but quite important logic. We change the state and in this case the individual parts of the repository must be checked. In addition, we quite often directly in the gearboxes can somehow process data arrays, write some conditions. In these places, people are often mistaken. So to speak, stumble out of the blue. This has been repeatedly noticed in our projects. When you write a reducer through tests, you will at least think twice what data from the incoming event it should process and what new state the storage will accept after such processing.

image

One of our frequent techniques is tests with the initial state of the repository (INIT_STATE) and with the changed (DIRTY_STATE). The first who began to divide the tests of the reducers into “from the initial state” and “with the changed state” was our developer Dima Poluyan. In his opinion, tests with altered state are even more useful than with the initial one.

image

API services and actions through Unit Testing


There is the same principle as with gearboxes. You will think over several times what endpoint you should call and how exactly to create a request for this point. In the end, the TDD approach involves writing code through tests, which means that neither services nor gearboxes can be implemented without pre-written tests.

image

You need to pass a lot of parameters that form the filter value for data retrieval. Which HTTP verb do you choose for this? Usually they think about it when they start to implement the end point of the service. With the test-first approach, you will have to think about it during the test implementation.

Of course, we, like huge idlers, did not immediately come to the conclusion that services should be covered with tests. It all started with a series of small bugs. Some endpoints were called in different places, but with a different filter parameter. The logic (condition) was written inside the service method, which, depending on the parameters, formed the necessary filter value, or sent a request without it. After some time, one of the developers added the default function parameters for the filter to the service method. It broke the place where the method was called without parameters, the data did not come.

This case was the beginning and a call for API-services to be developed through tests too.

Practically the same motives for action functions. Moreover, tests for them are written immediately along with tests for the gearbox, as if they are inseparable and go together.

image

Learning tests


Another type of test that we had to use was training tests. On one of our projects, the server was developed by an independent team of Go-developers. Unfortunately, the start of the development of the server part was planned after the start of the client part. At first, we developed API services with layout data. The data format was based on the description of the Go-developers through chats and documentation, which was supplied earlier than endpoints. With the advent of some endpoint, its study (verification and understanding of the format) and integration with it passed through training tests.

image

So we synchronized the knowledge with the documentation and insured ourselves for errors not from our side. Go-developers did not cover service endpoints with integration tests, but this was not a problem, we did it with educational tests. In the end, even before the delivery of the collected client part, we sent the PMO a report stating that all service endpoints are stable and the application will work as expected in the integration part. We can say that the training tests turned for us into a performance test (smoke test, sanity check)

image

-> Read more about training tests: Fighting the unknown

Continuous integration and delivery


image

The presence of tests in the repository is in itself useless if they are not automated in any way and are not involved in the CI / CD processes.

In our case, the tests are run as much as 3 times.

The first stage is a developer commit, which he makes from his car. When performing this operation, the git pre-commit hook is automatically triggered, which runs all the tests on the developer’s local machine. If successful, the code gets into gitlab and the merge request is sent on the code.

If everything is ok, then the branch merges into develop and then the second test run runs on the remote machine, this is the so-called gitlab runner , the functionality of which is provided by gitlab ci .

image

And the last stage is the release, the test run, which is initiated by the Jenkins tool at the end of each sprint, this is part of the CD process (we used to run the Jenkins build more often and on schedule)

Coating


We have it, but I do not believe in it. We use the nyc module, which is the same Istanbul under the hood. Why not believe? Because all tools measuring coverage are “stupid”. They check just the fact of the call in the tests of a particular function (class / method), but this approach breaks down the so-called calculator example .

Suppose you need to test the function of mathematical division. Having written one test 2/2 = 1” , the test coverage test tool will show the value 100% after launch. But is it? You see 100% and this seems to be an indicator of the fact that the work is done well, but can you simply count the call count as a good code cover by tests?

What about “2/0” ? Or “2 / null” ? Or “NaN / undefined” ?

***


Somehow, everything is simple and smooth. And where are any difficulties?

If at the end of the note it is this thought that settled in your head, then this means that we have achieved our goal. How do you test your React.js application? Write to artur.basak.devingrodno@gmail.com

Source: https://habr.com/ru/post/335020/


All Articles