Не прошло и полгода как я наконец добрался до написания второй главы учебника. Первую главу я тоже немного переработал с учетом пожеланий читателей, так что можете снова ее просмотреть — Web-разработка на node.js и express. Изучаем node.js на практике
Глава 2. Демонстрационное приложение и первые тесты
В этой главе мы приступим к разработке нашего приложения, которое мы будем использовать в качестве примера на протяжении всего учебника, и начнем с самого простого, а именно со статических страниц. Так же мы познакомимся с тестированием приложений на node.js и с инструментами, которые используются для этого.
2.1 Model-View-Controller (MVC)
Перед тем как приступать собственно к разработке приложения, полезно поговорить о том, что из себя представляет типичная архитектура web-приложения на наиболее высоком уровне абстракции. Самым популярным архитектурным паттерном на сегодняшний день является model-view-controller (MVC), общий смысл паттерна заключается в том, чтобы разделить бизнес логику приложения (её привязывают к моделям) и представление — view. Кроме того, модели реализуют интерфейс к базе данных. Контроллер играет роль посредника между моделью и представлением. В случае web-приложения — это выглядит так: браузер пользователя отправляет запрос на сервер, контроллер обрабатывает запрос, получает необходимые данные из модели и отправляет их во view. View получает данные из контроллера и превращает их в красивую HTML страничку, которую контроллер в итоге отправит пользователю.
2.2 Демонстрационное приложение
Пришло время приступить к разработке нашего демонстрационного приложения. В первой главе мы уже развернули тестовое приложение, но воспользовались при этом генератором express и не написали ни строчки кода. Теперь мы будем писать наше приложение сами и начнем с «Hello, World».
$ cd ~/projects/node-tutorial
$ mkdir node-demo-app
$ cd node-demo-app
2.2.1 Пакеты npm
Что такое npm? Все просто, это node package manager (хотя авторы это оспаривают). В общих чертах пакет npm — это директория содержащая программу и файл package.json, описывающий эту программу, в том числе в этом файле можно указать от каких других пакетов зависит наша программа, почитайте описание package.json.
Для того чтобы воспользоваться всеми прелестями, которые нам может предоставить npm, мы создадим в корневой директории нашего проекта файлик:
$ touch package.json
package.json:
{
"name": "node-demo-app"
, "version": "0.0.1"
, "scripts": { "start": "node server.js" }
, "dependencies": { "express": "3.0.x" }
}
Теперь можно выполнить
$ npm install
В результате npm создаст директорию node_modules в которую поместит все модули от которых зависит наш проект.
2.2.2 Hello, World!
Основной файл назовем server.js:
$ touch server.js
server.js:
var express = require('express')
, app = express()
, port = process.env.PORT || 3000
app.get('/', function (req, res) {
res.send('Hello, World!')
})
app.listen(port, function () {
console.log('Listening on port ', port)
})
Сразу определимся с терминологией и разберем этот код. Нашим приложением будет объект app
, вызов функции app.get
монтирует экшн (action), роль которого в данном случае выполняет анонимная функция, к пути (route) '/'. Фактически это означает, что каждый раз при получении http запроса GET /, приложение выполнит указанный экшн. Переменная port
в этом примере инициализируется переменной окружения PORT
при её наличии, а если такой переменной нет, то принимает значение 3000. app.listen
запускает http-сервер на указанном порте и начинает слушать входящие запросы.
Для того, чтобы полюбоваться результатом нашего труда, есть два способа:
$ node server.js
либо
$ npm start
Второй способ доступен потому что мы добавили соответствующую строчку в файл конфигурации package.json в разделе «scripts».
Теперь по адресу http://localhost:3000/ можно получить строчку 'Hello, World!'.
Настало время залить что-нибудь в GitHub. Создаем новый репозиторий на GitHub с названием node-demo-app и выполняем в директории проекта следующий набор команд, сперва создадим файл README.md (правило хорошего тона)
$ echo '# Node.js demo app' > README.md
Создадим файл .gitignore для того чтобы не коммитить лишние файлы в git, а именно директорию node_modules:
$ echo 'node_modules' > .gitignore
Возможно кто-то читал статью Mikeal Rogers и хотел бы возразить против добавления node_modules в .gitignore. Для тех кому лень читать, в проектах на node.js рекомендуется такой подход:
- Для проектов которые мы разворачиваем, таких как веб-приложения, node_modules помещаются в репозиторий.
- Для библиотек и другого повторно используемого кода node_modules не добавляются в репозиторий.
- Для развертывания на production npm не используется.
Но! Мы в качестве
Создаем репозиторий, коммитимся и заливаем все на GitHub:
$ git init
$ git add .
$ git commit -m 'Hello, World'
$ git remote add origin git@github.com:<username>/node-demo-app.git
$ git push -u origin master
2.2.3 Структура приложения
Express пока не диктует строгой структуры для файлов приложения, так что мы придумаем свою. Предлагаю такой вариант:
/node-demo-app
|- /app
| |- /controllers - контроллеры
| |- /models - модели
| |- /views - html темплейты
| |- config.js - файл с настройками приложения
| |- main.js - основной файл приложения
|- /public - статика - картинки, клиентские скрипты, стили и т.д.
|- /tests - автоматические тесты
|- app.js - загрузчик приложения
|- server.js - http сервер
Никто не заставляет придерживаться именно такой схемы расположения файлов, но мне она кажется удобной, так что просто запомним эту картинку и по мере продвижения по туториалу будем создавать необходимые файлы и директории.
2.3 Тестирование приложения
О том что такое TDD и зачем нужно писать тесты вы наверняка уже слышали, а если нет, то можете прочитать об этом здесь. В этом учебнике для тестирования приложения мы воспользуемся подходом, который называется BDD (behavior-driven development). В тестах мы будем описывать предполагаемое поведение приложения. Сами тесты разделим на две категории: integration тесты — они будут имитировать поведение пользователя и тестировать систему целиком, и unit тесты — для тестирования отдельных модулей приложения.
2.3.1 Автоматические тесты
В качестве фреймворков для написания тестов мы будем использовать библиотеки mocha (читается как мокка, кофе-мокка :)), should.js, и supertest. Mocha служит для организации описаний тест-кейсов, should.js предоставляет синтаксис для осуществления различных проверок, а supertest — это надстройка над простеньким http-клиентом, которая позволяет проверять результаты http-запросов. Для подключения библиотек сделаем необходимые изменения в package.json
{
"name": "node-demo-app"
, "version": "0.0.1"
, "scripts": { "start": "node server.js" }
, "dependencies": { "express": "3.0.x" }
, "devDependencies": {
"mocha": "1.7.0"
, "should": "1.2.1"
, "supertest": "0.4.0"
}
}
Зависимости мы разместили в разделе «devDependencies», так как нет никакой необходимости тащить эти библиотеки на продакшн сервер. Для установки библиотек выполняем
$ npm install
Для того что бы понять как это работает, попробуем создать свой первый тест и прогнать его через наш фреймворк
$ mkdir tests
$ touch tests/test.js
В test.js положим такой тест
describe('Truth', function () {
it('should be true', function () {
true.should.be.true
})
it('should not be false', function () {
true.should.not.be.false
})
})
и запустим его
$ ./node_modules/.bin/mocha --require should --reporter spec tests/test.js
Вполне естественно, что такой тест пройдет, так что заменим его на что-то неработающее
describe('foo variable', function () {
it('should equal bar', function () {
foo.should.equal('bar')
})
})
запускаем
$ ./node_modules/.bin/mocha --require should --reporter spec tests
и видим, что тесты не прошли, придется чинить код, добавляем объявление переменной
var foo = 'bar'
describe('foo variable', function () {
it('should equal bar', function () {
foo.should.equal('bar')
})
})
запускаем
$ ./node_modules/.bin/mocha --require should --reporter spec tests/test.js
и видим что код рабочий.
Основной принцип TDD состоит в том, чтобы напсать тесты до того как написан код, таким образом мы можем убедиться в том, что тесты действительно что-то тестируют, а не просто запускают код на выполнение и делают проверки в стиле true.should.be.true. То есть процесс разработки выглядит следующим образом:
- Пишем тест
- Выполняем тест и убеждаемся в том что он падает
- Пишем код
- Выполняем тест и убеждаемся в том что он проходит, если нет, возвращаемся в п.3
И так много раз.
Чтобы упростить запуск тестов добавим таск прогоняющий тесты в Makefile
$ touch Makefile
Содержимое Makefile:
REPORTER=spec
TESTS=$(shell find ./tests -type f -name "*.js")
test:
@NODE_ENV=test ./node_modules/.bin/mocha
--require should
--reporter $(REPORTER)
$(TESTS)
.PHONY: test
Традиционно make использовался для сборки проекта, но его удобно использовать и в целом для автоматизации рутинных задач. Об использовании Makefile читайте здесь. Обращаю внимание на то, что отступы после названия таска должны быть сделаны табами, а не пробелами.
Теперь test-suite можно запускать коммандой:
$ make test
Попробуем потестировать http запросы. Для того чтобы сделать тестирование более удобным проведем небольшой рефакторинг кода и вынесем приложение express из файла server.js в отдельный модуль app/main.js, а также создадим файл app.js который будет этот модуль экспортировать. Сейчас это может выглядеть нецелесообразным, но такой способ организации кода нам пригодится, когда мы будем проверять покрытие кода тестами.
$ mkdir app
$ touch app/main.js
app/main.js:
var express = require('express')
, app = express()
app.get('/', function (req, res) {
res.send('Hello, World!')
})
module.exports = app
$ touch app.js
app.js:
module.exports = require(__dirname + '/app/main')
server.js заменяем на
var app = require(__dirname + '/app')
, port = process.env.PORT || 3000
app.listen(port, function () {
console.log('Listening on port ', port)
})
Для того чтобы понять как работают модули node.js, а также что означают require
и module.exports
читаем документацию
Для того, чтобы проверить корректность http запроса напишем в test.js следующий код
var request = require('supertest')
, app = require(__dirname + '/../app')
describe('GET /', function () {
it('should contain text "Hello, Express!"', function (done) {
request(app)
.get('/')
.expect(/Hello, Express!/, done)
})
})
В этом тесте мы проверяем, что сервер отвечает нам строчкой «Hello, Express!». Так как вместо этого сервер отвечает «Hello, World!», тест упадет. Важный момент, на который нужно обратить внимание, запросы к http серверу происходят асинхронно, по-этому нам нужно будет назначить callback на завешение теста. Mocha предоставляет такую возможность с помощью функции done, которую можно опционально передать в функцию с тест-кейсом. Чтобы тест прошел, нужно заменить строчку «Hello, World!» на «Hello, Express!» в файле app/main.js и выполнить make test
.
2.3.2 Покрытие кода тестами
В принципе, этот параграф можно пропустить, так как на процесс написания тестового приложения он никак не влияет, но отчет о покрытии кода тестами будет приятным дополнением к нашему test-suite.
Чтобы выяснить насколько полно наш код покрыт тестами, потребуется еще один инструмент, он называется jscoverage. Его придется скомпилировать. Так что если у вас еще не установлен компилятор, стоит его поставить:
$ sudo apt-get install g++
После чего устанавливаем jscoverage:
$ cd /tmp
$ git clone git://github.com/visionmedia/node-jscoverage.git
$ cd node-jscoverage
$ ./configure && make
$ sudo make install
Вернемся в директорию проекта:
$ cd ~/projects/node-tutorial/node-demo-app/
Нам потребуется внести некоторые изменения в Makefile и app.js чтобы иметь возможность генерировать отчеты о покрытии.
Makefile:
REPORTER=spec
TESTS=$(shell find ./tests -type f -name "*.js")
test:
@NODE_ENV=test ./node_modules/.bin/mocha
--require should
--reporter $(REPORTER)
$(TESTS)
test-cov: app-cov
@APP_COV=1 $(MAKE) --quiet test REPORTER=html-cov > coverage.html
app-cov:
@jscoverage app app-cov
.PHONY: test
app.js:
module.exports = process.env.APP_COV
? require(<strong>dirname + '/app-cov/main')
: require(</strong>dirname + '/app/main')
Мы добавили таск test-cov в Makefile так что теперь для генерации отчета coverage.js достаточно будет запустить make test-cov
. Изменения в app.js связаны с тем, что для генерации отчета используется инструментированная копия приложения, которую генерирует jscoverage. То есть мы проверяем переменную окружения APP_COV
и если она установлена загружаем приложение из директории /app-cov, а если нет, то берем обычную версию из /app.
Генерируем отчет:
$ make test-cov
Должен появиться файл coverage.html, который можно открыть в браузере.
Осталось добавить в .gitignore app-cov и coverage.html:
$ echo 'app-cov' >> .gitignore
$ echo 'coverage.html' >> .gitignore
С тестами мы разобрались, так что удаляем тестовый тест
$ rm tests/test.js
И коммитимся
$ git add .
$ git ci -m "Added testing framework"
$ git push
Исходники демонстрационного приложения можно получить тут: github.com/DavidKlassen/node-demo-app
На подходе третья глава, в ней мы напишем полноценный контроллер для страниц сайта и разберемся с тем как работает шаблонизация в express.
Автор: f0rk