Тестирование веб-приложений — это объемная и сложная тема. Обычно различают функциональное, нагрузочное, регрессивное, модульное и интеграционное тестирование. В этой статье мы будем говорить лишь о модульном тестировании (от англ. unit), так как это наиболее часто встречающийся вид тестирования в мире веб-разработки.
В идеале тестирование должно быть неотъемлемым процессом разработки и, опять таки, в идеале тестирование должно основываться и придерживаться какой-нибудь методологии, например, TDD. Разработка через тестирование (от англ. test-driven development) — это методика, предлагающая вам написать сначала тест, а затем уже код, проходящий этот тест и только после этого, при необходимости, заниматься его рефакторингом. Однако, как мы все с вами хорошо знаем, заставить себя писать код для кода — задача из разряда сверхъестественного. Поэтому рассматривать методологии в этой статье я не вижу смысла.
На этом этапе нужно просто уяснить, что есть какой-то тест, и написанный в приложении код его должен каким-то образом пройти. При этом не важно когда этот тест был написан: до кода реализующего простейшую функцию или после него. Так как я приверженец идеологии «одна функция — одно действие», то никаких проблем с тестированием у меня, по идее, быть не может. Хотя читатель должен понимать, что такая идеология не всегда уместна, например, если есть функция посылающая POST-запрос на удаленный сервер, то, конечно же, она включает в себя простейшую проверку на валидность данных и вообще их формирование перед запросом. Возможно это не особо удачный пример, но здесь важно понять, что понятие одного действия достаточно абстрактно и, наверное, лучше понимать этот лозунг как «одна функция — одно законченное действие».
Шаг нулевой. Xo-xo-xo
Нет, в заголовок не закралась шутка и это не новогоднее настроение, просто сейчас речь пойдет о таком инструменте как XO. XO — это обертка великолепного инструмента для проверки кода ESLint от Николаса Закаса, разработанная, пожалуй, самым знаменитым npm-пользователем — Синдре Сорхусом. Я не буду еще раз говорить про то, как важно держать свой код в строгом порядке, так как разнорогласия здесь неуместны.
Основная идея XO в упрощении конфигурации ESLint (имеется предустановленный конфиг) и переносе настроек из файла .eslint
в файл package.json
. То есть конфигурация XO будет состоять из заранее определенных правил проверки в самом пакете и их переопределении в файле package.json
:
{
"name": "...",
...
"xo": {
"space": true,
"rules": {
"object-curly-spacing": [2, "always"],
"space-before-function-paren": 0
},
"envs": [
"node"
]
}
}
Стоит упомянуть, что ESLint — это гибкий аналог частенько вместе используемых JSHint и JSCS. Поэтому теперь необходимость использования этих инструментов полностью отпадает.
AVA, как Grunt, но для тестов
Как я уже говорил ранее, тестирование — это сложная тема и, как это не удивительно, есть большое количество инструментов, упрощающих этот процесс. Например, есть целый класс инструментов для автоматизации тестирования. Из всего многообразия (QUnit, Nodeunit, Mocha и т.д.) мне нравится AVA, написанный, все тем же Синдре Сорхусом.
Для примера напишем функцию, которая всегда возвращает true
и протестируем ее
var test = require('ava');
function thisIsTrue() {
return true;
}
test('True is true?', function(t) {
t.is(thisIsTrue(), true);
t.end();
});
Вот и все. Никаких сложных действий не требуется — только сравнить возвращаемое значение функцией с каким-то эталонным результатом, используя метод .is()
. Если функция вернет true, то тест будет пройден.
Важно заметить, что AVA работает асинхронно и для того, чтобы создать очередь, нужно использовать методы:
test.serial()
— создание очереди на основе следования тестов в файлеtest.before()
— запуск перед всеми тестамиtest.after()
— запуск после всех тестов
Разумеется, что существуют и другие методы, позволяющие проводить и более глубокое сравнение. Вот некоторые из них:
.pass()
— пропуск теста.ok()
— проверка истинности первого параметра.fail()
— провоцирование ошибки.is()
— проверка равенства первого и второго параметра.not()
— противоположный метод для.is()
.same()
— строгое сравнение первого и второго параметра.notSame()
— противоположный метод для.same()
.regexTest()
— проверка с помощью регулярного выражения
О других вы с превеликим удовольствием сможете узнать из документации.
Тестирование, тестирование, тестирование...
Пожалуй, теперь перейдем к тестированию чего-то более реального и рассмотрим пару тестов. Для этого я предлагаю обратиться к недавно написанному плагину grunt-bower-sync, основное предназначение которого заключается в синхронизации двух директорий, на основе файла bower.json
.
К сожалению, чтобы что-то тестировать, нужно понимать работу этого чего-то, поэтому вот небольшой экскурс для читателя в работу моего плагина:
- Чтение файла
bower.json
- Составление списка на основе выбранных в опциях секций (dependencies, devDependencies и peerDependencies)
- Копирование всех зависимостей из директории
bower_components
в целевую директорию с перезаписью - Получение списка зависимостей в целевой директории
- Поиск разности списков зависимостей в директории
bower_components
и целевой директории - Удаление лишних зависимостей в целевой директории
Что здесь можно протестировать? Буквально все, но делать этого я не буду. Дело в том, что меня здесь интересует лишь конечный результат.
Первым делом нужно создать директорию test/fixtures
и поместить туда файлы, который потребуются для тестирования. В нашем случае это директория bower_components
, содержащая фейковые директории и файлы зависимостей, а также сам файл bower.json
.
Теперь нужно натравить Grunt на наши фейковые данные и заставить его отработать их. Создадим задачу в Grunt-файле и укажем все необходимые данные для работы плагина:
bowersync: {
options: {
bowerFile: 'test/fixtures/bower.json'
},
copySingle: {
files: {
'tmp/copySingle': 'test/fixtures/bower_components'
}
}
}
Затем напишем тест, который сначала получает список зависимостей в целевой директории и затем сравнивает их с эталоном:
var fs = require('fs');
var test = require('ava');
test('copySingle | Copy only one dependency', function(t) {
var actual = fs.readdirSync('tmp/copySingle').toString();
t.is(actual, 'jquery');
t.end();
});
Восхитительно, неправда ли? Но не всегда возможно тестировать работу приложений, используя лишь их конечный функционал. Часто приходится подключать какие-то их части и обращаться к конкретным методам. Например, чтобы протестировать возможность удаления зависимостей у этого плагина, нужно сначала их скопировать, используя задачу в Grunt, затем изменить список зависимостей, имитируя изменение файла bower.json
и, только после этого начать процедуру удаления:
bowersync: {
options: {
bowerFile: 'test/fixtures/bower.json'
},
removeSingle: {
options: {
devDependencies: true,
peerDependencies: true
},
files: {
'tmp/removeSingle': 'test/fixtures/bower_components'
}
}
}
В этом случае тест будет выглядить малость сложнее:
var fs = require('fs');
var test = require('ava');
var Fsys = require('../tasks/lib/fsys');
test('removeSingle | Delete only one dependency', function(t) {
var fsys = new Fsys({ updateAndDelete: true });
// Сначала удаление зависимостей
fsys.removeDependencies('tmp/removeSingle', ['bootstrap', 'salvattore']).then(function(err) {
if (err) {
throw new Error(err);
}
// И лишь затем тестирование
var actual = fs.readdirSync('tmp/removeSingle').toString();
t.is(actual, 'bootstrap,salvattore');
t.end();
});
});
К сожалению, весь список тестов написать сразу не получится из-за того, что вы разработчик и знаете как использовать свое творение. После релиза этого плагина и его применения в rwk пришлось добавить тест и немного подправить логику плагина в случае, если директории bower_components
не существует или файл bower.json
не имеет вообще никаких зависимостей.
Автоматизация по крупному
Сейчас для того, чтобы запустить тесты, нужно всего лишь написать в консоль npm test
, однако такой подход начинает давать сбои, когда ваш проект начинает получать помощь из вне и когда:
- Кроме вашей ОС есть и другие
- Кроме вас в проекте могут участвовать другие люди
- Кроме вас и ваших людей в проект могут присылать PR
Окей, здесь два варианта решения проблемы:
- Вручную проверять каждый раз все изменения в проекте
- Использовать Travis Ci
Travis Ci — это синоним непрерывной интеграции. Сервис, который следит за изменениями в вашем репозитории и запускает описанные вами задачи (по умолчанию npm test
) каждый раз, когда вы изменяете в нем код. Кроме того каждый PR будет проверяться на прохождение уже написанных тестов и тестов, которые допишет автор PR.
Для примера обратимся к пакету windows-ls, который представляет собой имплементацию команды ls из Linux для Windows систем.
Чтобы использовать Travis Ci необходимо включить слежение за репозиторием в самом сервисе, а затем добавить в него файл .travis.yml
, содержащий используемый язык и настройки для него:
language: node_js
node_js:
- "0.10"
- "0.12"
- iojs
- "stable"
Кроме базовых настроек, существуют и более продвинутые. Например, можно намекнуть Трэвису, что перед выполнением любых команд, нужно установить какие-либо пакеты:
language: node_js
node_js:
- "0.10"
- "0.12"
- iojs
- "stable"
before_script:
- npm install -g grunt-cli
Более подробно о том, какие события запускаются в жизненном цикле каждого тестирования и как их использовать, можно прочитать в специальном разделе документации на официальном сайте сервиса.
Каждый раз при изменении содержимого репозитория и освобождения у Travis Ci юнит-сервера будет начинаться автоматическая сборка проекта и его тестирование. К сожалению, мое первое знакомство с сервисом было сложным из-за возможности тестировать код только на Linux и OS X:
Пришлось избавиться от одного из тестов именно для этой платформы, используя проверку os.type()
и изменить уже написанные тесты из-за сортировки файлов конкретно в этой ОС.
После проведения тестов станет доступна их история и подробный отчет о проведенных операциях и ошибках при условии, что они вообще были. Заметьте, что перед тестами также запускается XO, который описан у меня в задаче npm test
:
# Копирование репозитория
$ git clone --depth=50 --branch=master https://github.com/mrmlnc/windows-ls.git mrmlnc/windows-ls
# Настройка версии Node.js
$ nvm install stable
$ node --version
v4.0.0
$ npm --version
2.14.2
$ nvm --version
0.23.3
# Установка зависимостей и запуск тестов
$ npm install
$ npm test
> windows-ls@0.1.3 test /home/travis/build/mrmlnc/windows-ls
> xo --ignore=test/fixtures/** && node test/test.js
# Проверка кода с помощью XO
/home/travis/build/mrmlnc/windows-ls/test/test.js
19:55 warning Expected error to be handled handle-callback-err
26:58 warning Expected error to be handled handle-callback-err
33:58 warning Expected error to be handled handle-callback-err
40:58 warning Expected error to be handled handle-callback-err
47:58 warning Expected error to be handled handle-callback-err
54:58 warning Expected error to be handled handle-callback-err
61:60 warning Expected error to be handled handle-callback-err
68:59 warning Expected error to be handled handle-callback-err
76:66 warning Expected error to be handled handle-callback-err
# Запуск самих тестов
✖ 9 problems (0 errors, 9 warnings)
✔ ls glob
✔ ls -a
✔ ls
✔ ls -p
✔ ls -lh
✔ ls -F
✔ ls -R
✔ ls -laF
✔ ls -l
9 tests passed
Тестирование API
Если плагины для Grunt, да и любые другие пакеты, работающие внутри системы, можно назвать локальными, то внешние пакеты (веб-приложения), отвечающие на запросы из внешнего мира, тестировать немного сложнее. Дело в том, что если вы собираетесь тестировать API веб-приложения, то, скажем так, тестирование на «продакшене» недопустимо. Действительно, не будете же вы тестировать создание и удаление серверов, аккаунтов и прочие API прямо на боевом проекте? Для этого придется поднять фейковый сервер, на котором будет доступно фейковое API. Для таких нужд я часто использую Sandbox.
Sandbox позволяет в полуавтоматическом режиме строить API, используя для этого всем известные пакеты Express и Lodash, а также несколько GUI элементов. Первый отвечает за прием и отправку данных, а второй - за их обработку. К слову, можно использовать этот сервис лишь как конструктор API, так как он предоставляет код, который можно с легкостью переделать под «чистый» Express:
// Using named route parameters to simulate getting a specific user
Sandbox.define('/users/{username}', 'GET', function(req, res) {
// retrieve users or, if there are none, init to empty array
state.users = state.users || [];
// route param {username} is available on req.params
var username = req.params.username;
// log it to the console
console.log("Getting user " + username + " details");
// use lodash to find the user in the array
var user = _.find(state.users, { "username": username});
return res.json(user);
});
Выводы
Приступать к тестированию своих пакетов можно уже сейчас, используя описанные в этой статье инструменты или аналогичные им. Разумеется, что здесь описаны лишь базовые понятия, но эти знания можно применять и на более сложных проектах, например, в веб-приложениях, ведь практически в любой ситуации тестирование сводится к проверке эквивалентности получаемого значения и эталона.
Не стоит также забывать о тестировании клиентского JavaScript-кода, с которым не все так просто. Однако, по этому поводу я ничего рассказать читателям не могу.
Ссылки
Инструменты:
Статьи на тему: