Блог Стебунова Владимира

Javascript-подходы к поиску и предотвращению ошибок

В данной статье я хочу провести обзор подходов, которые могут помочь в поиске и предотвращении ошибок до попадания в готовый продукт.

Существует идея о необходимости переводить ошибки как можно левее по этой шкале.

Компилятор - Линтовшик - Тест - Выполнение

Пример кода

Возьмем следующий код. Он содержит одну ошибку. Попробуем посмотреть, какие практики нам помогут.

let x = 100;

let y = "500";

function sum(x, y) {
    return x + y;
}

console.log(sum(x,y));

Пассивные практики

Пассивными практиками я буду называть то, что делается один раз, при этом изменение кода не влияет на саму практику. Чем-то это похоже на пассивные навыки из Дьябло: один раз надел амулет или вставил камень в слот, и 10% от огня уже всегда с тобой.

Под такими практиками можно подразумевать анализатор кода, линтовшик и типизацию (которая она требует дополнительной работы).

Линтовшик

Воспользовался стандартным eslint с настройками от airbnb. Никаких ошибок, кроме того, что отключена консоль, он не нашёл. Негусто. Видимо, код был написан сразу в нужной стилистике, и ждать каких-то выдающихся результатов не стоит.

>eslint first.js

d:\projects\catchme\first.js
  8:1  error  Unexpected console statement  no-console

✖ 1 problem (1 error, 0 warnings)

Но всё-таки стиль предполагает, что какую-то часть ошибок получится убрать, придерживаясь стиля. Вот, например, очень распространенная ошибка, которую линтер всё-таки поймает.

let x = 100;
let y = "500";

function sum(x, y) {
    return x
    + y;
}

console.log(sum(x,y));

Мы видим, что её он поймал.

d:\projects\catchme>eslint first.js

d:\projects\catchme\first.js
  5:13  error  Trailing spaces not allowed                  no-trailing-spaces
  6:10  error  '+' should be placed at the end of the line  operator-linebreak
  9:1   error  Unexpected console statement                 no-console

✖ 3 problems (3 errors, 0 warnings)
  2 errors, 0 warnings potentially fixable with the `--fix` option.

Анализатор кода

Еще один защитник на пути ошибок — анализатор кода. Но для того, чтобы он отрабатывал, необходимо, чтобы ошибка была популярной и шаблонной, иначе он пробежится по коду и ничего не найдёт. Я не стал долго разбираться в этой теме и воспользовался JSHint в его web версии.

Metrics

There is only one function in this file.

It takes 2 arguments.

This function contains only one statement.

Cyclomatic complexity number for this function is 1.
Two warnings
1    'let' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).
2    'let' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

Как видим, популярных ошибок в нашем коде не обнаружено.

Типизирование входных и выходных параметров, наивное

Переименуем наш файл в .ts и попробуем скомпилировать. На выходе получается тот же файл, только let заменены на var и все пробелы убраны

tsc first.ts --outFile res.js

Но попробуем типизировать нашу функцию и посмотреть, к каким последствиям это приведёт. Для начала только выходной параметр сделаем числовым.

function sum(a, b):number {

И компилятор это съест без проблем.

А теперь типизируем и входные параметры.

function sum(a:number, b:number):number {

Ура первая победа!

first.ts:8:19 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

8 console.log(sum(x,y));

Теперь мы спасены от ошибок, когда параметры, заданные с самого начала, мы пытаемся пронести в нашу функцию. Это очень полезно при взаимодействии между модулями, которые меняются отдельно. За соответствие между частями программы следит компилятор. Но не все данные приходят к нам изнутри, есть еще внешние данные введённые пользователем, попробуем сэмулировать и такое поведение.

function sum(a:number, b:number):number {
    return a + b;
}

let x = 100;
let y:number;

y = parseInt("y500");

console.log(sum(x,y));

Компилятор это без проблем съедает и на выходе мы получаем

d:\projects\catchme>tsc first.ts --outFile res.js

d:\projects\catchme>node res.js
NaN

С NaN, конечно, смешно получается, потому что по логике это тот самый number.

Активные практики

Перейдем к активным практикам. Под ними я подразумеваю уже включение головы, рук и написание дополнительного кода. Самое дешёвое средство — это явное прописывание в требованиях входных и выходных параметров, но это не всегда возможно, так что теперь на плечи программиста ложится валидация работы программы.

Способами типа посмотреть на код или посмотреть на подозрительные места и запустить код с отладчиком и просто посмотреть, как всё работает, вы можете воспользоваться сами, так что перейдем к тем, где требуется дополнительный код сразу.

Написать тесты

Первое, что вынуждает нас сделать фреймворк для тестов, — это вынести тестируемый код в отдельный модуль. Получим следующее:

var sum = require("./sum");

let x = 100;
let y = "500";

console.log(sum(x,y));

и сам модуль

module.exports = function sum(x, y) {
    return x + y;
}

Напишем тесты. Так как нам необходимо только проверить, что функция будет работать с правильными параметрами и неправильными. Основное правило теста — проверить правильные, неправильные и граничные параметры как начало хорошего теста.

var assert = require("assert");
var sum = require("../sum");

describe("sum", () => {
    it('should work with simple data', () => {
        assert.equal(sum(1, 5), 6);
    })
    it('should return error with wrong data', () => {
        assert.throws(() => {
            sum(-1, "5")
        });
    })
})

Запустим тест.

d:\projects\catchme>mocha test


  sum
    √ should work with simple data
    1) should return error with wrong data


  1 passing (9ms)
  1 failing

  1) sum
       should return error with wrong data:
     AssertionError [ERR_ASSERTION]: Missing expected exception.
      at innerThrows (assert.js:646:7)
      at Function.throws (assert.js:662:3)
      at Context.it (test\test.js:9:16)

Видим, что тут уже отлавливается наша ошибка со строкой как параметром.

Прекондишены и посткондишены. Контрактное программирование.

const assert = require('assert');

function sum(x, y) {
    return x + y;
}

function contractedSum(x, y) {
    // Precondition
    assert(0 < x && x < 101 && typeof x === "number", "x is wrong!");
    assert(0 < y && y < 501 && typeof y === "number", "y is wrong!");
    
    // Action
    let result = sum(x, y);

    // Postcondition
    assert(!isNaN(result), "result is wrong");
    assert(typeof result === "number", "result is wrong");

    return result;
}

let x = 100;
let y = parseInt("y500");

console.log(contractedSum(x,y));

Компилируем

d:\projects\catchme>node assertme.js 
assert.js:41 
  throw new errors.AssertionError({ 

AssertionError [ERR_ASSERTION]: y is wrong!                                                   
    at contractedSum (d:\projects\catchme\assertme.js:10:5) 
    at Object.<anonymous> (d:\projects\catchme\assertme.js:25:13) 
    at Module._compile (module.js:624:30) 
    at Object.Module._extensions..js (module.js:635:10) 
    at Module.load (module.js:545:32) 

На мой взгляд, тесты получаются такими неудобными предкондишенами, потому что не существуют в самом коде и не дают будущим редакторам этого кода видеть те ограничения, которые мы на него установили.

Выводы

Единственный вывод, который я могу сделать, это вывод о необходимости использовать все эти техники правильно, выбирая те моменты, где они могут выстрелить и предупредить ошибку.

К сожалению, ни один из них полностью не гарантирует, что будут найдены все ошибки, как и применение их без оглядки только увеличит работу без какого-либо выхлопа.

Следующая статья

Предыдущая статья