Pull to refresh

Web разработка на node.js и express. Глава 2 — тестирование приложения

Reading time 8 min
Views 50K
Не прошло и полгода как я наконец добрался до написания второй главы учебника. Первую главу я тоже немного переработал с учетом пожеланий хабражителей, так что можете снова ее просмотреть — 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 не используется.


Но! Мы в качестве хостинга используем Heroku и способ деплоя не выбираем, а там node.js проекты деплоятся с помощью 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. То есть процесс разработки выглядит следующим образом:

  1. Пишем тест
  2. Выполняем тест и убеждаемся в том что он падает
  3. Пишем код
  4. Выполняем тест и убеждаемся в том что он проходит, если нет, возвращаемся в п.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(__dirname + '/app-cov/main') 
  : require(__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.
Tags:
Hubs:
+23
Comments 27
Comments Comments 27

Articles