Введение

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

Модульное тестирование

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

Фреймворк Мокко

Mocha является наиболее широко используемой из нескольких сред тестирования Javascript. Возможно, он самый популярный, поскольку поддерживает тестирование асинхронного кода и работает как в Node.js, так и в браузере. Это означает, что для использования Mocha на вашем компьютере должен быть установлен node.js.

Есть два способа установить мокко:

$ 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() — это функция, содержащая набор связанных тестов. Набор тестов может быть определен с помощью вложенных функций description(), если в этом есть необходимость.

it() —функция, содержащаяся в функции описать(). Он содержит имя изолированного теста и функцию, реализующую все утверждения для этого теста. it() можно описать как Spec.

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

Теперь, когда мы идентифицировали и определили ожидания от тестов, мы продолжим и напишем набор тестов.

Создайте новую папку с именем 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);
        });
    });
});

Модульные тесты для асинхронного кода

Асинхронность кода позволяет выполнению программы обрабатывать другие выстроенные в очередь процессы, ожидая возврата ожидающего вывода. Асинхронное программирование применяется с различными подходами, такими как обратные вызовы, обещания и асинхронное/ожидание. Самый простой способ представить асинхронность в 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.

Заключение

На протяжении всей статьи мы использовали только базовую версию Mocha.js без каких-либо дополнительных библиотек утверждений. Это ни в коем случае не единственный инструмент, доступный для расширенного тестирования. Вы также должны узнать, как использовать библиотеку утверждений Chai вместе с Mocha, чтобы насладиться более классическими стилями утверждений и возможностями цепочки.

Вы также можете запускать тесты со своего терминала, используя npm test вместо mocha. Чтобы эта команда работала, вам нужно добавить следующий скрипт в файл package.json:

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

Наконец, Promises и Async/Await — очень важные функции в современном асинхронном программировании на Javascript. Хотя объем этой статьи не может их охватить, важно ознакомиться с тем, как они работают и как они используются вместо обратных вызовов.