Въведение

Общоизвестно е, че много разработчици не обичат да пишат модулни тестове за своя софтуер, защото това е много допълнителна работа, която трябва да е по избор. Въпреки това, ползата от тестването на модула над щателното наблюдение и ръчните проверки за грешки не може да бъде надценена. Тази статия има за цел да представи изчерпателен, но опростен подход към модулното тестване за начинаещи, които искат да опитат „разработка, управлявана от тестове“.

Единично тестване

Изходният код на всяко приложение се състои от малки отделни единици или компоненти. „Тестване на единици“ е процес на разработка на софтуер, при който всяка тествана единица от софтуера е изолирана и индивидуално тествана, за да се гарантира валидността на съответния им изход.

Мока рамката

Mocha е най-широко използваната от няколко рамки за тестване на Javascript. Той е може би най-популярният, защото поддържа тестване на асинхронен код и работи както на Node.js, така и на браузъра. Това означава, че трябва да имате инсталиран node.js на вашата машина, за да използвате Mocha.

Има два начина за инсталиране на мока:

$ npm install --global mocha and execute with $ mocha
$ npm install --save-dev mocha and execute with $ ./node_modules/mocha/bin/mocha

Първи стъпки към тестване на единици

Започвайки със синхронна логика, искаме да напишем тестове за редица функции, които изчисляват площта на фигурите (т.е. правоъгълник, триъгълник и кръг). Ние ще потвърдим очакванията си с помощта на вградения в Node.js модул „assert“. Това ще ни позволи да установим правила за желаното изпълнение.

Идентифицирайте и дефинирайте очакванията от теста

Очакванията за функцията areaOfRectangle()трябва да включват:

  1. Изчислете правилно площта на правоъгълник: (3 * 8) е равно на 24
  2. Приема само цели числа: (‘3’ * ‘8’) хвърля грешка
  3. Извежда грешка с отрицателни стойности на параметър: (-3 * 5) извежда грешка

За да определите основните очаквания за тест с Mocha, трябва да използвате describe() и it() функционира, както е показано в примера по-долу.

describe('#areaOfRectangle()', function() {
  it('should return the area of a rectangle', function() {
    function areaOfRectangle(width, height) {
      return width * height;
    }
    assert.equal(areaOfRectangle(3, 8), 24);
  });
});

describe() —е функция, която съдържа колекция от свързани тестове. Тестов пакет може да бъде дефиниран с вложени функции describe(), ако има нужда от това.

it() —е функция, която се съдържа в describe(). Той съдържа името на изолиран тест и функция, която изпълнява всички твърдения за този тест. it() може да бъде описан като Spec.

assert module —успехът или неуспехът на теста зависи от този модул, тъй като тойопределя какъв трябва да бъде изходът на тествана функция.

Сега, след като идентифицирахме и дефинирахме очакванията за теста, ще продължим и ще напишем тестовия пакет.

Създайте нова папка с имеunit-testing. В тази папка създайте нов файл и го наречете Shapes.js, след което копирайте и поставете кода по-долу.

// Shapes.js
class Shapes {
    areaOfRectangle(width, height) {
        return width * height;
    }
    areaOfTriangle(base, height) {
        return (base * height) / 2
    }
    areaOfCircle(radius) {
        let pi = 3.14;
        return (pi * (radius ** 2));
    }
}
module.exports = new Shapes();

Все още в папката unit-testingсъздайте подпапка и я наименувайте конкретно „test“. Това е така, защото mocha търси тази директория, за да изпълни тестовите файлове в нея.

Във вашата папка test създайте файл specs.js и въведете следния код.

// test/specs.js


const assert = require('assert');
const Shapes = require('../Shapes');


describe('Calculate the Area of Shapes', function() {
    describe('areaOfRectangle', function() {
        it('should return the area of a rectangle', function() {
            assert.equal(Shapes.areaOfRectangle(3, 8), 24);
        });


        it('should throw a TypeError for string values', function() {
            assert.throws(function() {
                Shapes.areaOfRectangle('6', '12')
            }, TypeError, 'No string parameters');
        });


        it('should throw an Error for negative values', function() {
            assert.throws(function() {
                Shapes.areaOfRectangle(-20, -12)
            }, Error, 'No negative numbers');
        });
    });
});

Отидете до директорията unit-testing чрез терминал и стартирайте $ mocha. Резултатът от теста трябва да има само един от трите пакета тестове, които преминават.

От резултатите от теста става ясно, че функцията не отговаря на очакванията. Следващото нещо, което трябва да направите, е да преработите кода, за да включите липсващите грешки при твърдения, така че всички тестови спецификации да преминат.

Рефакторен код

// Shapes.js


class Shapes {
    areaOfRectangle(width, height) {
        if ((typeof width && typeof height) !== 'number') {
            throw new TypeError('Params must be a number.')
        }
        else if (width < 0 || height < 0){
            throw new Error('No negative numbers')
        }
        return width * height;
    }


    areaOfTriangle(base, height) {
        return (base * height) / 2
    }


    areaOfCircle(radius) {
        let pi = 3.14;
        return (pi * (radius ** 2));
    }
}


module.exports = new Shapes();

Ако стартираме теста отново в този момент, ще имаме три издържащи теста, защото сега имаме функция, която работи според очакванията.

Спецификациите за функциите areaOfTriangle() и areaOfCircle() могат да бъдат написани по същия модел.

// test/specs.js


const assert = require('assert');
const Shapes = require('../Shapes');


describe('Calculate the Area of Shapes', function() {
    describe('areaOfRectangle', function() {
        it('should return the area of a rectangle', function() {
            assert.equal(Area.areaOfRectangle(3, 8), 24);
        });


        it('should throw a TypeError for string values', function() {
            assert.throws(function() {
                shapes.areaOfRectangle('6', '12')
            }, TypeError, 'No string parameters');
        });


        it('should throw an Error for negative values', function() {
            assert.throws(function() {
                shapes.areaOfRectangle(-20, -12)
            }, Error, 'No negative numbers');
        });
    });


    describe('areaOfTriangle', function() {
        it('should return the area of a triangle', function() {
            assert.equal(shapes.areaOfTriangle(3, 8), 12);
        });
    });


    describe('areaOfCircle', function() {
        it('should return the area of a circle', function() {
            assert.equal(shapes.areaOfCircle(8), 200.96);
        });
    });
});

Единични тестове за асинхронен код

Асинхронността на кода позволява изпълнението на програмата да обработва други подредени процеси, докато чака чакащ изход да бъде върнат. Асинхронното програмиране се прилага с различни подходи като Callbacks, Promises и Async/Await. Най-лесният начин за представяне на асинхронност в Javascript е чрез използване на метода setTimeout() за планиране на забавени отговори.

Процесът на модулно тестване за асинхронен код не се различава от този на синхронния код. Акцентът обаче е върху необходимите предпазни мерки, които трябва да предотвратят завършването на вашите тестове преди получаването на забавени отговори.

Създайте нов файл AsyncShapes.js и копирайте кода в следващия фрагмент в него. Това е клас със сравнително прости асинхронни методи.

// AsyncShapes.js


class AsyncShapes {
    constructor(shapesArray) {
        this.shapesArray = shapesArray;
    }


    validateShape(item, callback) {
        setTimeout(function() {
            callback(this.shapesArray.includes(item))
        }.bind(this), 1000);
    }


    getShape(id, callback) {
        if (id >= this.shapesArray.length) {
            throw new Error(`There is no shape with index ${id}`)
        }
        setTimeout(function() {
            callback(this.shapesArray[id]);
        }.bind(this), 1000)
    }


    addShape(item, callback) {
        setTimeout(function() {
            callback(this.shapesArray.push(item));
        }.bind(this), 3000)
    }


    removeShape(id, callback) {
        setTimeout(function() {
            callback(this.shapesArray.splice(id, 1));
        }.bind(this), 1000)
    }
}


module.exports.AsyncShapes = AsyncShapes;

Създайте друг файл asyncShapesSpec.js в тестовата папка и напишете тестовия костюм за нашите асинхронни функции.

// test/asyncShapesSpec.js


const assert = require('assert');
const asyncShapes = require('../AsyncShapes');
let list = ['Rectangle', 'Triangle', 'Circle', 'Trapezium'];
let shapesObj = new AsyncShapes(list);
  
describe('Async Shapes Functions', function() {
    it ('should validate an existing shape', function() {
        shapesObj.validateShape('Square', function(validShape) {
            assert.equal(validShape, true);
        });
    });
});

Този тест не трябва да потвърждава „Квадрат“ като съществуваща форма. И все пак изненадващо, тестът ще премине погрешно на първа инстанция. Но след известно време той хвърля AssertionError, както се вижда по-долу.

Когато тест завърши преди обратното извикване да бъде върнато, той третира празното твърдение като положителен резултат, което дава тест за преминаване. В този случай, въпреки че „Квадрат“ не е включен в списъка, тестът не е неуспешен, тъй като обратното извикване все още не е върнало фалшив отговор.

За да попречите на тестовата рамка да компрометира резултатите от теста чрез това поведение, трябва да подадете параметър done към функцията във вашия it(). Трябва също да извикате done()след вашето твърдение. Това ще принуди теста да прекрати след получаване на забавения отговор.

Подобрен тестов пакет за методи на асинхронен клас

Освен обратното извикване done(), което беше споменато по-рано, бих искал да внеса най-простото използване на куката beforeEach(). Това е една от няколкото куки с имена before(), after(), beforeEach() и afterEach(). Те се използват за задаване на условия, които трябва да се прилагат преди или след всяка функция според случая. В този случай beforeEach() инициализира конструктора на класа AsyncShapes с масив от форми.

// test/asyncShapesSpec.js


const assert = require('assert');
let asyncShapes = require('../AsyncShapes.js');
let list = ['Rectangle', 'Triangle', 'Circle', 'Trapezium'];
let shapesObj;


beforeEach('Load Shapes Array', function(){
    shapesObj  = new asyncShapes.AsyncShapes(list)
});
  
describe('Async Shapes Functions', function() {
    it ('should return true for an existing shape', function(done) {
        shapesObj.validateShape('Circle', function(validShape) {
            assert.equal(validShape, true);
            done();
        });
    });


    it ('should get a shape by ID', function(done) {
        shapesObj.getShape(3, function(result) {
            assert.equal(result, 'Trapezium');
            done();
        });
    });


    it ('should increase the number of shapes by 1', function(done) {
        shapesObj.addShape('Parallelogram', function() {
            assert.equal(list.length, 5);
            done();
        });
    });


    it ('should remove a shape by ID', function(done) {
        shapesObj.removeShape(2, function() {
            assert.equal(list.length, 4);
            done();
        });
    });
})

Така че, ако изпълним теста в този момент, резултатът ще бъде списък от 9 спецификации за преминаване от двата тестови файла specs.js и asyncSpecs.js.

Заключение

В цялата статия използвахме само barebone Mocha.js без никакви допълнителни библиотеки за твърдения. Това в никакъв случай не е единственият инструмент, който е наличен за разширено тестване. Трябва също така да потърсите как да използвате Библиотеката за утвърждаване на Chai заедно с Mocha, за да се насладите на по-класически стилове на утвърждаване и възможности за вериги.

Можете също така да изпълнявате тестовете си от вашия терминал, като използвате npm test вместоmocha. За да работи тази команда, трябва да добавите следния скрипт към вашияpackage.json:

"scripts": {
    "test": "mocha"
}

И накрая, Promises и Async/Await са много важни функции в съвременното асинхронно програмиране на Javascript. Въпреки че обхватът на тази статия не може да ги обхване удобно, важно е да се запознаете с начина, по който работят и как се използват вместо обратни извиквания.