📜 ⬆️ ⬇️

We increase the stability of the front-end

In continuation of the previous article about testing interfaces in Tinkoff Bank I will tell you how we write unit tests in javascript.

image


There are already a lot of articles about approaches to testing TDD and BDD, so I’ll not talk more about their features again. This article is more likely for newbies or for developers who just want to start writing tests, but more experienced developers may also be able to find useful information for themselves.
')

A few words about the development


First about how we design the front-end at Tinkoff Bank so that you know about the tools that make life easier for us.

Stages of the development process

  1. Formulation of the problem
  2. Writing technical specifications
  3. Design development
  4. Code Development and Unit Tests
  5. QA testing and debugging
  6. Running in combat environments

Before the task gets to the developer, it passes the specification stage. At the output, in the ideal case, the task in JIRA + is described in WIKI + ready-made designs. After that, the task goes to the developer, and when development is completed, the task is transferred to the testing department. If it is successful, the release goes public.

In our work we use the following tools (their choice, including, is justified by simplifying the process of developing and interacting with managers):
  1. Atlassian Jira;
  2. Atlassian Stash;
  3. Atlassian Confluence;
  4. JetBrains TeamCity;
  5. JetBrains IntelliJ Idea.

All Atlassian products integrate well with each other and with TeamCity.

As Git Branch Workflow, we decided to use the familiar Gitflow Workflow, which you can read more about here .

In a few words, it all comes down to the following:
  1. There are two main branches of master, which corresponds to the latest release, and develop, which contains all the latest changes;
  2. For each release, a release branch is created from the develop branch, for example, release-1.0.0;
  3. further edits to the release merdzhatsya in the release branch;
  4. after a successful release, release-1.0.0 is merged into the master branch and can be deleted.

Atlassian Stash allows you to set up a similar workflow in a couple of clicks and work comfortably with it, allowing you to:
  1. check the names of branches;
  2. prohibit merge directly to parent branches;
  3. automatically merge pull requests from a release branch to a develop branch, and in case of conflicts, automatically create a branch to resolve the conflict;
  4. disable merge pull request if the task in jira is in an incorrect status, for example, in “In Progress” instead of “Ready”.

It is also very convenient to configure the integration of Atlassian Stash with TeamCity. We set it up so that when creating a new pull request or making changes to an existing one, TeamCity automatically starts building and testing the code for this pull request, and in Stash we set the merge ban setting until the build and tests are completed successfully. This allows us to keep the code in the parent branches in a healthy state.

Some theory


Front-end testing at Tinkoff Bank covers only critical parts of the code: business logic, calculations, and common components. The visual part of the UI is tested by our QA department. When writing tests, we are guided by the following principles:
  1. the code must be modular, not monolithic, since tests are written for a given unit;
  2. weak connectivity between components;
  3. Each unit must solve one problem, and not be universal.

If one of these principles is not met, then the code needs to be improved to make it easier to test.

Best of all, if the components are weakly interconnected, but this is not always the case. In this case, we use the decomposition method:
  1. we test each component separately and make sure that the tests pass, and the components work correctly;
  2. We test the dependent component apart from other modules using Mocks.

Since we are testing the behavior, describing the ideal work of the code, it is necessary to develop a standard for the behavior of the code, and also to provide for possible situations in which the code will break. That is, the test should describe the correct behavior of the code and respond to erroneous situations. This approach allows you to create a code specification at the output and eliminate the risk of breakage during refactoring.

With this approach, development is reduced to three steps:
  1. write a test and see how it faylitsya;
  2. We write the code so that the test is successfully passed;
  3. refactor code.



Developer Toolkit


To write tests, you must select test runner and test framework. Our development process uses the following technology stack:
  1. Jasmine BDD Testing framework;
  2. SinonJS;
  3. Karma;
  4. PhantomJS or any other browser;
  5. NodeJS;
  6. Gulp.

We run tests both locally and in CI (TeamCity). In CI, tests are run in PhantomJS, and reports are generated using teamcity-karma-reporter.

Practice


So let's get down to practice. I have already done a small draft of the project, the code of which can be found here . What to do with it, I think everyone should be clear.

I will not describe how to set up Karma and Gulp, everything is described in the official documentation on project sites.

We will run Karma in conjunction with Gulp. We will write two simple tasks - to run tests and watch to track changes with auto-start tests.

Jasminebdd

Jasmine has almost everything you need to test UI: matchers, spies, setUp / tearDown, stubs, timers.

Let us dwell on matchers:
toBe - equal
toEqual - identity
toMatch is a regular expression
toBeDefined / toBeUndefined - check for existence
toBeNull - null
toBeTruthy / toBeFalse - true or false
toContain - the presence of a substring in the string
toBeLessThan / toBeGreaterThan - comparison
toBeCloseTo - comparison of fractional values
toThrow - interception of exceptions

Each of the matchers may be accompanied by an exception not, for example:
expect (false) .not.toBeTruthy ()

Consider a simple example: let's say you need to implement a function that returns the sum of two numbers.
The first thing to do is to write a test:
describe('Matchers spec', function() { it("should return sum of 2 and 3", function() { expect(sum(2, 3)).toEqual(5); }); }) 


Now we will make the test pass:
 function sum(a, b) { return a + b; } 


Now the example is a bit more complicated: we will write the function for calculating the area of ​​a circle. Like last time, we write a test, and then a code.
 describe('Matchers spec', function() { it("should return area of circle with radius 5", function() { expect(circleArea(5)).toBeCloseTo(78.5, 1); }); }) 


 function circleArea(r) { return Math.PI * r * r; } 


Since we have tests, we can, without fear of refactoring the code, use the Math.pow function:
 function circleArea(r) { return Math.PI * Math.pow(r, 2); } 


Tests passed again - the code works.

Matchers are quite easy to use, and there’s no point in discussing them in more detail. Let's move on to more advanced functionality.

In most situations, you need to test the functionality that requires pre-initialization, for example, environment variables, and also allows you to get rid of code duplication in the specs. In order not to perform this initialization for each Spec, setUp and tearDown are provided in Jasmine.

beforeEach - performing the actions required for each Spec
afterEach - performing actions after each spec
beforeAll - perform actions before running all Specs
afterAll - performing actions after performing all Specs

In this case, the sharing of resources between each test cases can be done in two ways:
  1. use a local variable for the test case (code);
  2. use this;

To better understand how you can use setUp and tearDown, I’ll immediately give an example using Spies.
Code
 describe('Learn Spies, setUp and tearDown', function() { beforeEach(function(){ this.testObj = {// this    myfunc: function(x) { someValue = x; } } spyOn(this.testObj, 'myfunc');// Spies }); it('should call myfunc', function(){ this.testObj.myfunc('test');//  expect(this.testObj.myfunc).toHaveBeenCalled();//,  myfunc  }); it('should call myfunc with value \'Hello\'', function(){ this.testObj.myfunc('Hello'); expect(this.testObj.myfunc).toHaveBeenCalledWith('Hello');//,  myfunc   Hello }); }); 


spyOn essentially creates a wrapper over our method that calls the original method and stores the arguments of the call and the method call flag.
These are not all Spies features. Read more in the official documentation.
Javascript is an asynchronous language, so it’s hard to imagine code that needs to be tested without asynchronous calls. The whole point comes down to the following:
  1. beforeEach, it and afterEach accept an optional callback, which must be called after making an asynchronous call;
  2. Specs will not be executed until the callback starts, or until the end DEFAULT_TIMEOUT_INTERVAL

Code
 describe('Try async Specs', function() { var val = 0; it('should call async', function(done) { setTimeout(function(){ val++; done(); }, 1000); }); it('val should equeal to 1', function(){ expect(val).toEqual(1);//    done,    DEFAULT_TIMEOUT_INTERVAL }); }); 


Sinonjs

We mainly use SinonJS to test the functionality that makes AJAX requests to the API. In SinonJS for testing AJAX there are several ways:
  1. create a stub on an AJAX call function using sinon.stub;
  2. use fake XMLHttpRequest, which replaces native XMLHTTPRequest with fake;
  3. create a more flexible fakeServer that will respond to all AJAX requests.

We use a more flexible approach fakeServer, which allows you to respond to AJAX requests with pre-prepared JSON mocks. So the logic of working with the API can be tested in more detail.
Code
 describe('Use SinonJS fakeServer', function() { var fakeServer, spy, response = JSON.stringify({ "status" : "success"}); beforeEach(function(){ fakeServer = sinon.fakeServer.create();// fake server }); afterEach(function(){ fakeServer.restore();// fake server }); it('should call AJAX request', function(done){ var request = new XMLHttpRequest(); spy = jasmine.createSpy('spy');// Spies request.open('GET', 'https://some-fake-server.com/', true); request.onreadystatechange = function() { if(request.readyState == 4 && request.status == 200) { spy(request.responseText);//  done(); } }; request.send(null); //    fakeServer.requests[0].respond( 200, { "Content-Type": "application/json" }, response ); }); it('should respond with JSON', function(){ expect(spy).toHaveBeenCalledWith(response);//  }); }); 


In this example, the easiest way to respond to requests was used, but SinonJS allows creating more flexible fakeServer settings with specifying the url map, method and response, that is, it provides the ability to fully emulate the API operation.

PS


Write tests cool and fun. Do not think that with this approach, the development becomes more complicated and stretches in time.

There are several advantages to testing code:
  1. the code covered with tests can be refactored without fear of breaking it;
  2. the output provides a code specification expressed by tests;
  3. development is faster, since there is no need to manually check the performance of the code - for this, tests and test cases have already been written.

The most important thing: remember that tests are the same code, and therefore, you must be extremely careful when writing them. Incorrectly running test will not be able to signal an error in the code.

Resources


  1. JasmineBDD ;
  2. SinonJS ;
  3. Karma ;
  4. Book Testable Javascript ;
  5. The book Test-Driven Javascript Development ;
  6. Gitflow Workflow ;
  7. Code

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


All Articles