3 способа автоматического тестирования Javascript

November 6, 2012

С++ я уважал за мощь и строгость, Руби обожаю за работу с строками, массивами и хэшами, но к Javascript всегда относился и продолжаю относится холодно. Мне не нравится как осуществляется работа с this, смущает обилие операторов и зарезервированных слов undefined, null, Infinite, Nan, ==, ===, а также я плохо ориентируются в колбеках. Кроме того, я не прочитал ни одной книги по Javascript, что конечно же не способствует установлению приятельских отношений с этим языком.

Однако в ближайшие лет 5 вряд ли появится альтернатива для разработки на стороне клиента, поэтому с javascript придется работать еще очень долго. В проекте, в котором я сейчас работаю, накопилось достаточно большое количество клиентского кода и пришла пора его тестировать автоматически. Так как тема для меня новая, то я провел несколько эспериментов и сегодня хочу предложить вашему вниманию 3 простых способа сделать ваш javascript более надежным.

Оговорюсь, что у нас простой интерфейс, однако есть сложные алгоритмические куски кода на javascript. Поэтому наши задачи идеально подходят под классическое юнит-тестирование, про которое я буду рассказывать сегодня. Как тестировать сложный UI я пока не знаю, так как с такой задачей еще не сталкивался.

Способ 1. ExecJS

Предположим, что нам нужно протестировать функцию, которая удаляет все элементы массива, совпадающие с заданным:

// Удаляет все элементы e из массива
Array.prototype.remove = function(e) {
  for (var i = 0; i < this.length; i++) {
    if (this[i] === e) {
      this.splice(i, 1);
      i--;
    }
  }
  return this;
};

Автоматические тесты запускаются на сервере после каждого комита, поэтому желательно, что бы js-тесты встроились в этот процесс. К счастью в любом рейлс-приложении у нас уже есть все необходимые компоненты. Гем execjs, который используется при компилиции coffee-ассетов, можно использовать для выполнения произвольного кода на сервере.

Добавляем execjs в секцию test:

group :test do
  gem 'execjs'
end

Создаем спек для тестирования:

require 'spec_helper'

describe "array.js" do
  it "implements Array#remove method" do
    # Тестовые случаи
    spec = <<-JS
      var r1 = [1, 2, 2, 3].remove(2)
      var r2 = [1, 1, 1, 1].remove(1)
      // ...
    JS

    # Создаем контекст
    src = File.read(Rails.root.join('app/assets/javascripts/array.js'))
    js_context = ExecJS.compile(src + spec)

    # Проверка ожиданий
    js_context.eval('r1').should == [1, 3]
    js_context.eval('r2').should == []
    # ...
  end
end

Запускаем спеки:

~/proj/blog-2-js-testing(1.9.3-p194)[master]$ rspec
..

Finished in 0.32502 seconds
1 example, 0 failures

Вуаля, работает! Таким образом уже можно писать простые спеки.

Если файл, который нужно протестировать, написан на coffee-скрипте, то его можно скомпилировать с помощью гема coffee-script, который также подключен к каждому рейлс-приложению:

group :test do
  gem 'execjs'
  gem 'coffee-script'
end

компилируем его следующим образом:

  coffee = File.read(Rails.root.join('app/assets/javascripts/array.js'))
  src = CoffeeScript.compile(coffee)
  js_context = ExecJS.compile(src)

Конечно такого рода код для работы с js-файлами лучше выделить в отдельное место, но для проверки технологии можно оставить и так.

У данного метода тестирования есть много недостатков, главными из которых на мой взгляд являются: смешение js- и руби-кода в одном файле, а также возможная потеря информации на границе ruby и скрипта (undefined, null, Infinite все перейдут в nil, кроме того можно проверить только json-совместимые результаты). Главный положительный момент - тестирование органично встраивается в регулярный прогон тестов и не требует никаких дополнительных настроек.

Способ 2. Jasmine + ExecJS

Я слышал положительные отзывы о библиотеке jasmine. Синтаксис выглядит приятно и rspec-подобно. Поэтому решил модернизировать способ 1, чтобы писать спеки на чистом js (coffee).

Скачиваем файлы jasmine.js и ConsoleReporter.js, помещаем их в vendor/assets/javascripts.

Создаем файл, который будет запускать js-спеки. Он выглядит немного сложно, но так всегда происходит, когда мы начинаем решать нестандартную задачу на стыке языков.

# encoding: utf-8
require 'spec_helper'

# Запустить js-спеки, используя jasmine и execjs
describe 'JS specs' do
  def assets(js_files)
    Array(js_files).map{ |js_file|
      if File.extname(js_file) == '.coffee'
        CoffeeScript.compile File.read(Rails.root.join(js_file))
      else
        File.read(Rails.root.join(js_file))
      end
    }.join("\n")
  end

  ASSETS = [
    'vendor/assets/javascripts/jasmine.js',
    'vendor/assets/javascripts/ConsoleReporter.js',

    'app/assets/javascripts/array.js'
  ]

  Dir[Rails.root.join('spec/javascripts/**/*_spec.js*')].each do |asset|
    it "passed #{Pathname.new(asset).relative_path_from(Rails.root)}" do
      # Подкладываем переменную `exports`, которая нужна jasmine.js
      src = "var exports = {};\n"

      # Загружаем жасмин и тестируемый файл
      src += assets(ASSETS + [asset])

      # Подключаем jasmine reporter
      src += <<-JS
        var out = "";
        var env = jasmine.getEnv();

        // Собирать вывод мы будем в переменную `out`
        var reporter = new jasmine.ConsoleReporter(function(msg){ out += msg; });

        // Скажем jasmine не использовать setTimeout и все сделать в один поток
        env.updateInterval = null;

        // Запускаем js-cпеки
        env.addReporter(reporter);
        env.execute();
      JS

      js_context = ExecJS.compile(src)

      # Используем assert, чтобы вывод в случае ошибки был понятнее
      out = js_context.eval('out')
      js_specs_passed = (out =~ /\d+ specs?, 0 failures/)
      assert js_specs_passed, out
    end
  end
end

Пишем наш первый спек с помощью жасмина:

describe 'Array', ->
  it "#remove", ->
    expect([1, 2, 2, 3].remove(2)).toEqual([1, 3])
    expect([1, 1, 1, 1].remove(2)).toEqual([])

Запускаем:

~/proj/blog-2-js-testing(1.9.3-p194)[master]$ rspec
..

Finished in 0.57251 seconds
2 examples, 0 failures

Ура! Спеки пройдены.

Данный способ мне идеологически нравится гораздо больше, рассмотрим его недостатки:

  • Не учитываются зависимости между файлами.
  • Нет возможности протестировать js, который работает с DOM.
  • Сложно находить ошибку, когда спеки падают.
  • Плохая гибкость, так как приходится указывать файлы явно.

Способ 3. ???

Способ 3 я еще не придумал. Мне любопытно посмотреть на jasmine-gem и совместить его с phantomjs. Так же интересно поиграть с полтергейстом от Джона Лейгтона. Я буду рад, если вы поделитесь своим опытом тестирования javascript, так как тема важная, но мне показалось, что единого решения пока нет.

До новых встреч!

Ссылки

comments powered by Disqus