Fork me on GitHub

11. Улучшаем тестирование

Добро пожаловать в одиннадцатую часть руководства по созданию веб-приложения с помощью Node.js. В рамках серии уроков будет рассказано про основные особенности и трудности, которые возникают при работе с Node.js.

Предыдущие части:

11.1. Check Out для каждого коммита

Вы, наверное, уже заметили, что я добавляю часть SHA1-хэша для каждого коммита. Делается это для того, чтобы можно было скачать отдельную часть кода данного руководства. Делается это очень просто:

git clone git://github.com/alexyoung/nodepad.git
git checkout -b tutorial_8 df0b954

Эта команда создаст новую локальную ветку, которая будет соответствовать состоянию репозитория при коммите df0b954. Имя ветки задается в части -b tutorial_8, так что можно без проблем создать по ветке хоть на каждый коммит и переключаться между ними.

11.2. Ошибки и улучшения

В выходные Matthias Lübken помог мне разобраться с некоторыми странностями в Nodepad. У нас была достаточно долгая дискуссия в GitHub и Twitter, в результате которой мы пришли к выводу, что Nodepad не очень хорошо работает на Node 0.3.x (сейчас уже Node 0.4.x, прим. перев.). Так что я рекомендую использовать, по-возможности, 0.2.4.

В DailyJS мы уже рассматривали один из менеджеров версий Node.js — n. Так что его вполне можно использовать для того, чтобы переключаться между 0.2.x и 0.3.x ветками. Еще одним иструментом, позволяющий это сделать, является nvm.

Примечание

Еще одним инструментом, позволяющим работать одновременно с несколькими версиями node.js, является утилита nodeenv.

Кроме того, Matthias обнаружил, что при использовании последней версии Jade в flash-сообщениях выдается escaped HTML. Это не будет исправляться.

Matthias так же предложил, что для большей безопасности следует использовать секретный ключ. Вам так же следует, по-возможности, всегда использовать настройку secret для express.session.

11.3. Тестирование с Expresso и Zombie.js

Ранее написанные тесты больше не работают. Это частично моя вина, но так же значительную роль в этом сыграло отсутствие хороших руководств по тестированию с помощью Expresso. Как и большинству наших читателей, мне комфортно с серверным JavaScript и клиентскими утилитами, которые заставили меня задаться вопросом, было бы легче писать тесты в стиле, который отражает это. Поэтому я решил совместить Expresso и Zombie.js.

Так что я пошел на поиски, чтобы написать идиоматический тестовый код с помощью Expresso, Zombie.js и Mongoose. У меня получился следующий скелет:

// Используем тестовое окружение
process.env.NODE_ENV = 'test';

var app = require('../app'),
    path = require('path'),
    fs = require('fs'),
    assert = require('assert'),
    zombie = require('zombie'),
    events = require('events'),
    testRunner = require('./runner'),
    models;

// Модели Mongoose, определенные в приложении.
// На самом деле, я экспортирую их как app.User
models = ['User'];

function removeTestData(models, next) {
  // Удаляем тестовые данные
}

(function() {
  // Запускаем приложение, чтобы проверить его из Zombie
  app.listen(3001);

  // Чистим данные на каждом запуске
  removeTestData(models, function() {
    // Заготовочка / Фикстура
    var user = new app.User({
            'email' : 'alex@example.com',
            'password' : 'test'
        });
    user.save(start);
  });
})();

function teardown() {
  removeTestData(models, function() {
    process.exit();
  });
}

function start() {
  exports['test login'] = function() {
    // Тут будет код Zombie
  };
}

Всё начинается с само-исполняемой анонимной функции. Она запускает приложение на отдельном порту, очищает тестовые данные и создает пользователя. Всё это выполняется асинхронно, так что, если у нас есть несколько фикстур, то в самом последнем вызове save должна вызывать функция start. Expresso будет ждать пока не проэкспортируются все тесты, что позволяет нам использовать асинхронное Mongoose API и запускать тесты тогда, когда у нас всё готово.

Если вам всё понятно, то взгляните на следующий код теста:

// Используем тестовое окружение
process.env.NODE_ENV = 'test';

var app = require('../app'),
    path = require('path'),
    fs = require('fs'),
    assert = require('assert'),
    zombie = require('zombie'),
    events = require('events'),
    testRunner = require('./runner'),
    models;

models = ['User'];

function removeTestData(models, next) {
  var modelCount = models.length;
  models.forEach(function(modelName) {
    modelCount--;
    app[modelName].find().all(function(records) {
      var count = records.length;
      records.forEach(function(result) {
        result.remove();
        count--;
      });
      if (count === 0 && modelCount === 0) next();
    });
  });
}

(function() {
  // Запускаем приложение, чтобы проверить его из Zombie
  app.listen(3001);

  // Чистим данные на каждом запуске
  removeTestData(models, function() {
    // Заготовочка / Фикстура
    var user = new app.User({
            'email' : 'alex@example.com',
            'password' : 'test'
        });
    user.save(start);
  });
})();

function teardown() {
  removeTestData(models, function() {
    process.exit();
  });
}

function start() {
  exports['test login'] = function() {
    zombie.visit('http://localhost:3001/', function(err, browser, status) {
      // Заполняем email/пароль и подтверждаем форму
      browser.
        fill('user[email]', 'alex@example.com').
        fill('user[password]', 'test').
        pressButton('Log In', function(err, browser, status) {
          // Форма подтверждена, новая страница загружена
          assert.equal(browser.text('#header a.destroy'), 'Log Out');
          teardown();
        });
    });
  };
}

Код Zombie.js делает запрос на указанный URL (обратите внимание, что используется 3001 порт), после чего используя объект browser заполняется форма входа и подтверждается. После чего, используя стандартный assert.equal, сравнивается текстовый узел со строкой ‘Log Out’.

11.4. Формализуем процесс

Если мы хотим разделить тесты для Nodepad на несколько файлов, то повторение кода для Mongo фикстур будет выглядеть не очень хорошо. Поэтому попытаемся формализовать работу с фикстурами. Вот мой test/helper.js файл:

// Устанавливаем тестовое окружение
process.env.NODE_ENV = 'test';
var state = {
  models: []
};

function prepare(models, next) {
  var modelCount = models.length;
  models.forEach(function(model) {
    modelCount--;
    model.find().all(function(records) {
      var count = records.length;
      records.forEach(function(result) {
        result.remove();
        count--;
      });
      if (count === 0 && modelCount === 0) next();
    });
  });
};

module.exports = {
  run: function(e) {
    for (var test in state.tests) {
      e[test] = state.tests[test];
    }
  },

  setup: function(next) {
    prepare(state.models, next);
  },

  end: function() {
    prepare(state.models, process.exit);
  },

  set models(models) {
    state.models = models;
  },

  set tests(tests) {
    state.tests = tests;
  }
};

Чтобы безопасно удалять данные после каждого запуска набора тестов, я в настройках Nodepad’a определил БД test и именно для этой цели я принудительно устанавливаю окружение test. Функция prepare обходит все модели Mongoose и удаляет все ассоциированные данные (этот фрагмент взят из предыдущего примера).

Теперь тесты стали немного короче:

var app = require('../app'),
    assert = require('assert'),
    zombie = require('zombie'),
    events = require('events'),
    testHelper = require('./helper');

app.listen(3001);

testHelper.models = [app.User];

testHelper.setup(function() {
  // Фикстуры
  var user = new app.User({
        'email' : 'alex@example.com',
        'password' : 'test'
      });
  user.save(testHelper.run(exports));
});

testHelper.tests = {
  'test login': function() {
    zombie.visit('http://localhost:3001/', function(err, browser, status) {
      // Заполняем email, пароль и подтверждаем форму
      browser.
        fill('user[email]', 'alex@example.com').
        fill('user[password]', 'test').
        pressButton('Log In', function(err, browser, status) {
          // Форма подтверждена, загружена новая форма
          assert.equal(browser.text('#header a.destroy'), 'Log Out');
          testHelper.end();
        });
    });
  }
};

Обратили внимание, что я использовал сеттер для указания тестов и моделей? Я считаю, что это был интересный ходи решил его оставить.

11.5. Заключение

При написании тестов с Expresso важно помнить, что есть возможность откладывать запуск тестов, установив настройки в module.exports. В документации по Expresso TJ использует setTimeout, но помимо это можно пользовать и функциями обратного вызова (callback).

Если вы запустите мой код (не забудьте использовать expresso), то должны увидеть следущий результат:

alex@mog ~/Code/nodepad[git:master]$ expresso test/app.test.js
GET / 2 ms
GET /sessions/new 10 ms
GET /javascripts/application.js 1 ms
GET /javascripts/json.js 1 ms
POST /sessions 2 ms
GET /documents 8 ms
GET /javascripts/application.js 1 ms
GET /javascripts/json.js 1 ms
GET /documents/4d21d96f2410f20000000037.json 5 ms

   100% 1 tests

Zombie.js (или Tobi) — очень удобный для JavaScript разработчика способ тестирования, но при его использовании необходим какой-либо «запускатель» тестов. Многие используют Vows, а так же, я думаю, nodeunit - будет тоже хорошо работать.

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

Теперь, вы, по крайней мере, способны запустить тесты для Nodepad. Надеюсь, что на следующей неделе я смогу показать более подробный тест на Zombie.js.

Комит для текущей части — 6a269ce.

11.6. Ссылки