📜 ⬆️ ⬇️

Improving JavaScript code using the StarWars API

image

Hi, my name is Raymond, and I write bad code. Well, not really bad, but I definitely don’t follow all the “best practices”. However, let me tell you how one project helped me get started writing code that I can be proud of.

Somehow on the weekend I decided to stop using the computer. But nothing happened. I stumbled upon the Star Wars API. This simple interface is based on REST, and it can be used to request information about characters, movies, spaceships and other things from the SW universe. No search, but the service is free.

And I quickly relaxed the JS library to work with the API. In the simplest case, you can request all the resources of one type:
')
//    swapiModule.getStarships(function(data) { console.log(" getStarships", data); }); 


Or get one item:

 //   ,  2 –    swapiModule.getStarship(2,function(data) { console.log(" getStarship/2", data); }); 


The code itself is in one file, I made test.html for it and uploaded it on GitHub: github.com/cfjedimaster/SWAPI-Wrapper/tree/v1.0 . (This is the original project. The final version is here ).

But then I began to overcome doubts. Can I improve something in the code? Do not need to write unit tests? Add a smaller version?

And I began to gradually make a list of what can be done to improve projects.

- while writing code, some parts of it were repeated, and this required optimization. I ignored these cases and focused on making the code work so as not to engage in premature optimization. Now I want to go back and optimize
- obviously, you need to do unit tests. Although the system works with a remote interface and tests in this case are quite difficult to do, but even a test that assumes that the remote service works 100% is better than without tests at all. And then, having written the tests, I can make sure that my subsequent code changes will not break the program.
- I'm a big fan of JSHint , and I would like to drive it through my code
- I would like to make a smaller version of the library - I think some command line utility would be suitable for this
- Finally, I am sure that I can perform unit tests, JSHint validation and minification automatically through tools like Grunt or Gulp.

And as a result, I will have a project with which I will feel more confident, a project that will be more like a Jedi than JJ Binks.

Adding unit tests

They are easiest to imagine as a set of tests that make sure that different aspects of the code work as they should. Imagine a library with two functions: getPeople and getPerson. You can make two tests, one for each. Now imagine that getPeople allows you to perform a search. It is necessary to make a third test for the search. If getPeople also allows you to divide the results into pages and set the page number from which you want to return the results - you need more tests for this. Well, you understand. The more tests, the more you can be sure of the code.

My library has 3 call types. The first is getResources. Returns a list of other API entry points. Then there is the opportunity to get one position and all positions. That is, for planets there is getPlanet and getPlanets. But these calls return the data divided into pages. Therefore, the API also supports a call of the form getPlanets (n), where n is the page number. So four things need to be tested:

- call getResources
- call getSingular for each resource
- call getPlural for each resource
- call getPlural for a given page

We have one common method and three for each resource, so the tests should be

1 + (3 * number of resources)

There are 6 types of resources, total - 19 tests. Not bad. My favorite library is Moment.js with 43,399 tests.

I decided to use the Jasmine framework for tests, since I like him and I know him best. One of the nice things is the availability of sample tests that can be changed to fit your needs and get started, as well as a file for running tests. This is an HTML file that includes your library and your tests. When opened, he chases them all and displays the result. I started with the getResources test. Even if you are not familiar with Jasmine, you can figure out what is happening:

 it("   ", function(done) { swapiModule.getResources(function(data) { expect(data.films).toBeDefined(); expect(data.people).toBeDefined(); expect(data.planets).toBeDefined(); expect(data.species).toBeDefined(); expect(data.starships).toBeDefined(); expect(data.vehicles).toBeDefined(); done(); }); }); 


The getResources method returns an object with a set of keys representing each resource supported by the API. Therefore, I simply use toBeDefined as a way to say "there should be such a key." done () is needed for asynchronous call processing. Now consider the other types. First, get one object from the resource.

 it("   Person", function(done) { swapiModule.getPerson(2,function(person) { var keys = ["birth_year", "created", "edited", "eye_color", "films", "gender", "hair_color", "height", "homeworld", "mass", "name", "skin_color", "species", "starships", "url", "vehicles"]; for(var i=0, len=keys.length; i<len; i++) { expect(person[keys[i]]).toBeDefined(); } done(); }); }); 


There is a small problem - I assume the presence of a character with identifier 2, and also that the keys describing it will not change. But it is not scary - in which case the test can be easily corrected. Do not get involved in premature test optimization.

Now return the set.

 it("   People", function(done) { swapiModule.getPeople(function(people) { var keys = ["count", "next", "previous", "results"]; for(var i=0, len=keys.length; i<len; i++) { expect(people[keys[i]]).toBeDefined(); } done(); }); }); 


Second page.

 it("     People", function(done) { swapiModule.getPeople(2, function(people) { var keys = ["count", "next", "previous", "results"]; for(var i=0, len=keys.length; i<len; i++) { expect(people[keys[i]]).toBeDefined(); } expect(people.previous).toMatch("page=1"); done(); }); }); 


Actually that's all. Now you only need to repeat these three calls for the remaining five types of resources. When writing tests, I already saw flaws in the code. For example, getFilms returns only one page. In addition, I did not handle error handling. What should I return to getFilms (2) request? An object? An exception? I do not know yet, but I will decide later.

Here is the result of the tests.

image

Using JSHint Linters


The next step is to use the linter. This is a tool to assess the quality of the code. It may highlight errors, indicate the possibility of speed optimization, or highlight code that does not comply with the recommended rules.

Initially, JSLint was used for JS, but I use the JSHint alternative. He is more relaxed, and I am also quite relaxed, so he suits me more.

There are many ways to use JSHint, including in your favorite editor. I personally use Brackets , for which there is an extension that supports JSHint. But for this project I will use the command line utility. If you have npm installed, you can just say

 npm install -g jshint 


After that, you can test the code. Example:

 jshint swapi.js 


Reduce the library


Although the library is small (128 lines), but it will obviously not decrease over time. And in general, if it is not worth any effort, why not do it. Minification removes extra spaces, shortens variable names and shrinks the file. I chose UglifyJS for this purpose:

 uglifyjs swapi.js -c -m -o swapi.min.js 


It's funny that this tool noticed the unused function getResource, which I left in the code:

 //    . todo -  function getResource(u, cb) { } 


Total, the file from 2750 bytes began to occupy 1397 - approximately 2 times less. 2.7 Kb - not much, but over time, libraries only grow.

Automate it!


As a very lazy person, I want to automate the whole process. Ideally, this should be:

- run the unit tests. in case of success
- run JSHint. in case of success
- create a mini-version of the library

For this, I will take Grunt . This is not the only choice, there is Gulp , but I did not use it. Grunt allows you to run a set of tasks, and it can be done so that the chain is interrupted in case of failure of one of them. For those who have not used Grunt, I suggest reading the introductory text .

Having added the package.json download to load the Grunt plugins plugins (Jasmine, JSHint and Uglify), I built the following Gruntfile.js:

 module.exports = function(grunt) { //   grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), uglify: { build: { src: 'lib/swapi.js', dest: 'lib/swapi.min.js' } }, jshint: { all: ['lib/swapi.js'] }, jasmine: { all: { src:"lib/swapi.js", options: { specs:"tests/spec/swapiSpec.js", '--web-security':false } } } }); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-jasmine'); grunt.registerTask('default', ['jasmine','jshint','uglify']); }; 


Simply put - run all the tests (Jasmine), run JSHint and then uglify. In the command line you just need to type "grunt".

image

If I break something, for example, add code that breaks JSHint, Grunt will report it and stop.

image

What is the result?


As a result, the library has not functionally changed, but:

- I have unit tests to check the work. By adding new features, I will be sure that I will not break the old ones.
- I used the linter to check the code of compliance with the recommendations. Verification of the code by an external observer is always a plus.
- added library minification. Especially not saved, but it is a reserve for the future
- automated all this kitchen. Now all this can be done in one small team. Life is beautiful, and I became a super-ninja code.

Now my project is better and I like it. The final version can be downloaded here .

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


All Articles