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)
На мой взгляд, тесты получаются такими неудобными предкондишенами, потому что не существуют в самом коде и не дают будущим редакторам этого кода видеть те ограничения, которые мы на него установили.
Выводы
Единственный вывод, который я могу сделать, это вывод о необходимости использовать все эти техники правильно, выбирая те моменты, где они могут выстрелить и предупредить ошибку.
К сожалению, ни один из них полностью не гарантирует, что будут найдены все ошибки, как и применение их без оглядки только увеличит работу без какого-либо выхлопа.