Hetzel edition of 20000 Lieues Sous les MersA 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

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:

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):

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

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.

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:

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.


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

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:

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.

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:


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

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.

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

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.

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.

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.

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.

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.

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)
-> Read more about training tests: Fighting the unknownContinuous integration and delivery

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 .

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